appwrite-utils-cli 1.4.1 → 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 +1 -1
  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
package/README.md CHANGED
@@ -325,7 +325,28 @@ Available specifications:
325
325
 
326
326
  This updated CLI ensures that developers have robust tools at their fingertips to manage complex Appwrite projects effectively from the command line, with both interactive and non-interactive modes available for flexibility.
327
327
 
328
- ## Changelog
328
+ ## Changelog
329
+
330
+ ### 1.5.0 - Recursive Zod v4 Schemas + One‑Way Relationship Support
331
+
332
+ Highlights
333
+ - Recursive getters: Generated schemas now use Zod v4 recursive getters (no `z.lazy`, no `BaseSchema` layering). Cleaner types and fully inferred mutual recursion.
334
+ - One‑way relationships: Relationship attributes are now included even when `twoWay: false`. Related imports resolve collection IDs to names.
335
+ - Required semantics: Relationship getters respect `required` and `array`:
336
+ - required scalar → `RelatedSchema`
337
+ - optional scalar → `RelatedSchema.nullish()`
338
+ - required array → `RelatedSchema.array()`
339
+ - optional array → `RelatedSchema.array().nullish()`
340
+ - JSON Schemas: `$ref` definitions use resolved collection names when YAML provides IDs.
341
+
342
+ Validation changes
343
+ - Relationship schema: `twoWayKey` and `side` are now required only when `twoWay` is `true`.
344
+ - Helpful errors: Keeps strong validation but removes false negatives for one‑way relationships.
345
+
346
+ Developer notes
347
+ - Imports: Schema generators import only `...Schema` from related collections (no type imports needed).
348
+ - Example YAML: `Posts.yaml` demonstrates a required `manyToMany` (`categories`) and a one‑way `manyToOne` (`author`).
349
+
329
350
 
330
351
  ### 1.3.0 - Zod v4 Upgrade & Collection Management Fixes
331
352
 
