Skip to content

Commit e8c4940

Browse files
committed
Works for basic collection types
0 parents  commit e8c4940

16 files changed

+558
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
lib/
2+
node_modules/
3+
types/

bin/index.js

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
#!/usr/bin/env node
2+
require("../lib/index.js")

package-lock.json

+69
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "@oak-digital/types-4-strapi-2",
3+
"version": "0.1.0",
4+
"description": "Typescript interface generator for Strapi 4 models",
5+
"bin": {
6+
"t4s": "./bin/index.js"
7+
},
8+
"scripts": {
9+
"build": "tsc -p .",
10+
"t4s": "node ./bin/index.js"
11+
},
12+
"repository": {
13+
"type": "git",
14+
"url": "git+https://github.com/Oak-Digital/types-4-strapi-2.git"
15+
},
16+
"keywords": [
17+
"strapi",
18+
"typescript",
19+
"types",
20+
"generator"
21+
],
22+
"author": "Oak digital",
23+
"license": "MIT",
24+
"bugs": {
25+
"url": "https://github.com/Oak-Digital/types-4-strapi-2/issues"
26+
},
27+
"homepage": "https://github.com/Oak-Digital/types-4-strapi-2",
28+
"devDependencies": {
29+
"@types/node": "^18.7.2",
30+
"typescript": "^4.7.4"
31+
},
32+
"dependencies": {
33+
"commander": "^9.4.0"
34+
}
35+
}

src/index.ts

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { program } from 'commander'
2+
import * as fs from 'node:fs/promises'
3+
import InterfaceManager from './interface/InterfaceManager'
4+
5+
program
6+
.name("t4s")
7+
8+
program
9+
.option('-i, --in <dir>', 'The src directory for strapi', './src')
10+
.option('-o, --out <dir>', 'The output directory to output the types to', './types')
11+
12+
program.parse()
13+
const options = program.opts()
14+
const {
15+
in: input,
16+
out,
17+
} = options
18+
19+
const manager = new InterfaceManager(out, input)
20+
manager.run().catch((err) => {
21+
console.error(err)
22+
});

src/interface/Attributes.ts

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import Interface from "./Interface";
2+
3+
export default class Attributes {
4+
Attrs: Record<string, Record<string, any>>;
5+
private RelationNames: Record<string, [string, Interface]> = {};
6+
7+
constructor(attr: Record<string, Record<string, any>>, relationNames: Record<string, [string, Interface]>) {
8+
this.Attrs = attr;
9+
this.RelationNames = relationNames;
10+
}
11+
12+
isAttributeOptional(attr: any) {
13+
// If it is a component / relation / dynamiczone it is always optional due to population
14+
switch (attr.type) {
15+
case "component":
16+
case "dynamiczone":
17+
case "relation":
18+
return false;
19+
default:
20+
break;
21+
}
22+
23+
return attr.required !== true;
24+
}
25+
26+
getDependencies() {
27+
const dependencies = [];
28+
for (const attrName in this.Attrs) {
29+
const attr = this.Attrs[attrName];
30+
let dependencyName : string;
31+
switch (attr.type) {
32+
case "relation":
33+
dependencyName = attr.target;
34+
break;
35+
case "component":
36+
dependencyName = attr.component;
37+
break;
38+
default:
39+
continue;
40+
}
41+
dependencies.push(dependencyName);
42+
}
43+
return dependencies;
44+
}
45+
46+
attributeToString(attrName: string, attr: any) {
47+
let optionalString = this.isAttributeOptional(attr) ? '?' : '';
48+
let str = `${attrName}${optionalString}: `
49+
let isArray : boolean = false;
50+
switch (attr.type) {
51+
case "relation":
52+
const apiName = attr.target;
53+
const dependencyName = this.RelationNames[apiName][0];
54+
isArray = attr.relation.endsWith("ToMany");
55+
str += dependencyName;
56+
break;
57+
case "component":
58+
const componentName = attr.component;
59+
const dependencyComponentName = this.RelationNames[componentName][0];
60+
isArray = attr.repeatable ?? false;
61+
str += dependencyComponentName;
62+
break;
63+
case "password":
64+
return null;
65+
case "string":
66+
case "text":
67+
case "richtext":
68+
case "email":
69+
case "uid":
70+
str += "string";
71+
break;
72+
case "integer":
73+
case "biginteger":
74+
case "decimal":
75+
case "float":
76+
str += "number";
77+
break;
78+
case "date":
79+
case "datetime":
80+
case "time":
81+
str += "Date";
82+
break;
83+
case "boolean":
84+
str += attr.type;
85+
case "json":
86+
default:
87+
str += "any";
88+
break;
89+
}
90+
const isArrayString = isArray ? '[]' : ''
91+
str += `${isArrayString};`;
92+
return str;
93+
}
94+
95+
toString() : string {
96+
const strings = [ "{" ];
97+
for (const attrName in this.Attrs) {
98+
const attr = this.Attrs[attrName];
99+
const attrString = this.attributeToString(attrName, attr);
100+
if (attrString === null) {
101+
continue;
102+
}
103+
strings.push(attrString)
104+
}
105+
strings.push("}")
106+
return strings.join("\n");
107+
}
108+
}

