appwrite-utils-cli 0.0.285 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/README.md +122 -96
  2. package/dist/collections/attributes.d.ts +4 -0
  3. package/dist/collections/attributes.js +224 -0
  4. package/dist/collections/indexes.d.ts +4 -0
  5. package/dist/collections/indexes.js +27 -0
  6. package/dist/collections/methods.d.ts +16 -0
  7. package/dist/collections/methods.js +216 -0
  8. package/dist/databases/methods.d.ts +6 -0
  9. package/dist/databases/methods.js +33 -0
  10. package/dist/interactiveCLI.d.ts +19 -0
  11. package/dist/interactiveCLI.js +555 -0
  12. package/dist/main.js +227 -62
  13. package/dist/migrations/afterImportActions.js +37 -40
  14. package/dist/migrations/appwriteToX.d.ts +26 -25
  15. package/dist/migrations/appwriteToX.js +42 -6
  16. package/dist/migrations/attributes.js +21 -20
  17. package/dist/migrations/backup.d.ts +93 -87
  18. package/dist/migrations/collections.d.ts +6 -0
  19. package/dist/migrations/collections.js +149 -20
  20. package/dist/migrations/converters.d.ts +2 -18
  21. package/dist/migrations/converters.js +13 -2
  22. package/dist/migrations/dataLoader.d.ts +276 -161
  23. package/dist/migrations/dataLoader.js +535 -292
  24. package/dist/migrations/databases.js +8 -2
  25. package/dist/migrations/helper.d.ts +3 -0
  26. package/dist/migrations/helper.js +21 -0
  27. package/dist/migrations/importController.d.ts +5 -2
  28. package/dist/migrations/importController.js +125 -88
  29. package/dist/migrations/importDataActions.d.ts +9 -1
  30. package/dist/migrations/importDataActions.js +15 -3
  31. package/dist/migrations/indexes.js +3 -2
  32. package/dist/migrations/logging.js +20 -8
  33. package/dist/migrations/migrationHelper.d.ts +9 -4
  34. package/dist/migrations/migrationHelper.js +6 -5
  35. package/dist/migrations/openapi.d.ts +1 -1
  36. package/dist/migrations/openapi.js +33 -18
  37. package/dist/migrations/queue.js +3 -2
  38. package/dist/migrations/relationships.d.ts +2 -2
  39. package/dist/migrations/schemaStrings.js +53 -41
  40. package/dist/migrations/setupDatabase.d.ts +2 -4
  41. package/dist/migrations/setupDatabase.js +24 -105
  42. package/dist/migrations/storage.d.ts +3 -1
  43. package/dist/migrations/storage.js +110 -16
  44. package/dist/migrations/transfer.d.ts +30 -0
  45. package/dist/migrations/transfer.js +337 -0
  46. package/dist/migrations/users.d.ts +2 -1
  47. package/dist/migrations/users.js +78 -43
  48. package/dist/schemas/authUser.d.ts +2 -2
  49. package/dist/storage/methods.d.ts +15 -0
  50. package/dist/storage/methods.js +207 -0
  51. package/dist/storage/schemas.d.ts +687 -0
  52. package/dist/storage/schemas.js +175 -0
  53. package/dist/utils/getClientFromConfig.d.ts +4 -0
  54. package/dist/utils/getClientFromConfig.js +16 -0
  55. package/dist/utils/helperFunctions.d.ts +11 -1
  56. package/dist/utils/helperFunctions.js +38 -0
  57. package/dist/utils/retryFailedPromises.d.ts +2 -0
  58. package/dist/utils/retryFailedPromises.js +21 -0
  59. package/dist/utils/schemaStrings.d.ts +13 -0
  60. package/dist/utils/schemaStrings.js +403 -0
  61. package/dist/utils/setupFiles.js +110 -61
  62. package/dist/utilsController.d.ts +40 -22
  63. package/dist/utilsController.js +164 -84
  64. package/package.json +13 -15
  65. package/src/collections/attributes.ts +483 -0
  66. package/src/collections/indexes.ts +53 -0
  67. package/src/collections/methods.ts +331 -0
  68. package/src/databases/methods.ts +47 -0
  69. package/src/init.ts +64 -64
  70. package/src/interactiveCLI.ts +767 -0
  71. package/src/main.ts +292 -83
  72. package/src/migrations/afterImportActions.ts +553 -490
  73. package/src/migrations/appwriteToX.ts +237 -174
  74. package/src/migrations/attributes.ts +483 -422
  75. package/src/migrations/backup.ts +205 -205
  76. package/src/migrations/collections.ts +545 -300
  77. package/src/migrations/converters.ts +161 -150
  78. package/src/migrations/dataLoader.ts +1615 -1304
  79. package/src/migrations/databases.ts +44 -25
  80. package/src/migrations/dbHelpers.ts +92 -92
  81. package/src/migrations/helper.ts +40 -0
  82. package/src/migrations/importController.ts +448 -384
  83. package/src/migrations/importDataActions.ts +315 -307
  84. package/src/migrations/indexes.ts +40 -37
  85. package/src/migrations/logging.ts +29 -16
  86. package/src/migrations/migrationHelper.ts +207 -201
  87. package/src/migrations/openapi.ts +83 -70
  88. package/src/migrations/queue.ts +118 -119
  89. package/src/migrations/relationships.ts +324 -324
  90. package/src/migrations/schemaStrings.ts +472 -460
  91. package/src/migrations/setupDatabase.ts +118 -219
  92. package/src/migrations/storage.ts +538 -358
  93. package/src/migrations/transfer.ts +608 -0
  94. package/src/migrations/users.ts +362 -285
  95. package/src/migrations/validationRules.ts +63 -63
  96. package/src/schemas/authUser.ts +23 -23
  97. package/src/setup.ts +8 -8
  98. package/src/storage/methods.ts +371 -0
  99. package/src/storage/schemas.ts +205 -0
  100. package/src/types.ts +9 -9
  101. package/src/utils/getClientFromConfig.ts +17 -0
  102. package/src/utils/helperFunctions.ts +181 -127
  103. package/src/utils/index.ts +2 -2
  104. package/src/utils/loadConfigs.ts +59 -59
  105. package/src/utils/retryFailedPromises.ts +27 -0
  106. package/src/utils/schemaStrings.ts +473 -0
  107. package/src/utils/setupFiles.ts +228 -182
  108. package/src/utilsController.ts +325 -194
  109. package/tsconfig.json +37 -37
