appwrite-utils-cli 1.4.1 → 1.5.1

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 (42) hide show
  1. package/README.md +22 -1
  2. package/dist/adapters/TablesDBAdapter.js +7 -4
  3. package/dist/collections/attributes.d.ts +1 -1
  4. package/dist/collections/attributes.js +42 -7
  5. package/dist/collections/indexes.js +13 -3
  6. package/dist/collections/methods.d.ts +9 -0
  7. package/dist/collections/methods.js +268 -0
  8. package/dist/databases/setup.js +6 -2
  9. package/dist/interactiveCLI.js +2 -1
  10. package/dist/migrations/appwriteToX.d.ts +2 -2
  11. package/dist/migrations/comprehensiveTransfer.js +12 -0
  12. package/dist/migrations/dataLoader.d.ts +5 -5
  13. package/dist/migrations/relationships.d.ts +2 -2
  14. package/dist/shared/jsonSchemaGenerator.d.ts +1 -0
  15. package/dist/shared/jsonSchemaGenerator.js +6 -2
  16. package/dist/shared/operationQueue.js +14 -1
  17. package/dist/shared/schemaGenerator.d.ts +2 -1
  18. package/dist/shared/schemaGenerator.js +61 -78
  19. package/dist/storage/schemas.d.ts +8 -8
  20. package/dist/utils/loadConfigs.js +44 -19
  21. package/dist/utils/schemaStrings.d.ts +2 -1
  22. package/dist/utils/schemaStrings.js +61 -78
  23. package/dist/utils/setupFiles.js +19 -1
  24. package/dist/utils/versionDetection.d.ts +6 -0
  25. package/dist/utils/versionDetection.js +30 -0
  26. package/dist/utilsController.js +32 -5
  27. package/package.json +1 -1
  28. package/src/adapters/TablesDBAdapter.ts +20 -17
  29. package/src/collections/attributes.ts +198 -156
  30. package/src/collections/indexes.ts +36 -28
  31. package/src/collections/methods.ts +292 -19
  32. package/src/databases/setup.ts +11 -7
  33. package/src/interactiveCLI.ts +8 -7
  34. package/src/migrations/comprehensiveTransfer.ts +22 -8
  35. package/src/shared/jsonSchemaGenerator.ts +36 -29
  36. package/src/shared/operationQueue.ts +48 -33
  37. package/src/shared/schemaGenerator.ts +128 -134
  38. package/src/utils/loadConfigs.ts +48 -29
  39. package/src/utils/schemaStrings.ts +124 -130
  40. package/src/utils/setupFiles.ts +21 -5
  41. package/src/utils/versionDetection.ts +48 -21
  42. package/src/utilsController.ts +59 -32