@@ -168,8 +168,9 @@ export class TablesDBAdapter extends BaseAdapter {
168
168
  // Attribute Operations
169
169
  async createAttribute(params) {
170
170
  try {
171
- // TablesDB may have different method names for attribute operations
172
- const result = await this.tablesDB.createAttribute(params);
171
+ // Prefer createColumn if available, fallback to createAttribute
172
+ const fn = this.tablesDB.createColumn || this.tablesDB.createAttribute;
173
+ const result = await fn.call(this.tablesDB, params);
173
174
  return { data: result };
174
175
  }
175
176
  catch (error) {
@@ -178,7 +179,8 @@ export class TablesDBAdapter extends BaseAdapter {
178
179
  }
179
180
  async updateAttribute(params) {
180
181
  try {
181
- const result = await this.tablesDB.updateAttribute(params);
182
+ const fn = this.tablesDB.updateColumn || this.tablesDB.updateAttribute;
183
+ const result = await fn.call(this.tablesDB, params);
182
184
  return { data: result };
183
185
  }
184
186
  catch (error) {
@@ -187,7 +189,8 @@ export class TablesDBAdapter extends BaseAdapter {
187
189
  }
188
190
  async deleteAttribute(params) {
189
191
  try {
190
- const result = await this.tablesDB.deleteAttribute(params);
192
+ const fn = this.tablesDB.deleteColumn || this.tablesDB.deleteAttribute;
193
+ const result = await fn.call(this.tablesDB, params);
191
194
  return { data: result };
192
195
  }
193
196
  catch (error) {
@@ -4,7 +4,7 @@ import { type Attribute } from "appwrite-utils";
4
4
  * Enhanced attribute creation with proper status monitoring and retry logic
5
5
  */
6
6
  export declare const createOrUpdateAttributeWithStatusCheck: (db: Databases, dbId: string, collection: Models.Collection, attribute: Attribute, retryCount?: number, maxRetries?: number) => Promise<boolean>;
7
- export declare const createOrUpdateAttribute: (db: Databases, dbId: string, collection: Models.Collection, attribute: Attribute) => Promise<void>;
7
+ export declare const createOrUpdateAttribute: (db: Databases, dbId: string, collection: Models.Collection, attribute: Attribute) => Promise<"queued" | "processed">;
8
8
  /**
9
9
  * Enhanced collection attribute creation with proper status monitoring
10
10
  */
@@ -157,7 +157,13 @@ export const createOrUpdateAttributeWithStatusCheck = async (db, dbId, collectio
157
157
  console.log(chalk.blue(`Creating/updating attribute '${attribute.key}' (attempt ${retryCount + 1}/${maxRetries + 1})`));
158
158
  try {
159
159
  // First, try to create/update the attribute using existing logic
160
- await createOrUpdateAttribute(db, dbId, collection, attribute);
160
+ const result = await createOrUpdateAttribute(db, dbId, collection, attribute);
161
+ // If the attribute was queued (relationship dependency unresolved),
162
+ // skip status polling and retry logic — the queue will handle it later.
163
+ if (result === "queued") {
164
+ console.log(chalk.yellow(`⏭️ Deferred relationship attribute '${attribute.key}' — queued for later once dependencies are available`));
165
+ return true;
166
+ }
161
167
  // Now wait for the attribute to become available
162
168
  const success = await waitForAttributeAvailable(db, dbId, collection.$id, attribute.key, 60000, // 1 minute timeout
163
169
  retryCount, maxRetries);
@@ -229,7 +235,7 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
229
235
  attributesSame(foundAttribute, attribute) &&
230
236
  updateEnabled) {
231
237
  // No need to do anything, they are the same
232
- return;
238
+ return "processed";
233
239
  }
234
240
  else if (foundAttribute &&
235
241
  !attributesSame(foundAttribute, attribute) &&
@@ -248,7 +254,7 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
248
254
  !attributesSame(foundAttribute, attribute)) {
249
255
  await db.deleteAttribute(dbId, collection.$id, attribute.key);
250
256
  console.log(`Deleted attribute: ${attribute.key} to recreate it because they diff (update disabled temporarily)`);
251
- return;
257
+ return "processed";
252
258
  }
253
259
  // console.log(`${action}-ing attribute: ${finalAttribute.key}`);
254
260
  // Relationship attribute logic with adjustments
@@ -256,7 +262,18 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
256
262
  let relatedCollectionId;
257
263
  if (finalAttribute.type === "relationship" &&
258
264
  finalAttribute.relatedCollection) {
259
- if (nameToIdMapping.has(finalAttribute.relatedCollection)) {
265
+ // First try treating relatedCollection as an ID directly
266
+ try {
267
+ const byIdCollection = await db.getCollection(dbId, finalAttribute.relatedCollection);
268
+ collectionFoundViaRelatedCollection = byIdCollection;
269
+ relatedCollectionId = byIdCollection.$id;
270
+ // Cache by name for subsequent lookups
271
+ nameToIdMapping.set(byIdCollection.name, byIdCollection.$id);
272
+ }
273
+ catch (_) {
274
+ // Not an ID or not found — fall back to name-based resolution below
275
+ }
276
+ if (!collectionFoundViaRelatedCollection && nameToIdMapping.has(finalAttribute.relatedCollection)) {
260
277
  relatedCollectionId = nameToIdMapping.get(finalAttribute.relatedCollection);
261
278
  try {
262
279
  collectionFoundViaRelatedCollection = await db.getCollection(dbId, relatedCollectionId);
@@ -268,7 +285,7 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
268
285
  collectionFoundViaRelatedCollection = undefined;
269
286
  }
270
287
  }
271
- else {
288
+ else if (!collectionFoundViaRelatedCollection) {
272
289
  const collectionsPulled = await db.listCollections(dbId, [
273
290
  Query.equal("name", finalAttribute.relatedCollection),
274
291
  ]);
@@ -288,7 +305,7 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
288
305
  attribute,
289
306
  dependencies: [finalAttribute.relatedCollection],
290
307
  });
291
- return;
308
+ return "queued";
292
309
  }
293
310
  }
294
311
  finalAttribute = parseAttribute(finalAttribute);
@@ -445,6 +462,7 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
445
462
  console.error("Invalid attribute type");
446
463
  break;
447
464
  }
465
+ return "processed";
448
466
  };
449
467
  /**
450
468
  * Enhanced collection attribute creation with proper status monitoring
@@ -20,13 +20,23 @@ retryCount = 0, maxRetries = 5) => {
20
20
  }
21
21
  while (Date.now() - startTime < maxWaitTime) {
22
22
  try {
23
- const indexList = await db.listIndexes(dbId, collectionId);
24
- const index = indexList.indexes.find((idx) => idx.key === indexKey);
23
+ const indexList = await (db instanceof Databases
24
+ ? db.listIndexes(dbId, collectionId)
25
+ : db.listIndexes({ databaseId: dbId, tableId: collectionId }));
26
+ const indexes = (db instanceof Databases)
27
+ ? indexList.indexes
28
+ : (indexList.data || indexList.indexes || []);
29
+ const index = indexes.find((idx) => idx.key === indexKey);
25
30
  if (!index) {
26
31
  console.log(chalk.red(`Index '${indexKey}' not found`));
27
32
  return false;
28
33
  }
29
- console.log(chalk.gray(`Index '${indexKey}' status: ${index.status}`));
34
+ if (db instanceof Databases) {
35
+ console.log(chalk.gray(`Index '${indexKey}' status: ${index.status}`));
36
+ }
37
+ else {
38
+ console.log(chalk.gray(`Index '${indexKey}' detected (TablesDB)`));
39
+ }
30
40
  switch (index.status) {
31
41
  case 'available':
32
42
  console.log(chalk.green(`✅ Index '${indexKey}' is now available`));
@@ -9,11 +9,20 @@ export declare const wipeDatabase: (database: Databases, databaseId: string) =>
9
9
  collectionName: string;
10
10
  }[]>;
11
11
  export declare const wipeCollection: (database: Databases, databaseId: string, collectionId: string) => Promise<void>;
12
+ export declare const wipeAllTables: (adapter: DatabaseAdapter, databaseId: string) => Promise<{
13
+ tableId: string;
14
+ tableName: string;
15
+ }[]>;
16
+ export declare const wipeTableRows: (adapter: DatabaseAdapter, databaseId: string, tableId: string) => Promise<void>;
12
17
  export declare const generateSchemas: (config: AppwriteConfig, appwriteFolderPath: string) => Promise<void>;
13
18
  export declare const createOrUpdateCollections: (database: Databases, databaseId: string, config: AppwriteConfig, deletedCollections?: {
14
19
  collectionId: string;
15
20
  collectionName: string;
16
21
  }[], selectedCollections?: Models.Collection[]) => Promise<void>;
22
+ export declare const createOrUpdateCollectionsViaAdapter: (adapter: DatabaseAdapter, databaseId: string, config: AppwriteConfig, deletedCollections?: {
23
+ collectionId: string;
24
+ collectionName: string;
25
+ }[], selectedCollections?: Models.Collection[]) => Promise<void>;
17
26
  export declare const generateMockData: (database: Databases, databaseId: string, configCollections: any[]) => Promise<void>;
18
27
  export declare const fetchAllCollections: (dbId: string, database: Databases) => Promise<Models.Collection[]>;
19
28
  /**
@@ -198,11 +198,89 @@ export const wipeCollection = async (database, databaseId, collectionId) => {
198
198
  const collection = collections.collections[0];
199
199
  await wipeDocumentsFromCollection(database, databaseId, collection.$id);
200
200
  };
201
+ // TablesDB helpers for wiping
202
+ export const wipeAllTables = async (adapter, databaseId) => {
203
+ MessageFormatter.info(`Wiping tables in database: ${databaseId}`, { prefix: 'Wipe' });
204
+ const res = await adapter.listTables({ databaseId, queries: [Query.limit(500)] });
205
+ const tables = res.tables || [];
206
+ const deleted = [];
207
+ const progress = ProgressManager.create(`wipe-db-${databaseId}`, tables.length, { title: 'Deleting tables' });
208
+ let processed = 0;
209
+ for (const t of tables) {
210
+ try {
211
+ await adapter.deleteTable({ databaseId, tableId: t.$id });
212
+ deleted.push({ tableId: t.$id, tableName: t.name });
213
+ }
214
+ catch (e) {
215
+ MessageFormatter.error(`Failed deleting table ${t.$id}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Wipe' });
216
+ }
217
+ processed++;
218
+ progress.update(processed);
219
+ await delay(100);
220
+ }
221
+ progress.stop();
222
+ return deleted;
223
+ };
224
+ export const wipeTableRows = async (adapter, databaseId, tableId) => {
225
+ try {
226
+ const initial = await adapter.listRows({ databaseId, tableId, queries: [Query.limit(1000)] });
227
+ let rows = initial.rows || [];
228
+ let total = rows.length;
229
+ let cursor = rows.length >= 1000 ? rows[rows.length - 1].$id : undefined;
230
+ while (cursor) {
231
+ const resp = await adapter.listRows({ databaseId, tableId, queries: [Query.limit(1000), ...(cursor ? [Query.cursorAfter(cursor)] : [])] });
232
+ const more = resp.rows || [];
233
+ rows.push(...more);
234
+ total = rows.length;
235
+ cursor = more.length >= 1000 ? more[more.length - 1].$id : undefined;
236
+ if (total % 10000 === 0) {
237
+ MessageFormatter.progress(`Found ${total} rows...`, { prefix: 'Wipe' });
238
+ }
239
+ }
240
+ MessageFormatter.info(`Found ${total} rows to delete`, { prefix: 'Wipe' });
241
+ if (total === 0)
242
+ return;
243
+ const progress = ProgressManager.create(`delete-${tableId}`, total, { title: 'Deleting rows' });
244
+ let processed = 0;
245
+ const maxStackSize = 50;
246
+ const batches = chunk(rows, maxStackSize);
247
+ for (const batch of batches) {
248
+ await Promise.all(batch.map(async (row) => {
249
+ try {
250
+ await adapter.deleteRow({ databaseId, tableId, id: row.$id });
251
+ }
252
+ catch (e) {
253
+ // ignore missing rows
254
+ }
255
+ processed++;
256
+ progress.update(processed);
257
+ }));
258
+ await delay(50);
259
+ }
260
+ progress.stop();
261
+ MessageFormatter.success(`Completed deletion of ${total} rows from table ${tableId}`, { prefix: 'Wipe' });
262
+ }
263
+ catch (error) {
264
+ MessageFormatter.error(`Error wiping rows from table ${tableId}`, error instanceof Error ? error : new Error(String(error)), { prefix: 'Wipe' });
265
+ throw error;
266
+ }
267
+ };
201
268
  export const generateSchemas = async (config, appwriteFolderPath) => {
202
269
  const schemaGenerator = new SchemaGenerator(config, appwriteFolderPath);
203
270
  schemaGenerator.generateSchemas();
204
271
  };
205
272
  export const createOrUpdateCollections = async (database, databaseId, config, deletedCollections, selectedCollections = []) => {
273
+ // If API mode is tablesdb, route to adapter-based implementation
274
+ try {
275
+ const { adapter, apiMode } = await getAdapterFromConfig(config);
276
+ if (apiMode === 'tablesdb') {
277
+ await createOrUpdateCollectionsViaAdapter(adapter, databaseId, config, deletedCollections, selectedCollections);
278
+ return;
279
+ }
280
+ }
281
+ catch {
282
+ // Fallback to legacy path below
283
+ }
206
284
  const collectionsToProcess = selectedCollections.length > 0 ? selectedCollections : config.collections;
207
285
  if (!collectionsToProcess) {
208
286
  return;
@@ -304,6 +382,196 @@ export const createOrUpdateCollections = async (database, databaseId, config, de
304
382
  MessageFormatter.info("No queued operations to process", { prefix: "Collections" });
305
383
  }
306
384
  };
385
+ // New: Adapter-based implementation for TablesDB
386
+ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, config, deletedCollections, selectedCollections = []) => {
387
+ const collectionsToProcess = selectedCollections.length > 0 ? selectedCollections : (config.collections || []);
388
+ if (!collectionsToProcess || collectionsToProcess.length === 0)
389
+ return;
390
+ const usedIds = new Set();
391
+ // Helper: create attributes through adapter
392
+ const createAttr = async (tableId, attr) => {
393
+ const base = {
394
+ databaseId,
395
+ tableId,
396
+ key: attr.key,
397
+ type: attr.type,
398
+ size: attr.size,
399
+ required: !!attr.required,
400
+ default: attr.xdefault,
401
+ array: !!attr.array,
402
+ min: attr.min,
403
+ max: attr.max,
404
+ elements: attr.elements,
405
+ encrypt: attr.encrypted,
406
+ relatedCollection: attr.relatedCollection,
407
+ relationType: attr.relationType,
408
+ twoWay: attr.twoWay,
409
+ twoWayKey: attr.twoWayKey,
410
+ onDelete: attr.onDelete,
411
+ side: attr.side,
412
+ };
413
+ await adapter.createAttribute(base);
414
+ await delay(150);
415
+ };
416
+ // Local queue for unresolved relationships
417
+ const relQueue = [];
418
+ for (const collection of collectionsToProcess) {
419
+ const { attributes, indexes, ...collectionData } = collection;
420
+ // Prepare permissions as strings (reuse Permission helper)
421
+ const permissions = [];
422
+ if (collection.$permissions && collection.$permissions.length > 0) {
423
+ for (const p of collection.$permissions) {
424
+ if (typeof p === 'string')
425
+ permissions.push(p);
426
+ else {
427
+ switch (p.permission) {
428
+ case 'read':
429
+ permissions.push(Permission.read(p.target));
430
+ break;
431
+ case 'create':
432
+ permissions.push(Permission.create(p.target));
433
+ break;
434
+ case 'update':
435
+ permissions.push(Permission.update(p.target));
436
+ break;
437
+ case 'delete':
438
+ permissions.push(Permission.delete(p.target));
439
+ break;
440
+ case 'write':
441
+ permissions.push(Permission.write(p.target));
442
+ break;
443
+ default: break;
444
+ }
445
+ }
446
+ }
447
+ }
448
+ // Find existing table by name
449
+ const list = await adapter.listTables({ databaseId, queries: [Query.equal('name', collectionData.name)] });
450
+ const items = list.tables || [];
451
+ let table = items[0];
452
+ let tableId;
453
+ if (!table) {
454
+ // Determine ID (prefer provided $id or re-use deleted one)
455
+ let foundColl = deletedCollections?.find((coll) => coll.collectionName.toLowerCase().trim().replace(" ", "") === collectionData.name.toLowerCase().trim().replace(" ", ""));
456
+ if (collectionData.$id)
457
+ tableId = collectionData.$id;
458
+ else if (foundColl && !usedIds.has(foundColl.collectionId))
459
+ tableId = foundColl.collectionId;
460
+ else
461
+ tableId = ID.unique();
462
+ usedIds.add(tableId);
463
+ const res = await adapter.createTable({
464
+ databaseId,
465
+ id: tableId,
466
+ name: collectionData.name,
467
+ permissions,
468
+ documentSecurity: !!collectionData.documentSecurity,
469
+ enabled: collectionData.enabled !== false
470
+ });
471
+ table = res.data || res;
472
+ nameToIdMapping.set(collectionData.name, tableId);
473
+ }
474
+ else {
475
+ tableId = table.$id;
476
+ await adapter.updateTable({
477
+ databaseId,
478
+ id: tableId,
479
+ name: collectionData.name,
480
+ permissions,
481
+ documentSecurity: !!collectionData.documentSecurity,
482
+ enabled: collectionData.enabled !== false
483
+ });
484
+ }
485
+ // Add small delay after table create/update
486
+ await delay(250);
487
+ // Create attributes: non-relationship first
488
+ const nonRel = (attributes || []).filter((a) => a.type !== 'relationship');
489
+ for (const attr of nonRel) {
490
+ await createAttr(tableId, attr);
491
+ }
492
+ // Relationship attributes — resolve relatedCollection to ID
493
+ const rels = (attributes || []).filter((a) => a.type === 'relationship');
494
+ for (const attr of rels) {
495
+ const relNameOrId = attr.relatedCollection;
496
+ if (!relNameOrId)
497
+ continue;
498
+ let relId = nameToIdMapping.get(relNameOrId) || relNameOrId;
499
+ // If looks like a name (not ULID) and not in cache, try query by name
500
+ if (!nameToIdMapping.has(relNameOrId)) {
501
+ try {
502
+ const relList = await adapter.listTables({ databaseId, queries: [Query.equal('name', relNameOrId)] });
503
+ const relItems = relList.tables || [];
504
+ if (relItems[0]?.$id) {
505
+ relId = relItems[0].$id;
506
+ nameToIdMapping.set(relNameOrId, relId);
507
+ }
508
+ }
509
+ catch { }
510
+ }
511
+ if (relId && typeof relId === 'string') {
512
+ attr.relatedCollection = relId;
513
+ await createAttr(tableId, attr);
514
+ }
515
+ else {
516
+ // Defer if unresolved
517
+ relQueue.push({ tableId, attr: attr });
518
+ }
519
+ }
520
+ // Indexes
521
+ const idxs = (indexes || []);
522
+ for (const idx of idxs) {
523
+ try {
524
+ await adapter.createIndex({
525
+ databaseId,
526
+ tableId,
527
+ key: idx.key,
528
+ type: idx.type,
529
+ attributes: idx.attributes,
530
+ orders: idx.orders || []
531
+ });
532
+ await delay(150);
533
+ }
534
+ catch (e) {
535
+ MessageFormatter.error(`Failed to create index ${idx.key}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Indexes' });
536
+ }
537
+ }
538
+ }
539
+ // Process queued relationships once mapping likely populated
540
+ for (const { tableId, attr } of relQueue) {
541
+ const relNameOrId = attr.relatedCollection;
542
+ if (!relNameOrId)
543
+ continue;
544
+ const relId = nameToIdMapping.get(relNameOrId) || relNameOrId;
545
+ if (relId) {
546
+ attr.relatedCollection = relId;
547
+ try {
548
+ await adapter.createAttribute({
549
+ databaseId,
550
+ tableId,
551
+ key: attr.key,
552
+ type: attr.type,
553
+ size: attr.size,
554
+ required: !!attr.required,
555
+ default: attr.xdefault,
556
+ array: !!attr.array,
557
+ min: attr.min,
558
+ max: attr.max,
559
+ elements: attr.elements,
560
+ relatedCollection: relId,
561
+ relationType: attr.relationType,
562
+ twoWay: attr.twoWay,
563
+ twoWayKey: attr.twoWayKey,
564
+ onDelete: attr.onDelete,
565
+ side: attr.side
566
+ });
567
+ await delay(150);
568
+ }
569
+ catch (e) {
570
+ MessageFormatter.error(`Failed queued relationship ${attr.key}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Attributes' });
571
+ }
572
+ }
573
+ }
574
+ };
307
575
  export const generateMockData = async (database, databaseId, configCollections) => {
308
576
  for (const { collection, mockFunction } of configCollections) {
309
577
  if (mockFunction) {
@@ -90,12 +90,12 @@ export declare class AppwriteToX {
90
90
  relatedCollection: string;
91
91
  relationType: "oneToMany" | "manyToOne" | "oneToOne" | "manyToMany";
92
92
  twoWay: boolean;
93
- twoWayKey: string;
94
93
  onDelete: "setNull" | "cascade" | "restrict";
95
- side: "parent" | "child";
96
94
  error?: string | undefined;
97
95
  required?: boolean | undefined;
98
96
  array?: boolean | undefined;
97
+ twoWayKey?: string | undefined;
98
+ side?: "parent" | "child" | undefined;
99
99
  importMapping?: {
100
100
  originalIdField: string;
101
101
  targetField?: string | undefined;
@@ -2,6 +2,7 @@ import { converterFunctions, tryAwaitWithRetry, parseAttribute, objectNeedsUpdat
2
2
  import { Client, Databases, Storage, Users, Functions, Teams, Query, AppwriteException, } from "node-appwrite";
3
3
  import { InputFile } from "node-appwrite/file";
4
4
  import { MessageFormatter } from "../shared/messageFormatter.js";
5
+ import { processQueue, queuedOperations } from "../shared/operationQueue.js";
5
6
  import { ProgressManager } from "../shared/progressManager.js";
6
7
  import { getClient } from "../utils/getClientFromConfig.js";
7
8
  import { transferDatabaseLocalToLocal, transferDatabaseLocalToRemote, transferStorageLocalToLocal, transferStorageLocalToRemote, transferUsersLocalToRemote, } from "./transfer.js";
@@ -343,6 +344,17 @@ export class ComprehensiveTransfer {
343
344
  MessageFormatter.error(`Error processing collection ${collection.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
344
345
  }
345
346
  }
347
+ // After processing all collections' attributes and indexes, process any queued
348
+ // relationship attributes so dependencies are resolved within this phase.
349
+ if (queuedOperations.length > 0) {
350
+ MessageFormatter.info(`Processing ${queuedOperations.length} queued relationship operations`, { prefix: "Transfer" });
351
+ await processQueue(this.targetDatabases, dbId);
352
+ }
353
+ else {
354
+ MessageFormatter.info("No queued relationship operations to process", {
355
+ prefix: "Transfer",
356
+ });
357
+ }
346
358
  }
347
359
  catch (error) {
348
360
  MessageFormatter.error(`Failed to create database structure for ${dbId}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
@@ -100,16 +100,16 @@ export declare const CollectionImportDataSchema: z.ZodObject<{
100
100
  manyToMany: "manyToMany";
101
101
  }>;
102
102
  twoWay: z.ZodBoolean;
103
- twoWayKey: z.ZodString;
103
+ twoWayKey: z.ZodOptional<z.ZodString>;
104
104
  onDelete: z.ZodDefault<z.ZodEnum<{
105
105
  setNull: "setNull";
106
106
  cascade: "cascade";
107
107
  restrict: "restrict";
108
108
  }>>;
109
- side: z.ZodEnum<{
109
+ side: z.ZodOptional<z.ZodEnum<{
110
110
  parent: "parent";
111
111
  child: "child";
112
- }>;
112
+ }>>;
113
113
  importMapping: z.ZodOptional<z.ZodObject<{
114
114
  originalIdField: z.ZodString;
115
115
  targetField: z.ZodOptional<z.ZodString>;
@@ -374,12 +374,12 @@ export declare class DataLoader {
374
374
  relatedCollection: string;
375
375
  relationType: "oneToMany" | "manyToOne" | "oneToOne" | "manyToMany";
376
376
  twoWay: boolean;
377
- twoWayKey: string;
378
377
  onDelete: "setNull" | "cascade" | "restrict";
379
- side: "parent" | "child";
380
378
  error?: string | undefined;
381
379
  required?: boolean | undefined;
382
380
  array?: boolean | undefined;
381
+ twoWayKey?: string | undefined;
382
+ side?: "parent" | "child" | undefined;
383
383
  importMapping?: {
384
384
  originalIdField: string;
385
385
  targetField?: string | undefined;
@@ -9,12 +9,12 @@ export declare const findCollectionsWithRelationships: (config: AppwriteConfig)
9
9
  relatedCollection: string;
10
10
  relationType: "oneToMany" | "manyToOne" | "oneToOne" | "manyToMany";
11
11
  twoWay: boolean;
12
- twoWayKey: string;
13
12
  onDelete: "setNull" | "cascade" | "restrict";
14
- side: "parent" | "child";
15
13
  error?: string | undefined;
16
14
  required?: boolean | undefined;
17
15
  array?: boolean | undefined;
16
+ twoWayKey?: string | undefined;
17
+ side?: "parent" | "child" | undefined;
18
18
  importMapping?: {
19
19
  originalIdField: string;
20
20
  targetField?: string | undefined;
@@ -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
  }