@@ -1,460 +1,472 @@
1
- import { toCamelCase, toPascalCase } from "../utils/index.js";
2
- import type {
3
- AppwriteConfig,
4
- Attribute,
5
- RelationshipAttribute,
6
- } from "appwrite-utils";
7
- import { z } from "zod";
8
- import fs from "fs";
9
- import path from "path";
10
- import { dump } from "js-yaml";
11
- import { getDatabaseFromConfig } from "./afterImportActions.js";
12
-
13
- interface RelationshipDetail {
14
- parentCollection: string;
15
- childCollection: string;
16
- parentKey: string;
17
- childKey: string;
18
- isArray: boolean;
19
- isChild: boolean;
20
- }
21
-
22
- export class SchemaGenerator {
23
- private relationshipMap = new Map<string, RelationshipDetail[]>();
24
- private config: AppwriteConfig;
25
- private appwriteFolderPath: string;
26
-
27
- constructor(config: AppwriteConfig, appwriteFolderPath: string) {
28
- this.config = config;
29
- this.appwriteFolderPath = appwriteFolderPath;
30
- this.extractRelationships();
31
- }
32
-
33
- public updateTsSchemas(): void {
34
- const collections = this.config.collections;
35
- delete this.config.collections;
36
-
37
- const configPath = path.join(this.appwriteFolderPath, "appwriteConfig.ts");
38
- const configContent = `import { AppwriteConfig } from "appwrite-utils";
39
-
40
- const appwriteConfig: AppwriteConfig = {
41
- appwriteEndpoint: "${this.config.appwriteEndpoint}",
42
- appwriteProject: "${this.config.appwriteProject}",
43
- appwriteKey: "${this.config.appwriteKey}",
44
- enableDevDatabase: ${this.config.enableDevDatabase},
45
- enableBackups: ${this.config.enableBackups},
46
- backupInterval: ${this.config.backupInterval},
47
- backupRetention: ${this.config.backupRetention},
48
- enableBackupCleanup: ${this.config.enableBackupCleanup},
49
- enableMockData: ${this.config.enableMockData},
50
- enableWipeOtherDatabases: ${this.config.enableWipeOtherDatabases},
51
- documentBucketId: "${this.config.documentBucketId}",
52
- usersCollectionName: "${this.config.usersCollectionName}",
53
- databases: ${JSON.stringify(this.config.databases)}
54
- };
55
-
56
- export default appwriteConfig;
57
- `;
58
- fs.writeFileSync(configPath, configContent, { encoding: "utf-8" });
59
-
60
- const collectionsFolderPath = path.join(
61
- this.appwriteFolderPath,
62
- "collections"
63
- );
64
- if (!fs.existsSync(collectionsFolderPath)) {
65
- fs.mkdirSync(collectionsFolderPath, { recursive: true });
66
- }
67
-
68
- collections?.forEach((collection) => {
69
- const { databaseId, ...collectionWithoutDbId } = collection; // Destructure to exclude databaseId
70
- const collectionFilePath = path.join(
71
- collectionsFolderPath,
72
- `${collection.name}.ts`
73
- );
74
- const collectionContent = `import { CollectionCreate } from "appwrite-utils";
75
-
76
- const ${collection.name}Config: Partial<CollectionCreate> = {
77
- name: "${collection.name}",
78
- $id: "${collection.$id}",
79
- enabled: ${collection.enabled},
80
- documentSecurity: ${collection.documentSecurity},
81
- $permissions: [
82
- ${collection.$permissions
83
- .map(
84
- (permission) =>
85
- `{ permission: "${permission.permission}", target: "${permission.target}" }`
86
- )
87
- .join(",\n ")}
88
- ],
89
- attributes: [
90
- ${collection.attributes
91
- .map((attr) => {
92
- return `{ ${Object.entries(attr)
93
- .map(([key, value]) => {
94
- // Check the type of the value and format it accordingly
95
- if (typeof value === "string") {
96
- // If the value is a string, wrap it in quotes
97
- return `${key}: "${value.replace(/"/g, '\\"')}"`; // Escape existing quotes in the string
98
- } else if (Array.isArray(value)) {
99
- // If the value is an array, join it with commas
100
- if (value.length > 0) {
101
- return `${key}: [${value
102
- .map((item) => `"${item}"`)
103
- .join(", ")}]`;
104
- } else {
105
- return `${key}: []`;
106
- }
107
- } else {
108
- // If the value is not a string (e.g., boolean or number), output it directly
109
- return `${key}: ${value}`;
110
- }
111
- })
112
- .join(", ")} }`;
113
- })
114
- .join(",\n ")}
115
- ],
116
- indexes: [
117
- ${(
118
- collection.indexes?.map((index) => {
119
- // Map each attribute to ensure it is properly quoted
120
- const formattedAttributes = index.attributes
121
- .map((attr) => `"${attr}"`)
122
- .join(", ");
123
- return `{ key: "${index.key}", type: "${
124
- index.type
125
- }", attributes: [${formattedAttributes}], orders: [${
126
- index.orders
127
- ?.filter((order) => order !== null)
128
- .map((order) => `"${order}"`)
129
- .join(", ") ?? ""
130
- }] }`;
131
- }) ?? []
132
- ).join(",\n ")}
133
- ]
134
- };
135
-
136
- export default ${collection.name}Config;
137
- `;
138
- fs.writeFileSync(collectionFilePath, collectionContent, {
139
- encoding: "utf-8",
140
- });
141
- console.log(`Collection schema written to ${collectionFilePath}`);
142
- });
143
- }
144
-
145
- private extractRelationships(): void {
146
- if (!this.config.collections) {
147
- return;
148
- }
149
- this.config.collections.forEach((collection) => {
150
- collection.attributes.forEach((attr) => {
151
- if (attr.type === "relationship" && attr.twoWay && attr.twoWayKey) {
152
- const relationshipAttr = attr as RelationshipAttribute;
153
- let isArrayParent = false;
154
- let isArrayChild = false;
155
- switch (relationshipAttr.relationType) {
156
- case "oneToMany":
157
- isArrayParent = true;
158
- isArrayChild = false;
159
- break;
160
- case "manyToMany":
161
- isArrayParent = true;
162
- isArrayChild = true;
163
- break;
164
- case "oneToOne":
165
- isArrayParent = false;
166
- isArrayChild = false;
167
- break;
168
- case "manyToOne":
169
- isArrayParent = false;
170
- isArrayChild = true;
171
- break;
172
- default:
173
- break;
174
- }
175
- this.addRelationship(
176
- collection.name,
177
- relationshipAttr.relatedCollection,
178
- attr.key,
179
- relationshipAttr.twoWayKey,
180
- isArrayParent,
181
- isArrayChild
182
- );
183
- console.log(
184
- `Extracted relationship: ${attr.key}\n\t${collection.name} -> ${relationshipAttr.relatedCollection}, databaseId: ${collection.databaseId}`
185
- );
186
- }
187
- });
188
- });
189
- }
190
-
191
- private addRelationship(
192
- parentCollection: string,
193
- childCollection: string,
194
- parentKey: string,
195
- childKey: string,
196
- isArrayParent: boolean,
197
- isArrayChild: boolean
198
- ): void {
199
- const relationshipsChild = this.relationshipMap.get(childCollection) || [];
200
- const relationshipsParent =
201
- this.relationshipMap.get(parentCollection) || [];
202
- relationshipsParent.push({
203
- parentCollection,
204
- childCollection,
205
- parentKey,
206
- childKey,
207
- isArray: isArrayParent,
208
- isChild: false,
209
- });
210
- relationshipsChild.push({
211
- parentCollection,
212
- childCollection,
213
- parentKey,
214
- childKey,
215
- isArray: isArrayChild,
216
- isChild: true,
217
- });
218
- this.relationshipMap.set(childCollection, relationshipsChild);
219
- this.relationshipMap.set(parentCollection, relationshipsParent);
220
- }
221
-
222
- public generateSchemas(): void {
223
- if (!this.config.collections) {
224
- return;
225
- }
226
- this.config.collections.forEach((collection) => {
227
- const schemaString = this.createSchemaString(
228
- collection.name,
229
- collection.attributes
230
- );
231
- const camelCaseName = toCamelCase(collection.name);
232
- const schemaPath = path.join(
233
- this.appwriteFolderPath,
234
- "schemas",
235
- `${camelCaseName}.ts`
236
- );
237
- fs.writeFileSync(schemaPath, schemaString, { encoding: "utf-8" });
238
- console.log(`Schema written to ${schemaPath}`);
239
- });
240
- }
241
-
242
- createSchemaString = (name: string, attributes: Attribute[]): string => {
243
- const pascalName = toPascalCase(name);
244
- let imports = `import { z } from "zod";\n`;
245
-
246
- // Use the relationshipMap to find related collections
247
- const relationshipDetails = this.relationshipMap.get(name) || [];
248
- const relatedCollections = relationshipDetails
249
- .filter((detail, index, self) => {
250
- const uniqueKey = `${detail.parentCollection}-${detail.childCollection}-${detail.parentKey}-${detail.childKey}`;
251
- return (
252
- index ===
253
- self.findIndex(
254
- (obj) =>
255
- `${obj.parentCollection}-${obj.childCollection}-${obj.parentKey}-${obj.childKey}` ===
256
- uniqueKey
257
- )
258
- );
259
- })
260
- .map((detail) => {
261
- const relatedCollectionName = detail.isChild
262
- ? detail.parentCollection
263
- : detail.childCollection;
264
- const key = detail.isChild ? detail.childKey : detail.parentKey;
265
- const isArray = detail.isArray ? "array" : "";
266
- return [relatedCollectionName, key, isArray];
267
- });
268
-
269
- let relatedTypes = "";
270
- let relatedTypesLazy = "";
271
- let curNum = 0;
272
- let maxNum = relatedCollections.length;
273
- relatedCollections.forEach((relatedCollection) => {
274
- console.log(relatedCollection);
275
- let relatedPascalName = toPascalCase(relatedCollection[0]);
276
- let relatedCamelName = toCamelCase(relatedCollection[0]);
277
- curNum++;
278
- let endNameTypes = relatedPascalName;
279
- let endNameLazy = `${relatedPascalName}Schema`;
280
- if (relatedCollection[2] === "array") {
281
- endNameTypes += "[]";
282
- endNameLazy += ".array().default([])";
283
- } else if (!(relatedCollection[2] === "array")) {
284
- endNameTypes += " | null";
285
- endNameLazy += ".nullish()";
286
- }
287
- imports += `import { ${relatedPascalName}Schema, type ${relatedPascalName} } from "./${relatedCamelName}";\n`;
288
- relatedTypes += `${relatedCollection[1]}?: ${endNameTypes};\n`;
289
- if (relatedTypes.length > 0 && curNum !== maxNum) {
290
- relatedTypes += " ";
291
- }
292
- relatedTypesLazy += `${relatedCollection[1]}: z.lazy(() => ${endNameLazy}),\n`;
293
- if (relatedTypesLazy.length > 0 && curNum !== maxNum) {
294
- relatedTypesLazy += " ";
295
- }
296
- });
297
-
298
- let schemaString = `${imports}\n\n`;
299
- schemaString += `export const ${pascalName}SchemaBase = z.object({\n`;
300
- schemaString += ` $id: z.string().optional(),\n`;
301
- schemaString += ` $createdAt: z.date().or(z.string()).optional(),\n`;
302
- schemaString += ` $updatedAt: z.date().or(z.string()).optional(),\n`;
303
- for (const attribute of attributes) {
304
- if (attribute.type === "relationship") {
305
- continue;
306
- }
307
- schemaString += ` ${attribute.key}: ${this.typeToZod(attribute)},\n`;
308
- }
309
- schemaString += `});\n\n`;
310
- schemaString += `export type ${pascalName}Base = z.infer<typeof ${pascalName}SchemaBase>`;
311
- if (relatedTypes.length > 0) {
312
- schemaString += ` & {\n ${relatedTypes}};\n\n`;
313
- } else {
314
- schemaString += `;\n\n`;
315
- }
316
- schemaString += `export const ${pascalName}Schema: z.ZodType<${pascalName}Base> = ${pascalName}SchemaBase`;
317
- if (relatedTypes.length > 0) {
318
- schemaString += `.extend({\n ${relatedTypesLazy}});\n\n`;
319
- } else {
320
- schemaString += `;\n`;
321
- }
322
- schemaString += `export type ${pascalName} = z.infer<typeof ${pascalName}Schema>;\n\n`;
323
-
324
- return schemaString;
325
- };
326
-
327
- typeToZod = (attribute: Attribute) => {
328
- let baseSchemaCode = "";
329
- const finalAttribute: Attribute = (
330
- attribute.type === "string" &&
331
- attribute.format &&
332
- attribute.format === "enum" &&
333
- attribute.type === "string"
334
- ? { ...attribute, type: attribute.format }
335
- : attribute
336
- ) as Attribute;
337
- switch (finalAttribute.type) {
338
- case "string":
339
- baseSchemaCode = "z.string()";
340
- if (finalAttribute.size) {
341
- baseSchemaCode += `.max(${finalAttribute.size}, "Maximum length of ${finalAttribute.size} characters exceeded")`;
342
- }
343
- if (finalAttribute.xdefault !== undefined) {
344
- baseSchemaCode += `.default("${finalAttribute.xdefault}")`;
345
- }
346
- if (!attribute.required && !attribute.array) {
347
- baseSchemaCode += ".nullish()";
348
- }
349
- break;
350
- case "integer":
351
- baseSchemaCode = "z.number().int()";
352
- if (finalAttribute.min !== undefined) {
353
- if (BigInt(finalAttribute.min) === BigInt(-9223372036854776000)) {
354
- delete finalAttribute.min;
355
- } else {
356
- baseSchemaCode += `.min(${finalAttribute.min}, "Minimum value of ${finalAttribute.min} not met")`;
357
- }
358
- }
359
- if (finalAttribute.max !== undefined) {
360
- if (BigInt(finalAttribute.max) === BigInt(9223372036854776000)) {
361
- delete finalAttribute.max;
362
- } else {
363
- baseSchemaCode += `.max(${finalAttribute.max}, "Maximum value of ${finalAttribute.max} exceeded")`;
364
- }
365
- }
366
- if (finalAttribute.xdefault !== undefined) {
367
- baseSchemaCode += `.default(${finalAttribute.xdefault})`;
368
- }
369
- if (!finalAttribute.required && !finalAttribute.array) {
370
- baseSchemaCode += ".nullish()";
371
- }
372
- break;
373
- case "float":
374
- baseSchemaCode = "z.number()";
375
- if (finalAttribute.min !== undefined) {
376
- baseSchemaCode += `.min(${finalAttribute.min}, "Minimum value of ${finalAttribute.min} not met")`;
377
- }
378
- if (finalAttribute.max !== undefined) {
379
- baseSchemaCode += `.max(${finalAttribute.max}, "Maximum value of ${finalAttribute.max} exceeded")`;
380
- }
381
- if (finalAttribute.xdefault !== undefined) {
382
- baseSchemaCode += `.default(${finalAttribute.xdefault})`;
383
- }
384
- if (!finalAttribute.required && !finalAttribute.array) {
385
- baseSchemaCode += ".nullish()";
386
- }
387
- break;
388
- case "boolean":
389
- baseSchemaCode = "z.boolean()";
390
- if (finalAttribute.xdefault !== undefined) {
391
- baseSchemaCode += `.default(${finalAttribute.xdefault})`;
392
- }
393
- if (!finalAttribute.required && !finalAttribute.array) {
394
- baseSchemaCode += ".nullish()";
395
- }
396
- break;
397
- case "datetime":
398
- baseSchemaCode = "z.date()";
399
- if (finalAttribute.xdefault !== undefined) {
400
- baseSchemaCode += `.default(new Date("${finalAttribute.xdefault}"))`;
401
- }
402
- if (!finalAttribute.required && !finalAttribute.array) {
403
- baseSchemaCode += ".nullish()";
404
- }
405
- break;
406
- case "email":
407
- baseSchemaCode = "z.string().email()";
408
- if (finalAttribute.xdefault !== undefined) {
409
- baseSchemaCode += `.default("${finalAttribute.xdefault}")`;
410
- }
411
- if (!finalAttribute.required && !finalAttribute.array) {
412
- baseSchemaCode += ".nullish()";
413
- }
414
- break;
415
- case "ip":
416
- baseSchemaCode = "z.string()"; // Add custom validation as needed
417
- if (finalAttribute.xdefault !== undefined) {
418
- baseSchemaCode += `.default("${finalAttribute.xdefault}")`;
419
- }
420
- if (!finalAttribute.required && !finalAttribute.array) {
421
- baseSchemaCode += ".nullish()";
422
- }
423
- break;
424
- case "url":
425
- baseSchemaCode = "z.string().url()";
426
- if (finalAttribute.xdefault !== undefined) {
427
- baseSchemaCode += `.default("${finalAttribute.xdefault}")`;
428
- }
429
- if (!finalAttribute.required && !finalAttribute.array) {
430
- baseSchemaCode += ".nullish()";
431
- }
432
- break;
433
- case "enum":
434
- baseSchemaCode = `z.enum([${finalAttribute.elements
435
- .map((element) => `"${element}"`)
436
- .join(", ")}])`;
437
- if (finalAttribute.xdefault !== undefined) {
438
- baseSchemaCode += `.default("${finalAttribute.xdefault}")`;
439
- }
440
- if (!attribute.required && !attribute.array) {
441
- baseSchemaCode += ".nullish()";
442
- }
443
- break;
444
- case "relationship":
445
- break;
446
- default:
447
- baseSchemaCode = "z.any()";
448
- }
449
-
450
- // Handle arrays
451
- if (attribute.array) {
452
- baseSchemaCode = `z.array(${baseSchemaCode})`;
453
- }
454
- if (attribute.array && !attribute.required) {
455
- baseSchemaCode += ".nullish()";
456
- }
457
-
458
- return baseSchemaCode;
459
- };
460
- }
1
+ import { toCamelCase, toPascalCase } from "../utils/index.js";
2
+ import type {
3
+ AppwriteConfig,
4
+ Attribute,
5
+ RelationshipAttribute,
6
+ } from "appwrite-utils";
7
+ import { z } from "zod";
8
+ import fs from "fs";
9
+ import path from "path";
10
+ import { dump } from "js-yaml";
11
+ import { getDatabaseFromConfig } from "./afterImportActions.js";
12
+
13
+ interface RelationshipDetail {
14
+ parentCollection: string;
15
+ childCollection: string;
16
+ parentKey: string;
17
+ childKey: string;
18
+ isArray: boolean;
19
+ isChild: boolean;
20
+ }
21
+
22
+ export class SchemaGenerator {
23
+ private relationshipMap = new Map<string, RelationshipDetail[]>();
24
+ private config: AppwriteConfig;
25
+ private appwriteFolderPath: string;
26
+
27
+ constructor(config: AppwriteConfig, appwriteFolderPath: string) {
28
+ this.config = config;
29
+ this.appwriteFolderPath = appwriteFolderPath;
30
+ this.extractRelationships();
31
+ }
32
+
33
+ public updateTsSchemas(): void {
34
+ const collections = this.config.collections;
35
+ delete this.config.collections;
36
+
37
+ const configPath = path.join(this.appwriteFolderPath, "appwriteConfig.ts");
38
+ const configContent = `import { type AppwriteConfig } from "appwrite-utils";
39
+
40
+ const appwriteConfig: AppwriteConfig = {
41
+ appwriteEndpoint: "${this.config.appwriteEndpoint}",
42
+ appwriteProject: "${this.config.appwriteProject}",
43
+ appwriteKey: "${this.config.appwriteKey}",
44
+ enableBackups: ${this.config.enableBackups},
45
+ backupInterval: ${this.config.backupInterval},
46
+ backupRetention: ${this.config.backupRetention},
47
+ enableBackupCleanup: ${this.config.enableBackupCleanup},
48
+ enableMockData: ${this.config.enableMockData},
49
+ documentBucketId: "${this.config.documentBucketId}",
50
+ usersCollectionName: "${this.config.usersCollectionName}",
51
+ databases: ${JSON.stringify(this.config.databases)},
52
+ buckets: ${JSON.stringify(this.config.buckets)}
53
+ };
54
+
55
+ export default appwriteConfig;
56
+ `;
57
+ fs.writeFileSync(configPath, configContent, { encoding: "utf-8" });
58
+
59
+ const collectionsFolderPath = path.join(
60
+ this.appwriteFolderPath,
61
+ "collections"
62
+ );
63
+ if (!fs.existsSync(collectionsFolderPath)) {
64
+ fs.mkdirSync(collectionsFolderPath, { recursive: true });
65
+ }
66
+
67
+ collections?.forEach((collection) => {
68
+ const { databaseId, ...collectionWithoutDbId } = collection; // Destructure to exclude databaseId
69
+ const collectionFilePath = path.join(
70
+ collectionsFolderPath,
71
+ `${collection.name}.ts`
72
+ );
73
+ const collectionContent = `import { type CollectionCreate } from "appwrite-utils";
74
+
75
+ const ${collection.name}Config: Partial<CollectionCreate> = {
76
+ name: "${collection.name}",
77
+ $id: "${collection.$id}",
78
+ enabled: ${collection.enabled},
79
+ documentSecurity: ${collection.documentSecurity},
80
+ $permissions: [
81
+ ${collection.$permissions
82
+ .map(
83
+ (permission) =>
84
+ `{ permission: "${permission.permission}", target: "${permission.target}" }`
85
+ )
86
+ .join(",\n ")}
87
+ ],
88
+ attributes: [
89
+ ${collection.attributes
90
+ .map((attr) => {
91
+ return `{ ${Object.entries(attr)
92
+ .map(([key, value]) => {
93
+ // Check the type of the value and format it accordingly
94
+ if (typeof value === "string") {
95
+ // If the value is a string, wrap it in quotes
96
+ return `${key}: "${value.replace(/"/g, '\\"')}"`; // Escape existing quotes in the string
97
+ } else if (Array.isArray(value)) {
98
+ // If the value is an array, join it with commas
99
+ if (value.length > 0) {
100
+ return `${key}: [${value
101
+ .map((item) => `"${item}"`)
102
+ .join(", ")}]`;
103
+ } else {
104
+ return `${key}: []`;
105
+ }
106
+ } else {
107
+ // If the value is not a string (e.g., boolean or number), output it directly
108
+ return `${key}: ${value}`;
109
+ }
110
+ })
111
+ .join(", ")} }`;
112
+ })
113
+ .join(",\n ")}
114
+ ],
115
+ indexes: [
116
+ ${(
117
+ collection.indexes?.map((index) => {
118
+ // Map each attribute to ensure it is properly quoted
119
+ const formattedAttributes =
120
+ index.attributes.map((attr) => `"${attr}"`).join(", ") ?? "";
121
+ return `{ key: "${index.key}", type: "${
122
+ index.type
123
+ }", attributes: [${formattedAttributes}], orders: [${
124
+ index.orders
125
+ ?.filter((order) => order !== null)
126
+ .map((order) => `"${order}"`)
127
+ .join(", ") ?? ""
128
+ }] }`;
129
+ }) ?? []
130
+ ).join(",\n ")}
131
+ ]
132
+ };
133
+
134
+ export default ${collection.name}Config;
135
+ `;
136
+ fs.writeFileSync(collectionFilePath, collectionContent, {
137
+ encoding: "utf-8",
138
+ });
139
+ console.log(`Collection schema written to ${collectionFilePath}`);
140
+ });
141
+ }
142
+
143
+ private extractRelationships(): void {
144
+ if (!this.config.collections) {
145
+ return;
146
+ }
147
+ this.config.collections.forEach((collection) => {
148
+ collection.attributes.forEach((attr) => {
149
+ if (attr.type === "relationship" && attr.twoWay && attr.twoWayKey) {
150
+ const relationshipAttr = attr as RelationshipAttribute;
151
+ let isArrayParent = false;
152
+ let isArrayChild = false;
153
+ switch (relationshipAttr.relationType) {
154
+ case "oneToMany":
155
+ isArrayParent = true;
156
+ isArrayChild = false;
157
+ break;
158
+ case "manyToMany":
159
+ isArrayParent = true;
160
+ isArrayChild = true;
161
+ break;
162
+ case "oneToOne":
163
+ isArrayParent = false;
164
+ isArrayChild = false;
165
+ break;
166
+ case "manyToOne":
167
+ isArrayParent = false;
168
+ isArrayChild = true;
169
+ break;
170
+ default:
171
+ break;
172
+ }
173
+ this.addRelationship(
174
+ collection.name,
175
+ relationshipAttr.relatedCollection,
176
+ attr.key,
177
+ relationshipAttr.twoWayKey,
178
+ isArrayParent,
179
+ isArrayChild
180
+ );
181
+ console.log(
182
+ `Extracted relationship: ${attr.key}\n\t${collection.name} -> ${relationshipAttr.relatedCollection}, databaseId: ${collection.databaseId}`
183
+ );
184
+ }
185
+ });
186
+ });
187
+ }
188
+
189
+ private addRelationship(
190
+ parentCollection: string,
191
+ childCollection: string,
192
+ parentKey: string,
193
+ childKey: string,
194
+ isArrayParent: boolean,
195
+ isArrayChild: boolean
196
+ ): void {
197
+ const relationshipsChild = this.relationshipMap.get(childCollection) || [];
198
+ const relationshipsParent =
199
+ this.relationshipMap.get(parentCollection) || [];
200
+ relationshipsParent.push({
201
+ parentCollection,
202
+ childCollection,
203
+ parentKey,
204
+ childKey,
205
+ isArray: isArrayParent,
206
+ isChild: false,
207
+ });
208
+ relationshipsChild.push({
209
+ parentCollection,
210
+ childCollection,
211
+ parentKey,
212
+ childKey,
213
+ isArray: isArrayChild,
214
+ isChild: true,
215
+ });
216
+ this.relationshipMap.set(childCollection, relationshipsChild);
217
+ this.relationshipMap.set(parentCollection, relationshipsParent);
218
+ }
219
+
220
+ public generateSchemas(): void {
221
+ if (!this.config.collections) {
222
+ return;
223
+ }
224
+ this.config.collections.forEach((collection) => {
225
+ const schemaString = this.createSchemaString(
226
+ collection.name,
227
+ collection.attributes
228
+ );
229
+ const camelCaseName = toCamelCase(collection.name);
230
+ const schemaPath = path.join(
231
+ this.appwriteFolderPath,
232
+ "schemas",
233
+ `${camelCaseName}.ts`
234
+ );
235
+ fs.writeFileSync(schemaPath, schemaString, { encoding: "utf-8" });
236
+ console.log(`Schema written to ${schemaPath}`);
237
+ });
238
+ }
239
+
240
+ createSchemaString = (name: string, attributes: Attribute[]): string => {
241
+ const pascalName = toPascalCase(name);
242
+ let imports = `import { z } from "zod";\n`;
243
+ const hasDescription = attributes.some((attr) => attr.description);
244
+ if (hasDescription) {
245
+ imports += `import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi";\n`;
246
+ imports += `extendZodWithOpenApi(z);\n`;
247
+ }
248
+
249
+ // Use the relationshipMap to find related collections
250
+ const relationshipDetails = this.relationshipMap.get(name) || [];
251
+ const relatedCollections = relationshipDetails
252
+ .filter((detail, index, self) => {
253
+ const uniqueKey = `${detail.parentCollection}-${detail.childCollection}-${detail.parentKey}-${detail.childKey}`;
254
+ return (
255
+ index ===
256
+ self.findIndex(
257
+ (obj) =>
258
+ `${obj.parentCollection}-${obj.childCollection}-${obj.parentKey}-${obj.childKey}` ===
259
+ uniqueKey
260
+ )
261
+ );
262
+ })
263
+ .map((detail) => {
264
+ const relatedCollectionName = detail.isChild
265
+ ? detail.parentCollection
266
+ : detail.childCollection;
267
+ const key = detail.isChild ? detail.childKey : detail.parentKey;
268
+ const isArray = detail.isArray ? "array" : "";
269
+ return [relatedCollectionName, key, isArray];
270
+ });
271
+
272
+ let relatedTypes = "";
273
+ let relatedTypesLazy = "";
274
+ let curNum = 0;
275
+ let maxNum = relatedCollections.length;
276
+ relatedCollections.forEach((relatedCollection) => {
277
+ console.log(relatedCollection);
278
+ let relatedPascalName = toPascalCase(relatedCollection[0]);
279
+ let relatedCamelName = toCamelCase(relatedCollection[0]);
280
+ curNum++;
281
+ let endNameTypes = relatedPascalName;
282
+ let endNameLazy = `${relatedPascalName}Schema`;
283
+ if (relatedCollection[2] === "array") {
284
+ endNameTypes += "[]";
285
+ endNameLazy += ".array().default([])";
286
+ } else if (!(relatedCollection[2] === "array")) {
287
+ endNameTypes += " | null";
288
+ endNameLazy += ".nullish()";
289
+ }
290
+ imports += `import { ${relatedPascalName}Schema, type ${relatedPascalName} } from "./${relatedCamelName}";\n`;
291
+ relatedTypes += `${relatedCollection[1]}?: ${endNameTypes};\n`;
292
+ if (relatedTypes.length > 0 && curNum !== maxNum) {
293
+ relatedTypes += " ";
294
+ }
295
+ relatedTypesLazy += `${relatedCollection[1]}: z.lazy(() => ${endNameLazy}),\n`;
296
+ if (relatedTypesLazy.length > 0 && curNum !== maxNum) {
297
+ relatedTypesLazy += " ";
298
+ }
299
+ });
300
+
301
+ let schemaString = `${imports}\n\n`;
302
+ schemaString += `export const ${pascalName}SchemaBase = z.object({\n`;
303
+ schemaString += ` $id: z.string().optional(),\n`;
304
+ schemaString += ` $createdAt: z.date().or(z.string()).optional(),\n`;
305
+ schemaString += ` $updatedAt: z.date().or(z.string()).optional(),\n`;
306
+ for (const attribute of attributes) {
307
+ if (attribute.type === "relationship") {
308
+ continue;
309
+ }
310
+ schemaString += ` ${attribute.key}: ${this.typeToZod(attribute)},\n`;
311
+ }
312
+ schemaString += `});\n\n`;
313
+ schemaString += `export type ${pascalName}Base = z.infer<typeof ${pascalName}SchemaBase>`;
314
+ if (relatedTypes.length > 0) {
315
+ schemaString += ` & {\n ${relatedTypes}};\n\n`;
316
+ } else {
317
+ schemaString += `;\n\n`;
318
+ }
319
+ schemaString += `export const ${pascalName}Schema: z.ZodType<${pascalName}Base> = ${pascalName}SchemaBase`;
320
+ if (relatedTypes.length > 0) {
321
+ schemaString += `.extend({\n ${relatedTypesLazy}});\n\n`;
322
+ } else {
323
+ schemaString += `;\n`;
324
+ }
325
+ schemaString += `export type ${pascalName} = z.infer<typeof ${pascalName}Schema>;\n\n`;
326
+
327
+ return schemaString;
328
+ };
329
+
330
+ typeToZod = (attribute: Attribute) => {
331
+ let baseSchemaCode = "";
332
+ const finalAttribute: Attribute = (
333
+ attribute.type === "string" &&
334
+ attribute.format &&
335
+ attribute.format === "enum" &&
336
+ attribute.type === "string"
337
+ ? { ...attribute, type: attribute.format }
338
+ : attribute
339
+ ) as Attribute;
340
+ switch (finalAttribute.type) {
341
+ case "string":
342
+ baseSchemaCode = "z.string()";
343
+ if (finalAttribute.size) {
344
+ baseSchemaCode += `.max(${finalAttribute.size}, "Maximum length of ${finalAttribute.size} characters exceeded")`;
345
+ }
346
+ if (finalAttribute.xdefault !== undefined) {
347
+ baseSchemaCode += `.default("${finalAttribute.xdefault}")`;
348
+ }
349
+ if (!attribute.required && !attribute.array) {
350
+ baseSchemaCode += ".nullish()";
351
+ }
352
+ break;
353
+ case "integer":
354
+ baseSchemaCode = "z.number().int()";
355
+ if (finalAttribute.min !== undefined) {
356
+ if (BigInt(finalAttribute.min) === BigInt(-9223372036854776000)) {
357
+ delete finalAttribute.min;
358
+ } else {
359
+ baseSchemaCode += `.min(${finalAttribute.min}, "Minimum value of ${finalAttribute.min} not met")`;
360
+ }
361
+ }
362
+ if (finalAttribute.max !== undefined) {
363
+ if (BigInt(finalAttribute.max) === BigInt(9223372036854776000)) {
364
+ delete finalAttribute.max;
365
+ } else {
366
+ baseSchemaCode += `.max(${finalAttribute.max}, "Maximum value of ${finalAttribute.max} exceeded")`;
367
+ }
368
+ }
369
+ if (finalAttribute.xdefault !== undefined) {
370
+ baseSchemaCode += `.default(${finalAttribute.xdefault})`;
371
+ }
372
+ if (!finalAttribute.required && !finalAttribute.array) {
373
+ baseSchemaCode += ".nullish()";
374
+ }
375
+ break;
376
+ case "float":
377
+ baseSchemaCode = "z.number()";
378
+ if (finalAttribute.min !== undefined) {
379
+ baseSchemaCode += `.min(${finalAttribute.min}, "Minimum value of ${finalAttribute.min} not met")`;
380
+ }
381
+ if (finalAttribute.max !== undefined) {
382
+ baseSchemaCode += `.max(${finalAttribute.max}, "Maximum value of ${finalAttribute.max} exceeded")`;
383
+ }
384
+ if (finalAttribute.xdefault !== undefined) {
385
+ baseSchemaCode += `.default(${finalAttribute.xdefault})`;
386
+ }
387
+ if (!finalAttribute.required && !finalAttribute.array) {
388
+ baseSchemaCode += ".nullish()";
389
+ }
390
+ break;
391
+ case "boolean":
392
+ baseSchemaCode = "z.boolean()";
393
+ if (finalAttribute.xdefault !== undefined) {
394
+ baseSchemaCode += `.default(${finalAttribute.xdefault})`;
395
+ }
396
+ if (!finalAttribute.required && !finalAttribute.array) {
397
+ baseSchemaCode += ".nullish()";
398
+ }
399
+ break;
400
+ case "datetime":
401
+ baseSchemaCode = "z.date()";
402
+ if (finalAttribute.xdefault !== undefined) {
403
+ baseSchemaCode += `.default(new Date("${finalAttribute.xdefault}"))`;
404
+ }
405
+ if (!finalAttribute.required && !finalAttribute.array) {
406
+ baseSchemaCode += ".nullish()";
407
+ }
408
+ break;
409
+ case "email":
410
+ baseSchemaCode = "z.string().email()";
411
+ if (finalAttribute.xdefault !== undefined) {
412
+ baseSchemaCode += `.default("${finalAttribute.xdefault}")`;
413
+ }
414
+ if (!finalAttribute.required && !finalAttribute.array) {
415
+ baseSchemaCode += ".nullish()";
416
+ }
417
+ break;
418
+ case "ip":
419
+ baseSchemaCode = "z.string()"; // Add custom validation as needed
420
+ if (finalAttribute.xdefault !== undefined) {
421
+ baseSchemaCode += `.default("${finalAttribute.xdefault}")`;
422
+ }
423
+ if (!finalAttribute.required && !finalAttribute.array) {
424
+ baseSchemaCode += ".nullish()";
425
+ }
426
+ break;
427
+ case "url":
428
+ baseSchemaCode = "z.string().url()";
429
+ if (finalAttribute.xdefault !== undefined) {
430
+ baseSchemaCode += `.default("${finalAttribute.xdefault}")`;
431
+ }
432
+ if (!finalAttribute.required && !finalAttribute.array) {
433
+ baseSchemaCode += ".nullish()";
434
+ }
435
+ break;
436
+ case "enum":
437
+ baseSchemaCode = `z.enum([${finalAttribute.elements
438
+ .map((element) => `"${element}"`)
439
+ .join(", ")}])`;
440
+ if (finalAttribute.xdefault !== undefined) {
441
+ baseSchemaCode += `.default("${finalAttribute.xdefault}")`;
442
+ }
443
+ if (!attribute.required && !attribute.array) {
444
+ baseSchemaCode += ".nullish()";
445
+ }
446
+ break;
447
+ case "relationship":
448
+ break;
449
+ default:
450
+ baseSchemaCode = "z.any()";
451
+ }
452
+
453
+ // Handle arrays
454
+ if (attribute.array) {
455
+ baseSchemaCode = `z.array(${baseSchemaCode})`;
456
+ }
457
+ if (attribute.array && !attribute.required) {
458
+ baseSchemaCode += ".nullish()";
459
+ }
460
+ if (attribute.description) {
461
+ if (typeof attribute.description === "string") {
462
+ baseSchemaCode += `.openapi({ description: "${attribute.description}" })`;
463
+ } else {
464
+ baseSchemaCode += `.openapi(${Object.entries(attribute.description)
465
+ .map(([key, value]) => `"${key}": ${value}`)
466
+ .join(", ")})`;
467
+ }
468
+ }
469
+
470
+ return baseSchemaCode;
471
+ };
472
+ }