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
@@ -33,39 +33,54 @@ export const processQueue = async (db: Databases, dbId: string) => {
33
33
  let collectionFound: Models.Collection | undefined;
34
34
 
35
35
  // Handle relationship attribute operations
36
- if (operation.attribute?.type === "relationship") {
37
- // Attempt to resolve the collection directly if collectionId is specified
38
- if (operation.collectionId) {
39
- console.log(`\tFetching collection by ID: ${operation.collectionId}`);
40
- try {
41
- collectionFound = await tryAwaitWithRetry(
42
- async () => await db.getCollection(dbId, operation.collectionId!)
43
- );
44
- } catch (e) {
45
- console.log(
46
- `\tCollection not found by ID: ${operation.collectionId}`
47
- );
48
- }
49
- }
50
- // Attempt to resolve related collection if specified and not already found
51
- if (!collectionFound && operation.attribute?.relatedCollection) {
52
- collectionFound = await fetchAndCacheCollectionByName(
53
- db,
54
- dbId,
55
- operation.attribute.relatedCollection
56
- );
57
- }
58
- // Handle dependencies if collection still not found
59
- if (!collectionFound) {
60
- for (const dep of operation.dependencies || []) {
61
- collectionFound = await fetchAndCacheCollectionByName(
62
- db,
63
- dbId,
64
- dep
65
- );
66
- if (collectionFound) break; // Break early if collection is found
67
- }
68
- }
36
+ if (operation.attribute?.type === "relationship") {
37
+ // Attempt to resolve the collection directly if collectionId is specified
38
+ if (operation.collectionId) {
39
+ console.log(`\tFetching collection by ID: ${operation.collectionId}`);
40
+ try {
41
+ collectionFound = await tryAwaitWithRetry(
42
+ async () => await db.getCollection(dbId, operation.collectionId!)
43
+ );
44
+ } catch (e) {
45
+ console.log(
46
+ `\tCollection not found by ID: ${operation.collectionId}`
47
+ );
48
+ }
49
+ }
50
+ // Attempt to resolve related collection if specified and not already found
51
+ if (!collectionFound && operation.attribute?.relatedCollection) {
52
+ // First, try treating relatedCollection as an ID
53
+ try {
54
+ const relAttr: any = operation.attribute as any;
55
+ const byId = await tryAwaitWithRetry(
56
+ async () => await db.getCollection(dbId, relAttr.relatedCollection as string)
57
+ );
58
+ // We still need the target collection (operation.collectionId) to create the attribute on,
59
+ // so only use this branch to warm caches/mappings and continue to dependency checks.
60
+ // Do not override collectionFound with the related collection.
61
+ } catch (_) {
62
+ // Not an ID or not found; fall back to name-based cache
63
+ }
64
+
65
+ // Warm cache by name (used by attribute creation path), but do not use as target collection
66
+ const relAttr: any = operation.attribute as any;
67
+ await fetchAndCacheCollectionByName(
68
+ db,
69
+ dbId,
70
+ relAttr.relatedCollection
71
+ );
72
+ }
73
+ // Handle dependencies if collection still not found
74
+ if (!collectionFound) {
75
+ for (const dep of operation.dependencies || []) {
76
+ collectionFound = await fetchAndCacheCollectionByName(
77
+ db,
78
+ dbId,
79
+ dep
80
+ );
81
+ if (collectionFound) break; // Break early if collection is found
82
+ }
83
+ }
69
84
  } else if (operation.collectionId) {
70
85
  // Handle non-relationship operations with a specified collectionId
71
86
  console.log(`\tFetching collection by ID: ${operation.collectionId}`);
@@ -22,16 +22,23 @@ interface RelationshipDetail {
22
22
  isChild: boolean;
23
23
  }
24
24
 
25
- export class SchemaGenerator {
26
- private relationshipMap = new Map<string, RelationshipDetail[]>();
27
- private config: AppwriteConfig;
28
- private appwriteFolderPath: string;
29
-
30
- constructor(config: AppwriteConfig, appwriteFolderPath: string) {
31
- this.config = config;
32
- this.appwriteFolderPath = appwriteFolderPath;
33
- this.extractRelationships();
34
- }
25
+ export class SchemaGenerator {
26
+ private relationshipMap = new Map<string, RelationshipDetail[]>();
27
+ private config: AppwriteConfig;
28
+ private appwriteFolderPath: string;
29
+
30
+ constructor(config: AppwriteConfig, appwriteFolderPath: string) {
31
+ this.config = config;
32
+ this.appwriteFolderPath = appwriteFolderPath;
33
+ this.extractRelationships();
34
+ }
35
+
36
+ private resolveCollectionName = (idOrName: string): string => {
37
+ const col = this.config.collections?.find(
38
+ (c) => c.$id === (idOrName as any) || c.name === idOrName
39
+ );
40
+ return col?.name ?? idOrName;
41
+ };
35
42
 
36
43
  public updateYamlCollections(): void {
37
44
  const collections = this.config.collections;
@@ -291,12 +298,12 @@ export default appwriteConfig;
291
298
  if (!collection.attributes) {
292
299
  return;
293
300
  }
294
- collection.attributes.forEach((attr) => {
295
- if (attr.type === "relationship" && attr.twoWay && attr.twoWayKey) {
296
- const relationshipAttr = attr as RelationshipAttribute;
297
- let isArrayParent = false;
298
- let isArrayChild = false;
299
- switch (relationshipAttr.relationType) {
301
+ collection.attributes.forEach((attr) => {
302
+ if (attr.type === "relationship" && attr.twoWay && attr.twoWayKey) {
303
+ const relationshipAttr = attr as RelationshipAttribute;
304
+ let isArrayParent = false;
305
+ let isArrayChild = false;
306
+ switch (relationshipAttr.relationType) {
300
307
  case "oneToMany":
301
308
  isArrayParent = true;
302
309
  isArrayChild = false;
@@ -316,14 +323,14 @@ export default appwriteConfig;
316
323
  default:
317
324
  break;
318
325
  }
319
- this.addRelationship(
320
- collection.name,
321
- relationshipAttr.relatedCollection,
322
- attr.key,
323
- relationshipAttr.twoWayKey,
324
- isArrayParent,
325
- isArrayChild
326
- );
326
+ this.addRelationship(
327
+ collection.name,
328
+ this.resolveCollectionName(relationshipAttr.relatedCollection),
329
+ attr.key,
330
+ relationshipAttr.twoWayKey!,
331
+ isArrayParent,
332
+ isArrayChild
333
+ );
327
334
  console.log(
328
335
  `Extracted relationship: ${attr.key}\n\t${collection.name} -> ${relationshipAttr.relatedCollection}, databaseId: ${collection.databaseId}`
329
336
  );
@@ -383,10 +390,10 @@ export default appwriteConfig;
383
390
  // Generate Zod schemas (TypeScript)
384
391
  if (format === "zod" || format === "both") {
385
392
  this.config.collections.forEach((collection) => {
386
- const schemaString = this.createSchemaString(
387
- collection.name,
388
- collection.attributes || []
389
- );
393
+ const schemaString = this.createSchemaStringV4(
394
+ collection.name,
395
+ collection.attributes || []
396
+ );
390
397
  const camelCaseName = toCamelCase(collection.name);
391
398
  const schemaPath = path.join(schemasPath, `${camelCaseName}.ts`);
392
399
  fs.writeFileSync(schemaPath, schemaString, { encoding: "utf-8" });
@@ -409,119 +416,106 @@ export default appwriteConfig;
409
416
  if (verbose) {
410
417
  console.log(`✓ Schema generation completed (format: ${format})`);
411
418
  }
412
- }
413
-
414
- createSchemaString = (name: string, attributes: Attribute[]): string => {
415
- const pascalName = toPascalCase(name);
416
- let imports = `import { z } from "zod";\n`;
417
-
418
- // Use the relationshipMap to find related collections
419
- const relationshipDetails = this.relationshipMap.get(name) || [];
420
- const relatedCollections = relationshipDetails
421
- .filter((detail, index, self) => {
422
- const uniqueKey = `${detail.parentCollection}-${detail.childCollection}-${detail.parentKey}-${detail.childKey}`;
423
- return (
424
- index ===
425
- self.findIndex(
419
+ }
420
+
421
+ // Zod v4 recursive getter-based schemas
422
+ createSchemaStringV4 = (name: string, attributes: Attribute[]): string => {
423
+ const pascalName = toPascalCase(name);
424
+ let imports = `import { z } from "zod";\n`;
425
+
426
+ // Use the relationshipMap to find related collections
427
+ const relationshipDetails = this.relationshipMap.get(name) || [];
428
+ let relatedCollections = relationshipDetails
429
+ .filter((detail, index, self) => {
430
+ const uniqueKey = `${detail.parentCollection}-${detail.childCollection}-${detail.parentKey}-${detail.childKey}`;
431
+ return (
432
+ index ===
433
+ self.findIndex(
426
434
  (obj) =>
427
435
  `${obj.parentCollection}-${obj.childCollection}-${obj.parentKey}-${obj.childKey}` ===
428
436
  uniqueKey
429
437
  )
430
438
  );
431
439
  })
432
- .map((detail) => {
433
- const relatedCollectionName = detail.isChild
434
- ? detail.parentCollection
435
- : detail.childCollection;
436
- const key = detail.isChild ? detail.childKey : detail.parentKey;
437
- const isArray = detail.isArray ? "array" : "";
438
- return [relatedCollectionName, key, isArray];
439
- });
440
+ .map((detail) => {
441
+ const relatedCollectionName = detail.isChild
442
+ ? detail.parentCollection
443
+ : detail.childCollection;
444
+ const key = detail.isChild ? detail.childKey : detail.parentKey;
445
+ const isArray = detail.isArray ? "array" : "";
446
+ return [relatedCollectionName, key, isArray];
447
+ });
448
+
449
+ // Include one-way relationship attributes directly (no twoWayKey)
450
+ const oneWayRels: Array<[string, string, string]> = [];
451
+ for (const attr of attributes) {
452
+ if (attr.type === "relationship" && attr.relatedCollection) {
453
+ const relatedName = this.resolveCollectionName(attr.relatedCollection);
454
+ const isArray =
455
+ attr.relationType === "oneToMany" || attr.relationType === "manyToMany"
456
+ ? "array"
457
+ : "";
458
+ oneWayRels.push([relatedName, attr.key, isArray]);
459
+ }
460
+ }
461
+
462
+ // Merge and dedupe (by relatedName+key)
463
+ relatedCollections = [...relatedCollections, ...oneWayRels].filter(
464
+ (item, idx, self) =>
465
+ idx === self.findIndex((o) => `${o[0]}::${o[1]}` === `${item[0]}::${item[1]}`)
466
+ );
440
467
 
441
- // Check if we have any relationships - if not, generate simple schema
442
- const hasRelationships = relatedCollections.length > 0;
443
-
444
- let schemaString = `${imports}\n\n`;
445
-
446
- if (!hasRelationships) {
447
- // Simple case: no relationships, generate single schema directly
448
- schemaString += `export const ${pascalName}Schema = z.object({\n`;
449
- schemaString += ` $id: z.string(),\n`;
450
- schemaString += ` $createdAt: z.string(),\n`;
451
- schemaString += ` $updatedAt: z.string(),\n`;
452
- schemaString += ` $permissions: z.array(z.string()),\n`;
453
- for (const attribute of attributes) {
454
- if (attribute.type === "relationship") {
455
- continue;
456
- }
457
- schemaString += ` ${attribute.key}: ${this.typeToZod(attribute)},\n`;
458
- }
459
- schemaString += `});\n\n`;
460
- schemaString += `export type ${pascalName} = z.infer<typeof ${pascalName}Schema>;\n\n`;
461
- } else {
462
- // Complex case: has relationships, generate BaseSchema + extended schema pattern
463
- let relatedTypes = "";
464
- let relatedTypesLazy = "";
465
- let curNum = 0;
466
- let maxNum = relatedCollections.length;
467
-
468
- relatedCollections.forEach((relatedCollection) => {
469
- console.log(relatedCollection);
470
- let relatedPascalName = toPascalCase(relatedCollection[0]);
471
- let relatedCamelName = toCamelCase(relatedCollection[0]);
472
- curNum++;
473
- let endNameTypes = relatedPascalName;
474
- let endNameLazy = `${relatedPascalName}Schema`;
475
- if (relatedCollection[2] === "array") {
476
- endNameTypes += "[]";
477
- endNameLazy += ".array().default([])";
478
- } else if (!(relatedCollection[2] === "array")) {
479
- endNameTypes += " | null";
480
- endNameLazy += ".nullish()";
481
- }
482
- imports += `import { ${relatedPascalName}Schema, type ${relatedPascalName} } from "./${relatedCamelName}";\n`;
483
- relatedTypes += `${relatedCollection[1]}?: ${endNameTypes};\n`;
484
- if (relatedTypes.length > 0 && curNum !== maxNum) {
485
- relatedTypes += " ";
486
- }
487
- relatedTypesLazy += `${relatedCollection[1]}: z.lazy(() => ${endNameLazy}),\n`;
488
- if (relatedTypesLazy.length > 0 && curNum !== maxNum) {
489
- relatedTypesLazy += " ";
490
- }
491
- });
492
-
493
- // Re-add imports after processing relationships
494
- schemaString = `${imports}\n\n`;
495
-
496
- schemaString += `export const ${pascalName}SchemaBase = z.object({\n`;
497
- schemaString += ` $id: z.string(),\n`;
498
- schemaString += ` $createdAt: z.string(),\n`;
499
- schemaString += ` $updatedAt: z.string(),\n`;
500
- schemaString += ` $permissions: z.array(z.string()),\n`;
501
- for (const attribute of attributes) {
502
- if (attribute.type === "relationship") {
503
- continue;
504
- }
505
- schemaString += ` ${attribute.key}: ${this.typeToZod(attribute)},\n`;
506
- }
507
- schemaString += `});\n\n`;
508
- schemaString += `export type ${pascalName}Base = z.infer<typeof ${pascalName}SchemaBase>`;
509
- if (relatedTypes.length > 0) {
510
- schemaString += ` & {\n ${relatedTypes}};\n\n`;
511
- } else {
512
- schemaString += `;\n\n`;
513
- }
514
- schemaString += `export const ${pascalName}Schema: z.ZodType<${pascalName}Base> = ${pascalName}SchemaBase`;
515
- if (relatedTypes.length > 0) {
516
- schemaString += `.extend({\n ${relatedTypesLazy}});\n\n`;
517
- } else {
518
- schemaString += `;\n`;
519
- }
520
- schemaString += `export type ${pascalName} = z.infer<typeof ${pascalName}Schema>;\n\n`;
521
- }
522
-
523
- return schemaString;
524
- };
468
+ const hasRelationships = relatedCollections.length > 0;
469
+
470
+ // Build imports for related collections
471
+ if (hasRelationships) {
472
+ const importLines = relatedCollections.map((rel) => {
473
+ const relatedPascalName = toPascalCase(rel[0]);
474
+ const relatedCamelName = toCamelCase(rel[0]);
475
+ return `import { ${relatedPascalName}Schema } from "./${relatedCamelName}";`;
476
+ });
477
+ const unique = Array.from(new Set(importLines));
478
+ imports += unique.join("\n") + (unique.length ? "\n" : "");
479
+ }
480
+
481
+ let schemaString = `${imports}\n`;
482
+
483
+ // Single object schema with recursive getters (Zod v4)
484
+ schemaString += `export const ${pascalName}Schema = z.object({\n`;
485
+ schemaString += ` $id: z.string(),\n`;
486
+ schemaString += ` $createdAt: z.string(),\n`;
487
+ schemaString += ` $updatedAt: z.string(),\n`;
488
+ schemaString += ` $permissions: z.array(z.string()),\n`;
489
+ for (const attribute of attributes) {
490
+ if (attribute.type === "relationship") continue;
491
+ schemaString += ` ${attribute.key}: ${this.typeToZod(attribute)},\n`;
492
+ }
493
+
494
+ // Add recursive getters for relationships (respect required flag)
495
+ relatedCollections.forEach((rel) => {
496
+ const relatedPascalName = toPascalCase(rel[0]);
497
+ const isArray = rel[2] === "array";
498
+ const key = String(rel[1]);
499
+ const attrMeta = attributes.find(a => a.key === key && a.type === "relationship");
500
+ const isRequired = !!attrMeta?.required;
501
+ let getterBody = "";
502
+ if (isArray) {
503
+ getterBody = isRequired
504
+ ? `${relatedPascalName}Schema.array()`
505
+ : `${relatedPascalName}Schema.array().nullish()`;
506
+ } else {
507
+ getterBody = isRequired
508
+ ? `${relatedPascalName}Schema`
509
+ : `${relatedPascalName}Schema.nullish()`;
510
+ }
511
+ schemaString += ` get ${key}(){\n return ${getterBody}\n },\n`;
512
+ });
513
+
514
+ schemaString += `});\n\n`;
515
+ schemaString += `export type ${pascalName} = z.infer<typeof ${pascalName}Schema>;\n\n`;
516
+
517
+ return schemaString;
518
+ };
525
519
 
526
520
  typeToZod = (attribute: Attribute) => {
527
521
  let baseSchemaCode = "";
@@ -4,7 +4,8 @@ import { type AppwriteConfig, type Collection, type CollectionCreate } from "app
4
4
  import { register } from "tsx/esm/api"; // Import the register function
5
5
  import { pathToFileURL } from "node:url";
6
6
  import chalk from "chalk";
7
- import { findYamlConfig, loadYamlConfig } from "../config/yamlConfig.js";
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";
@@ -165,17 +166,28 @@ export const loadConfigWithPath = async (
165
166
  throw new Error("No valid configuration found");
166
167
  }
167
168
 
168
- // Determine collections directory based on actual config file location
169
- let collectionsDir: string;
170
- const configFileDir = path.dirname(actualConfigPath);
171
-
172
- // Check if config is in .appwrite directory
173
- if (configFileDir.endsWith('.appwrite')) {
174
- collectionsDir = path.join(configFileDir, "collections");
175
- } else {
176
- // Config is in root or other directory
177
- collectionsDir = path.join(configFileDir, "collections");
178
- }
169
+ // Determine directory (collections or tables) based on server version / API mode
170
+ let dirName = "collections";
171
+ try {
172
+ const det = await detectAppwriteVersionCached(config.appwriteEndpoint, config.appwriteProject, config.appwriteKey);
173
+ if (det.apiMode === 'tablesdb' || isVersionAtLeast(det.serverVersion, '1.8.0')) {
174
+ dirName = 'tables';
175
+ } else {
176
+ // Try health version if not provided
177
+ const ver = await fetchServerVersion(config.appwriteEndpoint);
178
+ if (isVersionAtLeast(ver || undefined, '1.8.0')) dirName = 'tables';
179
+ }
180
+ } catch {}
181
+
182
+ // Determine collections directory based on actual config file location and dirName
183
+ let collectionsDir: string;
184
+ const configFileDir = path.dirname(actualConfigPath);
185
+ collectionsDir = path.join(configFileDir, dirName);
186
+ // Fallback if not found
187
+ if (!fs.existsSync(collectionsDir)) {
188
+ const fallback = path.join(configFileDir, dirName === 'tables' ? 'collections' : 'tables');
189
+ if (fs.existsSync(fallback)) collectionsDir = fallback;
190
+ }
179
191
 
180
192
  // Load collections if they exist
181
193
  if (fs.existsSync(collectionsDir)) {
@@ -267,22 +279,29 @@ export const loadConfig = async (
267
279
  throw new Error("No valid configuration found");
268
280
  }
269
281
 
270
- // Determine collections directory based on actual config file location
271
- let collectionsDir: string;
272
- if (actualConfigPath) {
273
- const configFileDir = path.dirname(actualConfigPath);
274
-
275
- // Check if config is in .appwrite directory
276
- if (configFileDir.endsWith('.appwrite')) {
277
- collectionsDir = path.join(configFileDir, "collections");
278
- } else {
279
- // Config is in root or other directory
280
- collectionsDir = path.join(configFileDir, "collections");
281
- }
282
- } else {
283
- // Fallback to original behavior if no actual config path found
284
- collectionsDir = path.join(configDir, "collections");
285
- }
282
+ // Determine directory (collections or tables) based on server version / API mode
283
+ let dirName2 = "collections";
284
+ try {
285
+ const det = await detectAppwriteVersionCached(config.appwriteEndpoint, config.appwriteProject, config.appwriteKey);
286
+ if (det.apiMode === 'tablesdb' || isVersionAtLeast(det.serverVersion, '1.8.0')) {
287
+ dirName2 = 'tables';
288
+ } else {
289
+ const ver = await fetchServerVersion(config.appwriteEndpoint);
290
+ if (isVersionAtLeast(ver || undefined, '1.8.0')) dirName2 = 'tables';
291
+ }
292
+ } catch {}
293
+
294
+ let collectionsDir: string;
295
+ if (actualConfigPath) {
296
+ const configFileDir = path.dirname(actualConfigPath);
297
+ collectionsDir = path.join(configFileDir, dirName2);
298
+ } else {
299
+ collectionsDir = path.join(configDir, dirName2);
300
+ }
301
+ if (!fs.existsSync(collectionsDir)) {
302
+ const fallback = path.join(path.dirname(actualConfigPath || configDir), dirName2 === 'tables' ? 'collections' : 'tables');
303
+ if (fs.existsSync(fallback)) collectionsDir = fallback;
304
+ }
286
305
 
287
306
  // Load collections if they exist
288
307
  if (fs.existsSync(collectionsDir)) {
@@ -459,4 +478,4 @@ const loadYamlCollection = (filePath: string): CollectionCreate | null => {
459
478
  console.error(`Error loading YAML collection from ${filePath}:`, error);
460
479
  return null;
461
480
  }
462
- };
481
+ };