Skip to content

Commit e9a324e

Browse files
authored
Merge pull request #784 from appwrite/feat-types-generator-command
feat: types generator command
2 parents 7592fa6 + 8a759ca commit e9a324e

File tree

13 files changed

+1103
-0
lines changed

13 files changed

+1103
-0
lines changed

src/SDK/Language/CLI.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,51 @@ public function getFiles(): array
181181
'destination' => 'lib/sdks.js',
182182
'template' => 'cli/lib/sdks.js.twig',
183183
],
184+
[
185+
'scope' => 'default',
186+
'destination' => 'lib/type-generation/attribute.js',
187+
'template' => 'cli/lib/type-generation/attribute.js.twig',
188+
],
189+
[
190+
'scope' => 'default',
191+
'destination' => 'lib/type-generation/languages/language.js',
192+
'template' => 'cli/lib/type-generation/languages/language.js.twig',
193+
],
194+
[
195+
'scope' => 'default',
196+
'destination' => 'lib/type-generation/languages/php.js',
197+
'template' => 'cli/lib/type-generation/languages/php.js.twig',
198+
],
199+
[
200+
'scope' => 'default',
201+
'destination' => 'lib/type-generation/languages/typescript.js',
202+
'template' => 'cli/lib/type-generation/languages/typescript.js.twig',
203+
],
204+
[
205+
'scope' => 'default',
206+
'destination' => 'lib/type-generation/languages/javascript.js',
207+
'template' => 'cli/lib/type-generation/languages/javascript.js.twig',
208+
],
209+
[
210+
'scope' => 'default',
211+
'destination' => 'lib/type-generation/languages/kotlin.js',
212+
'template' => 'cli/lib/type-generation/languages/kotlin.js.twig',
213+
],
214+
[
215+
'scope' => 'default',
216+
'destination' => 'lib/type-generation/languages/swift.js',
217+
'template' => 'cli/lib/type-generation/languages/swift.js.twig',
218+
],
219+
[
220+
'scope' => 'default',
221+
'destination' => 'lib/type-generation/languages/java.js',
222+
'template' => 'cli/lib/type-generation/languages/java.js.twig',
223+
],
224+
[
225+
'scope' => 'default',
226+
'destination' => 'lib/type-generation/languages/dart.js',
227+
'template' => 'cli/lib/type-generation/languages/dart.js.twig',
228+
],
184229
[
185230
'scope' => 'default',
186231
'destination' => 'lib/questions.js',
@@ -275,6 +320,11 @@ public function getFiles(): array
275320
'scope' => 'default',
276321
'destination' => 'lib/commands/organizations.js',
277322
'template' => 'cli/lib/commands/organizations.js.twig',
323+
],
324+
[
325+
'scope' => 'default',
326+
'destination' => 'lib/commands/types.js',
327+
'template' => 'cli/lib/commands/types.js.twig',
278328
]
279329
];
280330
}