src/interface/ComponentInterface.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import Interface from "./Interface";
2+
3+
export default class ComponentInterface extends Interface {
4+
5+
}

src/interface/Interface.ts

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { join, parse, relative } from "path";
2+
import Attributes from "./Attributes";
3+
4+
export default class Interface {
5+
private BaseName: string;
6+
private Relations: Interface[] = []; // Components and relations
7+
private RelationNames: Record<string, [string, Interface]> = {};
8+
private RelationNamesCounter: Record<string, number> = {};
9+
private NamePrefix: string = "";
10+
private Attributes: any;
11+
private RelativeDirectoryPath: string;
12+
private StrapiName: string;
13+
14+
constructor(baseName: string, attributes: any, relativeDirectoryPath: string, prefix: string = "") {
15+
this.BaseName = baseName;
16+
this.StrapiName = `api::${baseName}.${baseName}`;
17+
this.NamePrefix = prefix;
18+
this.Attributes = attributes;
19+
this.RelativeDirectoryPath = relativeDirectoryPath;
20+
}
21+
22+
getBaseName() {
23+
return this.BaseName;
24+
}
25+
26+
getStrapiName() {
27+
return this.StrapiName;
28+
}
29+
30+
getDependencies() {
31+
const attrs = new Attributes(this.Attributes, this.RelationNames);
32+
return attrs.getDependencies();
33+
}
34+
35+
getFullInterfaceName() {
36+
return `${this.NamePrefix}${this.BaseName}`;
37+
}
38+
39+
// For typescript import from index file
40+
getRelativeRootPath() {
41+
const path = join(this.RelativeDirectoryPath, this.getBaseName());
42+
const relativePath = (/^\.?\.\//).test(path) ? path : "./" + path;
43+
return relativePath;
44+
}
45+
46+
getRelativeRootPathFile() {
47+
return `${this.getRelativeRootPath()}.ts`
48+
}
49+
50+
setRelations(relations: Interface[]) {
51+
this.Relations = relations;
52+
this.RelationNames = {};
53+
this.Relations.forEach((inter: Interface) => {
54+
let name = inter.getBaseName();
55+
// Avoid duplicate names
56+
if (name in this.RelationNamesCounter) {
57+
name += ++this.RelationNamesCounter[name];
58+
} else {
59+
this.RelationNamesCounter[name] = 0;
60+
}
61+
this.RelationNames[inter.getStrapiName()] = [name, inter];
62+
})
63+
}
64+
65+
private getTsImports() {
66+
return Object.keys(this.RelationNames).map((strapiName: string) => {
67+
const relationName = this.RelationNames[strapiName][0];
68+
const inter = this.RelationNames[strapiName][1];
69+
const importPath = relative(this.getRelativeRootPath(), inter.getRelativeRootPath());
70+
const fullName = inter.getFullInterfaceName();
71+
const importNameString = fullName === relationName ? fullName : `${fullName} as ${relationName}`;
72+
return `import { ${importNameString} } from '${importPath}';`;
73+
}).join("\n");
74+
}
75+
76+
attributesToString() {
77+
let str = '';
78+
for (const attributeName in this.Attributes) {
79+
const attr = this.Attributes[attributeName];
80+
switch (attr.type) {
81+
default:
82+
83+
break;
84+
}
85+
}
86+
}
87+
88+
getInerfaceString() {
89+
let str = 'export interface ${this.getFullInterfaceName()} {\n';
90+
str += this.getInterfaceFieldsString();
91+
str += `}`
92+
return str;
93+
}
94+
95+
getInterfaceFieldsString() {
96+
let str: string;
97+
str += ` id: number;\n`;
98+
str += ` attributes: ${this.attributesToString()}\n`;
99+
return str;
100+
}
101+
102+
toString() {
103+
return this.getTsImports() + this.getInerfaceString();
104+
}
105+
}

0 commit comments

Comments
 (0)