appwrite-utils-cli 1.4.0 → 1.5.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 (38) 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 +24 -6
  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/migrations/appwriteToX.d.ts +2 -2
  9. package/dist/migrations/comprehensiveTransfer.js +12 -0
  10. package/dist/migrations/dataLoader.d.ts +5 -5
  11. package/dist/migrations/relationships.d.ts +2 -2
  12. package/dist/shared/jsonSchemaGenerator.d.ts +1 -0
  13. package/dist/shared/jsonSchemaGenerator.js +6 -2
  14. package/dist/shared/operationQueue.js +14 -1
  15. package/dist/shared/schemaGenerator.d.ts +2 -1
  16. package/dist/shared/schemaGenerator.js +61 -78
  17. package/dist/storage/schemas.d.ts +8 -8
  18. package/dist/utils/loadConfigs.js +44 -19
  19. package/dist/utils/schemaStrings.d.ts +2 -1
  20. package/dist/utils/schemaStrings.js +61 -78
  21. package/dist/utils/setupFiles.js +19 -1
  22. package/dist/utils/versionDetection.d.ts +6 -0
  23. package/dist/utils/versionDetection.js +30 -0
  24. package/dist/utilsController.js +28 -4
  25. package/package.json +2 -2
  26. package/src/adapters/TablesDBAdapter.ts +20 -17
  27. package/src/collections/attributes.ts +122 -99
  28. package/src/collections/indexes.ts +36 -28
  29. package/src/collections/methods.ts +292 -19
  30. package/src/migrations/comprehensiveTransfer.ts +22 -8
  31. package/src/shared/jsonSchemaGenerator.ts +36 -29
  32. package/src/shared/operationQueue.ts +48 -33
  33. package/src/shared/schemaGenerator.ts +128 -134
  34. package/src/utils/loadConfigs.ts +48 -29
  35. package/src/utils/schemaStrings.ts +124 -130
  36. package/src/utils/setupFiles.ts +21 -5
  37. package/src/utils/versionDetection.ts +48 -21
  38. package/src/utilsController.ts +44 -24
@@ -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) => {
@@ -1,6 +1,8 @@
1
1
  import { mkdirSync, writeFileSync, existsSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { findAppwriteConfig } from "./loadConfigs.js";
4
+ import { loadYamlConfig } from "../config/yamlConfig.js";
5
+ import { fetchServerVersion, isVersionAtLeast } from "./versionDetection.js";
4
6
  import { findYamlConfig } from "../config/yamlConfig.js";
5
7
  import { ID } from "node-appwrite";
6
8
  import { ulid } from "ulidx";
@@ -240,7 +242,23 @@ export const setupDirsFiles = async (example = false, currentDir, useYaml = true
240
242
  const appwriteSchemaFolder = path.join(appwriteFolder, "schemas");
241
243
  const appwriteYamlSchemaFolder = path.join(appwriteFolder, ".yaml_schemas");
242
244
  const appwriteDataFolder = path.join(appwriteFolder, "importData");
243
- const collectionsFolder = path.join(appwriteFolder, "collections");
245
+ // Decide between collections or tables folder
246
+ let useTables = false;
247
+ try {
248
+ // Try reading YAML config if present to detect version
249
+ const yamlPath = findYamlConfig(basePath);
250
+ if (yamlPath) {
251
+ const cfg = await loadYamlConfig(yamlPath);
252
+ if (cfg) {
253
+ const ver = await fetchServerVersion(cfg.appwriteEndpoint);
254
+ if (isVersionAtLeast(ver || undefined, '1.8.0'))
255
+ useTables = true;
256
+ }
257
+ }
258
+ }
259
+ catch { }
260
+ const targetFolderName = useTables ? "tables" : "collections";
261
+ const collectionsFolder = path.join(appwriteFolder, targetFolderName);
244
262
  // Create directory structure
245
263
  if (!existsSync(appwriteFolder)) {
246
264
  mkdirSync(appwriteFolder, { recursive: true });
@@ -54,3 +54,9 @@ export declare function detectSdkSupport(): Promise<{
54
54
  * Clear version detection cache (useful for testing)
55
55
  */
56
56
  export declare function clearVersionDetectionCache(): void;
57
+ /**
58
+ * Fetch server version from /health/version (no auth required)
59
+ */
60
+ export declare function fetchServerVersion(endpoint: string): Promise<string | null>;
61
+ /** Compare semantic versions (basic) */
62
+ export declare function isVersionAtLeast(current: string | undefined, target: string): boolean;
@@ -215,3 +215,33 @@ export async function detectSdkSupport() {
215
215
  export function clearVersionDetectionCache() {
216
216
  detectionCache.clear();
217
217
  }
218
+ /**
219
+ * Fetch server version from /health/version (no auth required)
220
+ */
221
+ export async function fetchServerVersion(endpoint) {
222
+ try {
223
+ const clean = endpoint.replace(/\/$/, '');
224
+ const res = await fetch(`${clean}/health/version`, { method: 'GET', signal: AbortSignal.timeout(5000) });
225
+ if (!res.ok)
226
+ return null;
227
+ const data = await res.json().catch(() => null);
228
+ const version = (data && (data.version || data.build || data.release)) ?? null;
229
+ return typeof version === 'string' ? version : null;
230
+ }
231
+ catch {
232
+ return null;
233
+ }
234
+ }
235
+ /** Compare semantic versions (basic) */
236
+ export function isVersionAtLeast(current, target) {
237
+ if (!current)
238
+ return false;
239
+ const toNums = (v) => v.split('.').map(n => parseInt(n, 10));
240
+ const [a1 = 0, a2 = 0, a3 = 0] = toNums(current);
241
+ const [b1, b2, b3] = toNums(target);
242
+ if (a1 !== b1)
243
+ return a1 > b1;
244
+ if (a2 !== b2)
245
+ return a2 > b2;
246
+ return a3 >= b3;
247
+ }