templates/cli/index.js.twig

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const inquirer = require("inquirer");
1414
{% if sdk.test != "true" %}
1515
const { login, logout, whoami, migrate, register } = require("./lib/commands/generic");
1616
const { init } = require("./lib/commands/init");
17+
const { types } = require("./lib/commands/types");
1718
const { pull } = require("./lib/commands/pull");
1819
const { run } = require("./lib/commands/run");
1920
const { push, deploy } = require("./lib/commands/push");
@@ -68,6 +69,7 @@ program
6869
.addCommand(init)
6970
.addCommand(pull)
7071
.addCommand(push)
72+
.addCommand(types)
7173
.addCommand(deploy)
7274
.addCommand(run)
7375
.addCommand(logout)
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
const ejs = require("ejs");
2+
const fs = require("fs");
3+
const path = require("path");
4+
const { LanguageMeta, detectLanguage } = require("../type-generation/languages/language");
5+
const { Command, Option, Argument } = require("commander");
6+
const { localConfig } = require("../config");
7+
const { success, log, actionRunner } = require("../parser");
8+
const { PHP } = require("../type-generation/languages/php");
9+
const { TypeScript } = require("../type-generation/languages/typescript");
10+
const { Kotlin } = require("../type-generation/languages/kotlin");
11+
const { Swift } = require("../type-generation/languages/swift");
12+
const { Java } = require("../type-generation/languages/java");
13+
const { Dart } = require("../type-generation/languages/dart");
14+
const { JavaScript } = require("../type-generation/languages/javascript");
15+
16+
/**
17+
* @param {string} language
18+
* @returns {import("../type-generation/languages/language").LanguageMeta}
19+
*/
20+
function createLanguageMeta(language) {
21+
switch (language) {
22+
case "ts":
23+
return new TypeScript();
24+
case "js":
25+
return new JavaScript();
26+
case "php":
27+
return new PHP();
28+
case "kotlin":
29+
return new Kotlin();
30+
case "swift":
31+
return new Swift();
32+
case "java":
33+
return new Java();
34+
case "dart":
35+
return new Dart();
36+
default:
37+
throw new Error(`Language '${language}' is not supported`);
38+
}
39+
}
40+
41+
const templateHelpers = {
42+
toPascalCase: LanguageMeta.toPascalCase,
43+
toCamelCase: LanguageMeta.toCamelCase,
44+
toSnakeCase: LanguageMeta.toSnakeCase,
45+
toKebabCase: LanguageMeta.toKebabCase,
46+
toUpperSnakeCase: LanguageMeta.toUpperSnakeCase
47+
}
48+
49+
const typesOutputArgument = new Argument(
50+
"<output-directory>",
51+
"The directory to write the types to"
52+
);
53+
54+
const typesLanguageOption = new Option(
55+
"-l, --language <language>",
56+
"The language of the types"
57+
)
58+
.choices(["auto", "ts", "js", "php", "kotlin", "swift", "java", "dart"])
59+
.default("auto");
60+
61+
const typesCommand = actionRunner(async (rawOutputDirectory, {language}) => {
62+
if (language === "auto") {
63+
language = detectLanguage();
64+
log(`Detected language: ${language}`);
65+
}
66+
67+
const meta = createLanguageMeta(language);
68+
69+
const outputDirectory = path.resolve(rawOutputDirectory);
70+
if (!fs.existsSync(outputDirectory)) {
71+
log(`Directory: ${outputDirectory} does not exist, creating...`);
72+
fs.mkdirSync(outputDirectory, { recursive: true });
73+
}
74+
75+
if (!fs.existsSync("appwrite.json")) {
76+
throw new Error("appwrite.json not found in current directory");
77+
}
78+
79+
const collections = localConfig.getCollections();
80+
if (collections.length === 0) {
81+
throw new Error("No collections found in appwrite.json");
82+
}
83+
84+
log(`Found ${collections.length} collections: ${collections.map(c => c.name).join(", ")}`);
85+
86+
const totalAttributes = collections.reduce((count, collection) => count + collection.attributes.length, 0);
87+
log(`Found ${totalAttributes} attributes across all collections`);
88+
89+
const templater = ejs.compile(meta.getTemplate());
90+
91+
if (meta.isSingleFile()) {
92+
const content = templater({
93+
collections,
94+
...templateHelpers,
95+
getType: meta.getType
96+
});
97+
98+
const destination = path.join(outputDirectory, meta.getFileName());
99+
100+
fs.writeFileSync(destination, content);
101+
log(`Added types to ${destination}`);
102+
} else {
103+
for (const collection of collections) {
104+
const content = templater({
105+
collection,
106+
...templateHelpers,
107+
getType: meta.getType
108+
});
109+
110+
const destination = path.join(outputDirectory, meta.getFileName(collection));
111+
112+
fs.writeFileSync(destination, content);
113+
log(`Added types for ${collection.name} to ${destination}`);
114+
}
115+
}
116+
117+
success(`Generated types for all the listed collections`);
118+
});
119+
120+
const types = new Command("types")
121+
.description("Generate types for your Appwrite project")
122+
.addArgument(typesOutputArgument)
123+
.addOption(typesLanguageOption)
124+
.action(actionRunner(typesCommand));
125+
126+
module.exports = { types };
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const AttributeType = {
2+
STRING: "string",
3+
INTEGER: "integer",
4+
FLOAT: "float",
5+
BOOLEAN: "boolean",
6+
DATETIME: "datetime",
7+
EMAIL: "email",
8+
IP: "ip",
9+
URL: "url",
10+
ENUM: "enum",
11+
RELATIONSHIP: "relationship",
12+
};
13+
14+
module.exports = {
15+
AttributeType,
16+
};
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/** @typedef {import('../attribute').Attribute} Attribute */
2+
const { AttributeType } = require('../attribute');
3+
const { LanguageMeta } = require("./language");
4+
5+
class Dart extends LanguageMeta {
6+
getType(attribute) {
7+
let type = "";
8+
switch (attribute.type) {
9+
case AttributeType.STRING:
10+
case AttributeType.EMAIL:
11+
case AttributeType.DATETIME:
12+
type = "String";
13+
if (attribute.format === AttributeType.ENUM) {
14+
type = LanguageMeta.toPascalCase(attribute.key);
15+
}
16+
break;
17+
case AttributeType.INTEGER:
18+
type = "int";
19+
break;
20+
case AttributeType.FLOAT:
21+
type = "double";
22+
break;
23+
case AttributeType.BOOLEAN:
24+
type = "bool";
25+
break;
26+
case AttributeType.RELATIONSHIP:
27+
type = LanguageMeta.toPascalCase(attribute.relatedCollection);
28+
if ((attribute.relationType === 'oneToMany' && attribute.side === 'parent') || (attribute.relationType === 'manyToOne' && attribute.side === 'child') || attribute.relationType === 'manyToMany') {
29+
type = `List<${type}>`;
30+
}
31+
break;
32+
default:
33+
throw new Error(`Unknown attribute type: ${attribute.type}`);
34+
}
35+
if (attribute.array) {
36+
type = `List<${type}>`;
37+
}
38+
if (!attribute.required) {
39+
type += "?";
40+
}
41+
return type;
42+
}
43+
44+
getTemplate() {
45+
return `<% for (const attribute of collection.attributes) { -%>
46+
<% if (attribute.type === 'relationship') { -%>
47+
import '<%- attribute.relatedCollection.toLowerCase() %>.dart';
48+
49+
<% } -%>
50+
<% } -%>
51+
<% for (const attribute of collection.attributes) { -%>
52+
<% if (attribute.format === 'enum') { -%>
53+
enum <%- toPascalCase(attribute.key) %> {
54+
<% for (const element of attribute.elements) { -%>
55+
<%- element %>,
56+
<% } -%>
57+
}
58+
59+
<% } -%>
60+
<% } -%>
61+
class <%= toPascalCase(collection.name) %> {
62+
<% for (const [index, attribute] of Object.entries(collection.attributes)) { -%>
63+
<%- getType(attribute) %> <%= toCamelCase(attribute.key) %>;
64+
<% } -%>
65+
66+
<%= toPascalCase(collection.name) %>({
67+
<% for (const [index, attribute] of Object.entries(collection.attributes)) { -%>
68+
<% if (attribute.required) { %>required <% } %>this.<%= toCamelCase(attribute.key) %>,
69+
<% } -%>
70+
});
71+
72+
factory <%= toPascalCase(collection.name) %>.fromMap(Map<String, dynamic> map) {
73+
return <%= toPascalCase(collection.name) %>(
74+
<% for (const [index, attribute] of Object.entries(collection.attributes)) { -%>
75+
<%= toCamelCase(attribute.key) %>: <% if (attribute.type === 'string' || attribute.type === 'email' || attribute.type === 'datetime') { -%>
76+
<% if (attribute.format === 'enum') { -%>
77+
<% if (attribute.array) { -%>
78+
(map['<%= attribute.key %>'] as List<dynamic>?)?.map((e) => <%- toPascalCase(attribute.key) %>.values.firstWhere((element) => element.name == e)).toList()<% if (!attribute.required) { %> ?? []<% } -%>
79+
<% } else { -%>
80+
<% if (!attribute.required) { -%>
81+
map['<%= attribute.key %>'] != null ? <%- toPascalCase(attribute.key) %>.values.where((e) => e.name == map['<%= attribute.key %>']).firstOrNull : null<% } else { -%>
82+
<%- toPascalCase(attribute.key) %>.values.firstWhere((e) => e.name == map['<%= attribute.key %>'])<% } -%>
83+
<% } -%>
84+
<% } else { -%>
85+
<% if (attribute.array) { -%>
86+
List<String>.from(map['<%= attribute.key %>'] ?? [])<% if (!attribute.required) { %> ?? []<% } -%>
87+
<% } else { -%>
88+
map['<%= attribute.key %>']<% if (!attribute.required) { %>?<% } %>.toString()<% if (!attribute.required) { %> ?? null<% } -%>
89+
<% } -%>
90+
<% } -%>
91+
<% } else if (attribute.type === 'integer') { -%>
92+
<% if (attribute.array) { -%>
93+
List<int>.from(map['<%= attribute.key %>'] ?? [])<% if (!attribute.required) { %> ?? []<% } -%>
94+
<% } else { -%>
95+
map['<%= attribute.key %>']<% if (!attribute.required) { %> ?? null<% } -%>
96+
<% } -%>
97+
<% } else if (attribute.type === 'float') { -%>
98+
<% if (attribute.array) { -%>
99+
List<double>.from(map['<%= attribute.key %>'] ?? [])<% if (!attribute.required) { %> ?? []<% } -%>
100+
<% } else { -%>
101+
map['<%= attribute.key %>']<% if (!attribute.required) { %> ?? null<% } -%>
102+
<% } -%>
103+
<% } else if (attribute.type === 'boolean') { -%>
104+
<% if (attribute.array) { -%>
105+
List<bool>.from(map['<%= attribute.key %>'] ?? [])<% if (!attribute.required) { %> ?? []<% } -%>
106+
<% } else { -%>
107+
map['<%= attribute.key %>']<% if (!attribute.required) { %> ?? null<% } -%>
108+
<% } -%>
109+
<% } else if (attribute.type === 'relationship') { -%>
110+
<% if ((attribute.relationType === 'oneToMany' && attribute.side === 'parent') || (attribute.relationType === 'manyToOne' && attribute.side === 'child') || attribute.relationType === 'manyToMany') { -%>
111+
(map['<%= attribute.key %>'] as List<dynamic>?)?.map((e) => <%- toPascalCase(attribute.relatedCollection) %>.fromMap(e)).toList()<% if (!attribute.required) { %> ?? []<% } -%>
112+
<% } else { -%>
113+
<% if (!attribute.required) { -%>
114+
map['<%= attribute.key %>'] != null ? <%- toPascalCase(attribute.relatedCollection) %>.fromMap(map['<%= attribute.key %>']) : null<% } else { -%>
115+
<%- toPascalCase(attribute.relatedCollection) %>.fromMap(map['<%= attribute.key %>'])<% } -%>
116+
<% } -%>
117+
<% } -%>,
118+
<% } -%>
119+
);
120+
}
121+
122+
Map<String, dynamic> toMap() {
123+
return {
124+
<% for (const [index, attribute] of Object.entries(collection.attributes)) { -%>
125+
"<%= attribute.key %>": <% if (attribute.type === 'relationship') { -%>
126+
<% if ((attribute.relationType === 'oneToMany' && attribute.side === 'parent') || (attribute.relationType === 'manyToOne' && attribute.side === 'child') || attribute.relationType === 'manyToMany') { -%>
127+
<%= toCamelCase(attribute.key) %><% if (!attribute.required) { %>?<% } %>.map((e) => e.toMap()).toList()<% if (!attribute.required) { %> ?? []<% } -%>
128+
<% } else { -%>
129+
<%= toCamelCase(attribute.key) %><% if (!attribute.required) { %>?<% } %>.toMap()<% if (!attribute.required) { %> ?? {}<% } -%>
130+
<% } -%>
131+
<% } else if (attribute.format === 'enum') { -%>
132+
<% if (attribute.array) { -%>
133+
<%= toCamelCase(attribute.key) %><% if (!attribute.required) { %>?<% } %>.map((e) => e.name).toList()<% if (!attribute.required) { %> ?? []<% } -%>
134+
<% } else { -%>
135+
<%= toCamelCase(attribute.key) %><% if (!attribute.required) { %>?<% } %>.name<% if (!attribute.required) { %> ?? null<% } -%>
136+
<% } -%>
137+
<% } else { -%>
138+
<%= toCamelCase(attribute.key) -%>
139+
<% } -%>,
140+
<% } -%>
141+
};
142+
}
143+
}
144+
`;
145+
}
146+
147+
getFileName(collection) {
148+
return LanguageMeta.toSnakeCase(collection.name) + ".dart";
149+
}
150+
}
151+
152+
module.exports = { Dart };

0 commit comments

Comments
 (0)