@@ -31,6 +31,7 @@ export declare class JsonSchemaGenerator {
31
31
  private appwriteFolderPath;
32
32
  private relationshipMap;
33
33
  constructor(config: AppwriteConfig, appwriteFolderPath: string);
34
+ private resolveCollectionName;
34
35
  private extractRelationships;
35
36
  private attributeToJsonSchemaProperty;
36
37
  private getBaseTypeSchema;
@@ -11,6 +11,10 @@ export class JsonSchemaGenerator {
11
11
  this.appwriteFolderPath = appwriteFolderPath;
12
12
  this.extractRelationships();
13
13
  }
14
+ resolveCollectionName = (idOrName) => {
15
+ const col = this.config.collections?.find((c) => c.$id === idOrName || c.name === idOrName);
16
+ return col?.name ?? idOrName;
17
+ };
14
18
  extractRelationships() {
15
19
  if (!this.config.collections)
16
20
  return;
@@ -22,7 +26,7 @@ export class JsonSchemaGenerator {
22
26
  const relationships = this.relationshipMap.get(collection.name) || [];
23
27
  relationships.push({
24
28
  attributeKey: attr.key,
25
- relatedCollection: attr.relatedCollection,
29
+ relatedCollection: this.resolveCollectionName(attr.relatedCollection),
26
30
  relationType: attr.relationType,
27
31
  isArray: attr.relationType === "oneToMany" || attr.relationType === "manyToMany"
28
32
  });
@@ -108,7 +112,7 @@ export class JsonSchemaGenerator {
108
112
  case "relationship":
109
113
  if (attribute.relatedCollection) {
110
114
  // For relationships, reference the related collection schema
111
- schema.$ref = `#/definitions/${toPascalCase(attribute.relatedCollection)}`;
115
+ schema.$ref = `#/definitions/${toPascalCase(this.resolveCollectionName(attribute.relatedCollection))}`;
112
116
  }
113
117
  else {
114
118
  schema.type = "string";
@@ -33,7 +33,20 @@ export const processQueue = async (db, dbId) => {
33
33
  }
34
34
  // Attempt to resolve related collection if specified and not already found
35
35
  if (!collectionFound && operation.attribute?.relatedCollection) {
36
- collectionFound = await fetchAndCacheCollectionByName(db, dbId, operation.attribute.relatedCollection);
36
+ // First, try treating relatedCollection as an ID
37
+ try {
38
+ const relAttr = operation.attribute;
39
+ const byId = await tryAwaitWithRetry(async () => await db.getCollection(dbId, relAttr.relatedCollection));
40
+ // We still need the target collection (operation.collectionId) to create the attribute on,
41
+ // so only use this branch to warm caches/mappings and continue to dependency checks.
42
+ // Do not override collectionFound with the related collection.
43
+ }
44
+ catch (_) {
45
+ // Not an ID or not found; fall back to name-based cache
46
+ }
47
+ // Warm cache by name (used by attribute creation path), but do not use as target collection
48
+ const relAttr = operation.attribute;
49
+ await fetchAndCacheCollectionByName(db, dbId, relAttr.relatedCollection);
37
50
  }
38
51
  // Handle dependencies if collection still not found
39
52
  if (!collectionFound) {
@@ -4,6 +4,7 @@ export declare class SchemaGenerator {
4
4
  private config;
5
5
  private appwriteFolderPath;
6
6
  constructor(config: AppwriteConfig, appwriteFolderPath: string);
7
+ private resolveCollectionName;
7
8
  updateYamlCollections(): void;
8
9
  updateTsSchemas(): void;
9
10
  updateConfig(config: AppwriteConfig, isYamlConfig?: boolean): Promise<void>;
@@ -15,6 +16,6 @@ export declare class SchemaGenerator {
15
16
  format?: "zod" | "json" | "both";
16
17
  verbose?: boolean;
17
18
  }): void;
18
- createSchemaString: (name: string, attributes: Attribute[]) => string;
19
+ createSchemaStringV4: (name: string, attributes: Attribute[]) => string;
19
20
  typeToZod: (attribute: Attribute) => string;
20
21
  }
@@ -16,6 +16,10 @@ export class SchemaGenerator {
16
16
  this.appwriteFolderPath = appwriteFolderPath;
17
17
  this.extractRelationships();
18
18
  }
19
+ resolveCollectionName = (idOrName) => {
20
+ const col = this.config.collections?.find((c) => c.$id === idOrName || c.name === idOrName);
21
+ return col?.name ?? idOrName;
22
+ };
19
23
  updateYamlCollections() {
20
24
  const collections = this.config.collections;
21
25
  delete this.config.collections;
@@ -265,7 +269,7 @@ export default appwriteConfig;
265
269
  default:
266
270
  break;
267
271
  }
268
- this.addRelationship(collection.name, relationshipAttr.relatedCollection, attr.key, relationshipAttr.twoWayKey, isArrayParent, isArrayChild);
272
+ this.addRelationship(collection.name, this.resolveCollectionName(relationshipAttr.relatedCollection), attr.key, relationshipAttr.twoWayKey, isArrayParent, isArrayChild);
269
273
  console.log(`Extracted relationship: ${attr.key}\n\t${collection.name} -> ${relationshipAttr.relatedCollection}, databaseId: ${collection.databaseId}`);
270
274
  }
271
275
  });
@@ -307,7 +311,7 @@ export default appwriteConfig;
307
311
  // Generate Zod schemas (TypeScript)
308
312
  if (format === "zod" || format === "both") {
309
313
  this.config.collections.forEach((collection) => {
310
- const schemaString = this.createSchemaString(collection.name, collection.attributes || []);
314
+ const schemaString = this.createSchemaStringV4(collection.name, collection.attributes || []);
311
315
  const camelCaseName = toCamelCase(collection.name);
312
316
  const schemaPath = path.join(schemasPath, `${camelCaseName}.ts`);
313
317
  fs.writeFileSync(schemaPath, schemaString, { encoding: "utf-8" });
@@ -329,12 +333,13 @@ export default appwriteConfig;
329
333
  console.log(`✓ Schema generation completed (format: ${format})`);
330
334
  }
331
335
  }
332
- createSchemaString = (name, attributes) => {
336
+ // Zod v4 recursive getter-based schemas
337
+ createSchemaStringV4 = (name, attributes) => {
333
338
  const pascalName = toPascalCase(name);
334
339
  let imports = `import { z } from "zod";\n`;
335
340
  // Use the relationshipMap to find related collections
336
341
  const relationshipDetails = this.relationshipMap.get(name) || [];
337
- const relatedCollections = relationshipDetails
342
+ let relatedCollections = relationshipDetails
338
343
  .filter((detail, index, self) => {
339
344
  const uniqueKey = `${detail.parentCollection}-${detail.childCollection}-${detail.parentKey}-${detail.childKey}`;
340
345
  return (index ===
@@ -349,86 +354,64 @@ export default appwriteConfig;
349
354
  const isArray = detail.isArray ? "array" : "";
350
355
  return [relatedCollectionName, key, isArray];
351
356
  });
352
- // Check if we have any relationships - if not, generate simple schema
353
- const hasRelationships = relatedCollections.length > 0;
354
- let schemaString = `${imports}\n\n`;
355
- if (!hasRelationships) {
356
- // Simple case: no relationships, generate single schema directly
357
- schemaString += `export const ${pascalName}Schema = z.object({\n`;
358
- schemaString += ` $id: z.string(),\n`;
359
- schemaString += ` $createdAt: z.string(),\n`;
360
- schemaString += ` $updatedAt: z.string(),\n`;
361
- schemaString += ` $permissions: z.array(z.string()),\n`;
362
- for (const attribute of attributes) {
363
- if (attribute.type === "relationship") {
364
- continue;
365
- }
366
- schemaString += ` ${attribute.key}: ${this.typeToZod(attribute)},\n`;
357
+ // Include one-way relationship attributes directly (no twoWayKey)
358
+ const oneWayRels = [];
359
+ for (const attr of attributes) {
360
+ if (attr.type === "relationship" && attr.relatedCollection) {
361
+ const relatedName = this.resolveCollectionName(attr.relatedCollection);
362
+ const isArray = attr.relationType === "oneToMany" || attr.relationType === "manyToMany"
363
+ ? "array"
364
+ : "";
365
+ oneWayRels.push([relatedName, attr.key, isArray]);
367
366
  }
368
- schemaString += `});\n\n`;
369
- schemaString += `export type ${pascalName} = z.infer<typeof ${pascalName}Schema>;\n\n`;
370
367
  }
371
- else {
372
- // Complex case: has relationships, generate BaseSchema + extended schema pattern
373
- let relatedTypes = "";
374
- let relatedTypesLazy = "";
375
- let curNum = 0;
376
- let maxNum = relatedCollections.length;
377
- relatedCollections.forEach((relatedCollection) => {
378
- console.log(relatedCollection);
379
- let relatedPascalName = toPascalCase(relatedCollection[0]);
380
- let relatedCamelName = toCamelCase(relatedCollection[0]);
381
- curNum++;
382
- let endNameTypes = relatedPascalName;
383
- let endNameLazy = `${relatedPascalName}Schema`;
384
- if (relatedCollection[2] === "array") {
385
- endNameTypes += "[]";
386
- endNameLazy += ".array().default([])";
387
- }
388
- else if (!(relatedCollection[2] === "array")) {
389
- endNameTypes += " | null";
390
- endNameLazy += ".nullish()";
391
- }
392
- imports += `import { ${relatedPascalName}Schema, type ${relatedPascalName} } from "./${relatedCamelName}";\n`;
393
- relatedTypes += `${relatedCollection[1]}?: ${endNameTypes};\n`;
394
- if (relatedTypes.length > 0 && curNum !== maxNum) {
395
- relatedTypes += " ";
396
- }
397
- relatedTypesLazy += `${relatedCollection[1]}: z.lazy(() => ${endNameLazy}),\n`;
398
- if (relatedTypesLazy.length > 0 && curNum !== maxNum) {
399
- relatedTypesLazy += " ";
400
- }
368
+ // Merge and dedupe (by relatedName+key)
369
+ relatedCollections = [...relatedCollections, ...oneWayRels].filter((item, idx, self) => idx === self.findIndex((o) => `${o[0]}::${o[1]}` === `${item[0]}::${item[1]}`));
370
+ const hasRelationships = relatedCollections.length > 0;
371
+ // Build imports for related collections
372
+ if (hasRelationships) {
373
+ const importLines = relatedCollections.map((rel) => {
374
+ const relatedPascalName = toPascalCase(rel[0]);
375
+ const relatedCamelName = toCamelCase(rel[0]);
376
+ return `import { ${relatedPascalName}Schema } from "./${relatedCamelName}";`;
401
377
  });
402
- // Re-add imports after processing relationships
403
- schemaString = `${imports}\n\n`;
404
- schemaString += `export const ${pascalName}SchemaBase = z.object({\n`;
405
- schemaString += ` $id: z.string(),\n`;
406
- schemaString += ` $createdAt: z.string(),\n`;
407
- schemaString += ` $updatedAt: z.string(),\n`;
408
- schemaString += ` $permissions: z.array(z.string()),\n`;
409
- for (const attribute of attributes) {
410
- if (attribute.type === "relationship") {
411
- continue;
412
- }
413
- schemaString += ` ${attribute.key}: ${this.typeToZod(attribute)},\n`;
414
- }
415
- schemaString += `});\n\n`;
416
- schemaString += `export type ${pascalName}Base = z.infer<typeof ${pascalName}SchemaBase>`;
417
- if (relatedTypes.length > 0) {
418
- schemaString += ` & {\n ${relatedTypes}};\n\n`;
419
- }
420
- else {
421
- schemaString += `;\n\n`;
422
- }
423
- schemaString += `export const ${pascalName}Schema: z.ZodType<${pascalName}Base> = ${pascalName}SchemaBase`;
424
- if (relatedTypes.length > 0) {
425
- schemaString += `.extend({\n ${relatedTypesLazy}});\n\n`;
378
+ const unique = Array.from(new Set(importLines));
379
+ imports += unique.join("\n") + (unique.length ? "\n" : "");
380
+ }
381
+ let schemaString = `${imports}\n`;
382
+ // Single object schema with recursive getters (Zod v4)
383
+ schemaString += `export const ${pascalName}Schema = z.object({\n`;
384
+ schemaString += ` $id: z.string(),\n`;
385
+ schemaString += ` $createdAt: z.string(),\n`;
386
+ schemaString += ` $updatedAt: z.string(),\n`;
387
+ schemaString += ` $permissions: z.array(z.string()),\n`;
388
+ for (const attribute of attributes) {
389
+ if (attribute.type === "relationship")
390
+ continue;
391
+ schemaString += ` ${attribute.key}: ${this.typeToZod(attribute)},\n`;
392
+ }
393
+ // Add recursive getters for relationships (respect required flag)
394
+ relatedCollections.forEach((rel) => {
395
+ const relatedPascalName = toPascalCase(rel[0]);
396
+ const isArray = rel[2] === "array";
397
+ const key = String(rel[1]);
398
+ const attrMeta = attributes.find(a => a.key === key && a.type === "relationship");
399
+ const isRequired = !!attrMeta?.required;
400
+ let getterBody = "";
401
+ if (isArray) {
402
+ getterBody = isRequired
403
+ ? `${relatedPascalName}Schema.array()`
404
+ : `${relatedPascalName}Schema.array().nullish()`;
426
405
  }
427
406
  else {
428
- schemaString += `;\n`;
407
+ getterBody = isRequired
408
+ ? `${relatedPascalName}Schema`
409
+ : `${relatedPascalName}Schema.nullish()`;
429
410
  }
430
- schemaString += `export type ${pascalName} = z.infer<typeof ${pascalName}Schema>;\n\n`;
431
- }
411
+ schemaString += ` get ${key}(){\n return ${getterBody}\n },\n`;
412
+ });
413
+ schemaString += `});\n\n`;
414
+ schemaString += `export type ${pascalName} = z.infer<typeof ${pascalName}Schema>;\n\n`;
432
415
  return schemaString;
433
416
  };
434
417
  typeToZod = (attribute) => {
@@ -162,12 +162,12 @@ export declare const getMigrationCollectionSchemas: () => {
162
162
  relatedCollection: string;
163
163
  relationType: "oneToMany" | "manyToOne" | "oneToOne" | "manyToMany";
164
164
  twoWay: boolean;
165
- twoWayKey: string;
166
165
  onDelete: "setNull" | "cascade" | "restrict";
167
- side: "parent" | "child";
168
166
  error?: string | undefined;
169
167
  required?: boolean | undefined;
170
168
  array?: boolean | undefined;
169
+ twoWayKey?: string | undefined;
170
+ side?: "parent" | "child" | undefined;
171
171
  importMapping?: {
172
172
  originalIdField: string;
173
173
  targetField?: string | undefined;
@@ -313,12 +313,12 @@ export declare const getMigrationCollectionSchemas: () => {
313
313
  relatedCollection: string;
314
314
  relationType: "oneToMany" | "manyToOne" | "oneToOne" | "manyToMany";
315
315
  twoWay: boolean;
316
- twoWayKey: string;
317
316
  onDelete: "setNull" | "cascade" | "restrict";
318
- side: "parent" | "child";
319
317
  error?: string | undefined;
320
318
  required?: boolean | undefined;
321
319
  array?: boolean | undefined;
320
+ twoWayKey?: string | undefined;
321
+ side?: "parent" | "child" | undefined;
322
322
  importMapping?: {
323
323
  originalIdField: string;
324
324
  targetField?: string | undefined;
@@ -414,12 +414,12 @@ export declare const getMigrationCollectionSchemas: () => {
414
414
  relatedCollection: string;
415
415
  relationType: "oneToMany" | "manyToOne" | "oneToOne" | "manyToMany";
416
416
  twoWay: boolean;
417
- twoWayKey: string;
418
417
  onDelete: "setNull" | "cascade" | "restrict";
419
- side: "parent" | "child";
420
418
  error?: string | undefined;
421
419
  required?: boolean | undefined;
422
420
  array?: boolean | undefined;
421
+ twoWayKey?: string | undefined;
422
+ side?: "parent" | "child" | undefined;
423
423
  importMapping?: {
424
424
  originalIdField: string;
425
425
  targetField?: string | undefined;
@@ -565,12 +565,12 @@ export declare const getMigrationCollectionSchemas: () => {
565
565
  relatedCollection: string;
566
566
  relationType: "oneToMany" | "manyToOne" | "oneToOne" | "manyToMany";
567
567
  twoWay: boolean;
568
- twoWayKey: string;
569
568
  onDelete: "setNull" | "cascade" | "restrict";
570
- side: "parent" | "child";
571
569
  error?: string | undefined;
572
570
  required?: boolean | undefined;
573
571
  array?: boolean | undefined;
572
+ twoWayKey?: string | undefined;
573
+ side?: "parent" | "child" | undefined;
574
574
  importMapping?: {
575
575
  originalIdField: string;
576
576
  targetField?: string | undefined;
@@ -5,6 +5,7 @@ import { register } from "tsx/esm/api"; // Import the register function
5
5
  import { pathToFileURL } from "node:url";
6
6
  import chalk from "chalk";
7
7
  import { findYamlConfig, loadYamlConfig } from "../config/yamlConfig.js";
8
+ import { detectAppwriteVersionCached, fetchServerVersion, isVersionAtLeast } from "./versionDetection.js";
8
9
  import yaml from "js-yaml";
9
10
  import { z } from "zod";
10
11
  import { MessageFormatter } from "../shared/messageFormatter.js";
@@ -149,16 +150,30 @@ export const loadConfigWithPath = async (configDir) => {
149
150
  if (!config || !actualConfigPath) {
150
151
  throw new Error("No valid configuration found");
151
152
  }
152
- // Determine collections directory based on actual config file location
153
+ // Determine directory (collections or tables) based on server version / API mode
154
+ let dirName = "collections";
155
+ try {
156
+ const det = await detectAppwriteVersionCached(config.appwriteEndpoint, config.appwriteProject, config.appwriteKey);
157
+ if (det.apiMode === 'tablesdb' || isVersionAtLeast(det.serverVersion, '1.8.0')) {
158
+ dirName = 'tables';
159
+ }
160
+ else {
161
+ // Try health version if not provided
162
+ const ver = await fetchServerVersion(config.appwriteEndpoint);
163
+ if (isVersionAtLeast(ver || undefined, '1.8.0'))
164
+ dirName = 'tables';
165
+ }
166
+ }
167
+ catch { }
168
+ // Determine collections directory based on actual config file location and dirName
153
169
  let collectionsDir;
154
170
  const configFileDir = path.dirname(actualConfigPath);
155
- // Check if config is in .appwrite directory
156
- if (configFileDir.endsWith('.appwrite')) {
157
- collectionsDir = path.join(configFileDir, "collections");
158
- }
159
- else {
160
- // Config is in root or other directory
161
- collectionsDir = path.join(configFileDir, "collections");
171
+ collectionsDir = path.join(configFileDir, dirName);
172
+ // Fallback if not found
173
+ if (!fs.existsSync(collectionsDir)) {
174
+ const fallback = path.join(configFileDir, dirName === 'tables' ? 'collections' : 'tables');
175
+ if (fs.existsSync(fallback))
176
+ collectionsDir = fallback;
162
177
  }
163
178
  // Load collections if they exist
164
179
  if (fs.existsSync(collectionsDir)) {
@@ -238,22 +253,32 @@ export const loadConfig = async (configDir) => {
238
253
  if (!config) {
239
254
  throw new Error("No valid configuration found");
240
255
  }
241
- // Determine collections directory based on actual config file location
242
- let collectionsDir;
243
- if (actualConfigPath) {
244
- const configFileDir = path.dirname(actualConfigPath);
245
- // Check if config is in .appwrite directory
246
- if (configFileDir.endsWith('.appwrite')) {
247
- collectionsDir = path.join(configFileDir, "collections");
256
+ // Determine directory (collections or tables) based on server version / API mode
257
+ let dirName2 = "collections";
258
+ try {
259
+ const det = await detectAppwriteVersionCached(config.appwriteEndpoint, config.appwriteProject, config.appwriteKey);
260
+ if (det.apiMode === 'tablesdb' || isVersionAtLeast(det.serverVersion, '1.8.0')) {
261
+ dirName2 = 'tables';
248
262
  }
249
263
  else {
250
- // Config is in root or other directory
251
- collectionsDir = path.join(configFileDir, "collections");
264
+ const ver = await fetchServerVersion(config.appwriteEndpoint);
265
+ if (isVersionAtLeast(ver || undefined, '1.8.0'))
266
+ dirName2 = 'tables';
252
267
  }
253
268
  }
269
+ catch { }
270
+ let collectionsDir;
271
+ if (actualConfigPath) {
272
+ const configFileDir = path.dirname(actualConfigPath);
273
+ collectionsDir = path.join(configFileDir, dirName2);
274
+ }
254
275
  else {
255
- // Fallback to original behavior if no actual config path found
256
- collectionsDir = path.join(configDir, "collections");
276
+ collectionsDir = path.join(configDir, dirName2);
277
+ }
278
+ if (!fs.existsSync(collectionsDir)) {
279
+ const fallback = path.join(path.dirname(actualConfigPath || configDir), dirName2 === 'tables' ? 'collections' : 'tables');
280
+ if (fs.existsSync(fallback))
281
+ collectionsDir = fallback;
257
282
  }
258
283
  // Load collections if they exist
259
284
  if (fs.existsSync(collectionsDir)) {
@@ -4,10 +4,11 @@ export declare class SchemaGenerator {
4
4
  private config;
5
5
  private appwriteFolderPath;
6
6
  constructor(config: AppwriteConfig, appwriteFolderPath: string);
7
+ private resolveCollectionName;
7
8
  updateTsSchemas(): void;
8
9
  private extractRelationships;
9
10
  private addRelationship;
10
11
  generateSchemas(): void;
11
- createSchemaString: (name: string, attributes: Attribute[]) => string;
12
+ createSchemaStringV4: (name: string, attributes: Attribute[]) => string;
12
13
  typeToZod: (attribute: Attribute) => string;
13
14
  }
@@ -15,6 +15,10 @@ export class SchemaGenerator {
15
15
  this.appwriteFolderPath = appwriteFolderPath;
16
16
  this.extractRelationships();
17
17
  }
18
+ resolveCollectionName = (idOrName) => {
19
+ const col = this.config.collections?.find((c) => c.$id === idOrName || c.name === idOrName);
20
+ return col?.name ?? idOrName;
21
+ };
18
22
  updateTsSchemas() {
19
23
  const collections = this.config.collections;
20
24
  const functions = this.config.functions || [];
@@ -168,7 +172,7 @@ export class SchemaGenerator {
168
172
  default:
169
173
  break;
170
174
  }
171
- this.addRelationship(collection.name, relationshipAttr.relatedCollection, attr.key, relationshipAttr.twoWayKey, isArrayParent, isArrayChild);
175
+ this.addRelationship(collection.name, this.resolveCollectionName(relationshipAttr.relatedCollection), attr.key, relationshipAttr.twoWayKey, isArrayParent, isArrayChild);
172
176
  console.log(`Extracted relationship: ${attr.key}\n\t${collection.name} -> ${relationshipAttr.relatedCollection}, databaseId: ${collection.databaseId}`);
173
177
  }
174
178
  });
@@ -201,19 +205,20 @@ export class SchemaGenerator {
201
205
  return;
202
206
  }
203
207
  this.config.collections.forEach((collection) => {
204
- const schemaString = this.createSchemaString(collection.name, collection.attributes);
208
+ const schemaString = this.createSchemaStringV4(collection.name, collection.attributes);
205
209
  const camelCaseName = toCamelCase(collection.name);
206
210
  const schemaPath = path.join(this.appwriteFolderPath, "schemas", `${camelCaseName}.ts`);
207
211
  fs.writeFileSync(schemaPath, schemaString, { encoding: "utf-8" });
208
212
  console.log(`Schema written to ${schemaPath}`);
209
213
  });
210
214
  }
211
- createSchemaString = (name, attributes) => {
215
+ // Zod v4 recursive getter-based schemas
216
+ createSchemaStringV4 = (name, attributes) => {
212
217
  const pascalName = toPascalCase(name);
213
218
  let imports = `import { z } from "zod";\n`;
214
219
  // Use the relationshipMap to find related collections
215
220
  const relationshipDetails = this.relationshipMap.get(name) || [];
216
- const relatedCollections = relationshipDetails
221
+ let relatedCollections = relationshipDetails
217
222
  .filter((detail, index, self) => {
218
223
  const uniqueKey = `${detail.parentCollection}-${detail.childCollection}-${detail.parentKey}-${detail.childKey}`;
219
224
  return (index ===
@@ -228,86 +233,64 @@ export class SchemaGenerator {
228
233
  const isArray = detail.isArray ? "array" : "";
229
234
  return [relatedCollectionName, key, isArray];
230
235
  });
231
- // Check if we have any relationships - if not, generate simple schema
232
- const hasRelationships = relatedCollections.length > 0;
233
- let schemaString = `${imports}\n\n`;
234
- if (!hasRelationships) {
235
- // Simple case: no relationships, generate single schema directly
236
- schemaString += `export const ${pascalName}Schema = z.object({\n`;
237
- schemaString += ` $id: z.string(),\n`;
238
- schemaString += ` $createdAt: z.string(),\n`;
239
- schemaString += ` $updatedAt: z.string(),\n`;
240
- schemaString += ` $permissions: z.array(z.string()),\n`;
241
- for (const attribute of attributes) {
242
- if (attribute.type === "relationship") {
243
- continue;
244
- }
245
- schemaString += ` ${attribute.key}: ${this.typeToZod(attribute)},\n`;
236
+ // Include one-way relationship attributes directly (no twoWayKey)
237
+ const oneWayRels = [];
238
+ for (const attr of attributes) {
239
+ if (attr.type === "relationship" && attr.relatedCollection) {
240
+ const relatedName = this.resolveCollectionName(attr.relatedCollection);
241
+ const isArray = attr.relationType === "oneToMany" || attr.relationType === "manyToMany"
242
+ ? "array"
243
+ : "";
244
+ oneWayRels.push([relatedName, attr.key, isArray]);
246
245
  }
247
- schemaString += `});\n\n`;
248
- schemaString += `export type ${pascalName} = z.infer<typeof ${pascalName}Schema>;\n\n`;
249
246
  }
250
- else {
251
- // Complex case: has relationships, generate BaseSchema + extended schema pattern
252
- let relatedTypes = "";
253
- let relatedTypesLazy = "";
254
- let curNum = 0;
255
- let maxNum = relatedCollections.length;
256
- relatedCollections.forEach((relatedCollection) => {
257
- console.log(relatedCollection);
258
- let relatedPascalName = toPascalCase(relatedCollection[0]);
259
- let relatedCamelName = toCamelCase(relatedCollection[0]);
260
- curNum++;
261
- let endNameTypes = relatedPascalName;
262
- let endNameLazy = `${relatedPascalName}Schema`;
263
- if (relatedCollection[2] === "array") {
264
- endNameTypes += "[]";
265
- endNameLazy += ".array().default([])";
266
- }
267
- else if (!(relatedCollection[2] === "array")) {
268
- endNameTypes += " | null";
269
- endNameLazy += ".nullish()";
270
- }
271
- imports += `import { ${relatedPascalName}Schema, type ${relatedPascalName} } from "./${relatedCamelName}";\n`;
272
- relatedTypes += `${relatedCollection[1]}?: ${endNameTypes};\n`;
273
- if (relatedTypes.length > 0 && curNum !== maxNum) {
274
- relatedTypes += " ";
275
- }
276
- relatedTypesLazy += `${relatedCollection[1]}: z.lazy(() => ${endNameLazy}),\n`;
277
- if (relatedTypesLazy.length > 0 && curNum !== maxNum) {
278
- relatedTypesLazy += " ";
279
- }
247
+ // Merge and dedupe (by relatedName+key)
248
+ relatedCollections = [...relatedCollections, ...oneWayRels].filter((item, idx, self) => idx === self.findIndex((o) => `${o[0]}::${o[1]}` === `${item[0]}::${item[1]}`));
249
+ const hasRelationships = relatedCollections.length > 0;
250
+ // Build imports for related collections
251
+ if (hasRelationships) {
252
+ const importLines = relatedCollections.map((rel) => {
253
+ const relatedPascalName = toPascalCase(rel[0]);
254
+ const relatedCamelName = toCamelCase(rel[0]);
255
+ return `import { ${relatedPascalName}Schema } from "./${relatedCamelName}";`;
280
256
  });
281
- // Re-add imports after processing relationships
282
- schemaString = `${imports}\n\n`;
283
- schemaString += `export const ${pascalName}SchemaBase = z.object({\n`;
284
- schemaString += ` $id: z.string(),\n`;
285
- schemaString += ` $createdAt: z.string(),\n`;
286
- schemaString += ` $updatedAt: z.string(),\n`;
287
- schemaString += ` $permissions: z.array(z.string()),\n`;
288
- for (const attribute of attributes) {
289
- if (attribute.type === "relationship") {
290
- continue;
291
- }
292
- schemaString += ` ${attribute.key}: ${this.typeToZod(attribute)},\n`;
293
- }
294
- schemaString += `});\n\n`;
295
- schemaString += `export type ${pascalName}Base = z.infer<typeof ${pascalName}SchemaBase>`;
296
- if (relatedTypes.length > 0) {
297
- schemaString += ` & {\n ${relatedTypes}};\n\n`;
298
- }
299
- else {
300
- schemaString += `;\n\n`;
301
- }
302
- schemaString += `export const ${pascalName}Schema: z.ZodType<${pascalName}Base> = ${pascalName}SchemaBase`;
303
- if (relatedTypes.length > 0) {
304
- schemaString += `.extend({\n ${relatedTypesLazy}});\n\n`;
257
+ const unique = Array.from(new Set(importLines));
258
+ imports += unique.join("\n") + (unique.length ? "\n" : "");
259
+ }
260
+ let schemaString = `${imports}\n`;
261
+ // Single object schema with recursive getters (Zod v4)
262
+ schemaString += `export const ${pascalName}Schema = z.object({\n`;
263
+ schemaString += ` $id: z.string(),\n`;
264
+ schemaString += ` $createdAt: z.string(),\n`;
265
+ schemaString += ` $updatedAt: z.string(),\n`;
266
+ schemaString += ` $permissions: z.array(z.string()),\n`;
267
+ for (const attribute of attributes) {
268
+ if (attribute.type === "relationship")
269
+ continue;
270
+ schemaString += ` ${attribute.key}: ${this.typeToZod(attribute)},\n`;
271
+ }
272
+ // Add recursive getters for relationships (respect required flag)
273
+ relatedCollections.forEach((rel) => {
274
+ const relatedPascalName = toPascalCase(rel[0]);
275
+ const isArray = rel[2] === "array";
276
+ const key = String(rel[1]);
277
+ const attrMeta = attributes.find(a => a.key === key && a.type === "relationship");
278
+ const isRequired = !!attrMeta?.required;
279
+ let getterBody = "";
280
+ if (isArray) {
281
+ getterBody = isRequired
282
+ ? `${relatedPascalName}Schema.array()`
283
+ : `${relatedPascalName}Schema.array().nullish()`;
305
284
  }
306
285
  else {
307
- schemaString += `;\n`;
286
+ getterBody = isRequired
287
+ ? `${relatedPascalName}Schema`
288
+ : `${relatedPascalName}Schema.nullish()`;
308
289
  }
309
- schemaString += `export type ${pascalName} = z.infer<typeof ${pascalName}Schema>;\n\n`;
310
- }
290
+ schemaString += ` get ${key}(){\n return ${getterBody}\n },\n`;
291
+ });
292
+ schemaString += `});\n\n`;
293
+ schemaString += `export type ${pascalName} = z.infer<typeof ${pascalName}Schema>;\n\n`;
311
294
  return schemaString;
312
295
  };
313
296
  typeToZod = (attribute) => {