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
@@ -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
+ }
@@ -7,12 +7,14 @@ import { ImportController } from "./migrations/importController.js";
7
7
  import { ImportDataActions } from "./migrations/importDataActions.js";
8
8
  import { setupMigrationDatabase, ensureDatabasesExist, wipeOtherDatabases, ensureCollectionsExist, } from "./databases/setup.js";
9
9
  import { createOrUpdateCollections, wipeDatabase, generateSchemas, fetchAllCollections, wipeCollection, } from "./collections/methods.js";
10
+ import { wipeAllTables, wipeTableRows } from "./collections/methods.js";
10
11
  import { backupDatabase, ensureDatabaseConfigBucketsExist, initOrGetBackupStorage, wipeDocumentStorage, } from "./storage/methods.js";
11
12
  import path from "path";
12
13
  import { converterFunctions, validationRules, } from "appwrite-utils";
13
14
  import { afterImportActions } from "./migrations/afterImportActions.js";
14
15
  import { transferDatabaseLocalToLocal, transferDatabaseLocalToRemote, transferStorageLocalToLocal, transferStorageLocalToRemote, transferUsersLocalToRemote, } from "./migrations/transfer.js";
15
16
  import { getClient } from "./utils/getClientFromConfig.js";
17
+ import { getAdapterFromConfig } from "./utils/getClientFromConfig.js";
16
18
  import { fetchAllDatabases } from "./databases/methods.js";
17
19
  import { listFunctions, updateFunctionSpecifications, } from "./functions/methods.js";
18
20
  import chalk from "chalk";
@@ -291,9 +293,20 @@ export class UtilsController {
291
293
  }
292
294
  async wipeDatabase(database, wipeBucket = false) {
293
295
  await this.init();
294
- if (!this.database)
296
+ if (!this.database || !this.config)
295
297
  throw new Error("Database not initialized");
296
- await wipeDatabase(this.database, database.$id);
298
+ try {
299
+ const { adapter, apiMode } = await getAdapterFromConfig(this.config);
300
+ if (apiMode === 'tablesdb') {
301
+ await wipeAllTables(adapter, database.$id);
302
+ }
303
+ else {
304
+ await wipeDatabase(this.database, database.$id);
305
+ }
306
+ }
307
+ catch {
308
+ await wipeDatabase(this.database, database.$id);
309
+ }
297
310
  if (wipeBucket) {
298
311
  await this.wipeBucketFromDatabase(database);
299
312
  }
@@ -323,9 +336,20 @@ export class UtilsController {
323
336
  }
324
337
  async wipeCollection(database, collection) {
325
338
  await this.init();
326
- if (!this.database)
339
+ if (!this.database || !this.config)
327
340
  throw new Error("Database not initialized");
328
- await wipeCollection(this.database, database.$id, collection.$id);
341
+ try {
342
+ const { adapter, apiMode } = await getAdapterFromConfig(this.config);
343
+ if (apiMode === 'tablesdb') {
344
+ await wipeTableRows(adapter, database.$id, collection.$id);
345
+ }
346
+ else {
347
+ await wipeCollection(this.database, database.$id, collection.$id);
348
+ }
349
+ }
350
+ catch {
351
+ await wipeCollection(this.database, database.$id, collection.$id);
352
+ }
329
353
  }
330
354
  async wipeDocumentStorage(bucketId) {
331
355
  await this.init();
@@ -418,9 +442,12 @@ export class UtilsController {
418
442
  const allDatabases = await fetchAllDatabases(this.database);
419
443
  databases = allDatabases;
420
444
  }
445
+ // Ensure DBs exist (this may internally ensure migrations exists)
421
446
  await this.ensureDatabasesExist(databases);
422
447
  await this.ensureDatabaseConfigBucketsExist(databases);
423
- await this.createOrUpdateCollectionsForDatabases(databases, collections);
448
+ // Do not push collections to the migrations database (prevents duplicate runs)
449
+ const dbsForCollections = databases.filter((db) => (this.config?.useMigrations ?? true) ? db.name.toLowerCase() !== "migrations" : true);
450
+ await this.createOrUpdateCollectionsForDatabases(dbsForCollections, collections);
424
451
  }
425
452
  getAppwriteFolderPath() {
426
453
  return this.appwriteFolderPath;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "appwrite-utils-cli",
3
3
  "description": "Appwrite Utility Functions to help with database management, data conversion, data import, migrations, and much more. Meant to be used as a CLI tool, I do not recommend installing this in frontend environments.",
4
- "version": "1.4.1",
4
+ "version": "1.5.1",
5
5
  "main": "src/main.ts",
6
6
  "type": "module",
7
7
  "repository": {
@@ -246,12 +246,13 @@ export class TablesDBAdapter extends BaseAdapter {
246
246
  }
247
247
 
248
248
  // Attribute Operations
249
- async createAttribute(params: CreateAttributeParams): Promise<ApiResponse> {
250
- try {
251
- // TablesDB may have different method names for attribute operations
252
- const result = await this.tablesDB.createAttribute(params);
253
- return { data: result };
254
- } catch (error) {
249
+ async createAttribute(params: CreateAttributeParams): Promise<ApiResponse> {
250
+ try {
251
+ // Prefer createColumn if available, fallback to createAttribute
252
+ const fn = this.tablesDB.createColumn || this.tablesDB.createAttribute;
253
+ const result = await fn.call(this.tablesDB, params);
254
+ return { data: result };
255
+ } catch (error) {
255
256
  throw new AdapterError(
256
257
  `Failed to create attribute: ${error instanceof Error ? error.message : 'Unknown error'}`,
257
258
  'CREATE_ATTRIBUTE_FAILED',
@@ -260,11 +261,12 @@ export class TablesDBAdapter extends BaseAdapter {
260
261
  }
261
262
  }
262
263
 
263
- async updateAttribute(params: UpdateAttributeParams): Promise<ApiResponse> {
264
- try {
265
- const result = await this.tablesDB.updateAttribute(params);
266
- return { data: result };
267
- } catch (error) {
264
+ async updateAttribute(params: UpdateAttributeParams): Promise<ApiResponse> {
265
+ try {
266
+ const fn = this.tablesDB.updateColumn || this.tablesDB.updateAttribute;
267
+ const result = await fn.call(this.tablesDB, params);
268
+ return { data: result };
269
+ } catch (error) {
268
270
  throw new AdapterError(
269
271
  `Failed to update attribute: ${error instanceof Error ? error.message : 'Unknown error'}`,
270
272
  'UPDATE_ATTRIBUTE_FAILED',
@@ -273,11 +275,12 @@ export class TablesDBAdapter extends BaseAdapter {
273
275
  }
274
276
  }
275
277
 
276
- async deleteAttribute(params: DeleteAttributeParams): Promise<ApiResponse> {
277
- try {
278
- const result = await this.tablesDB.deleteAttribute(params);
279
- return { data: result };
280
- } catch (error) {
278
+ async deleteAttribute(params: DeleteAttributeParams): Promise<ApiResponse> {
279
+ try {
280
+ const fn = this.tablesDB.deleteColumn || this.tablesDB.deleteAttribute;
281
+ const result = await fn.call(this.tablesDB, params);
282
+ return { data: result };
283
+ } catch (error) {
281
284
  throw new AdapterError(
282
285
  `Failed to delete attribute: ${error instanceof Error ? error.message : 'Unknown error'}`,
283
286
  'DELETE_ATTRIBUTE_FAILED',
@@ -426,4 +429,4 @@ export class TablesDBAdapter extends BaseAdapter {
426
429
  );
427
430
  }
428
431
  }
429
- }
432
+ }
@@ -224,33 +224,33 @@ const attributesSame = (
224
224
  databaseAttribute: Attribute,
225
225
  configAttribute: Attribute
226
226
  ): boolean => {
227
- const attributesToCheck = [
228
- "key",
229
- "type",
230
- "array",
231
- "encrypted",
232
- "required",
233
- "size",
234
- "min",
235
- "max",
236
- "xdefault",
237
- "elements",
238
- "relationType",
239
- "twoWay",
240
- "twoWayKey",
241
- "onDelete",
242
- "relatedCollection",
243
- ];
244
-
245
- return attributesToCheck.every((attr) => {
246
- // Check if both objects have the attribute
247
- const dbHasAttr = attr in databaseAttribute;
248
- const configHasAttr = attr in configAttribute;
227
+ const attributesToCheck = [
228
+ "key",
229
+ "type",
230
+ "array",
231
+ "encrypted",
232
+ "required",
233
+ "size",
234
+ "min",
235
+ "max",
236
+ "xdefault",
237
+ "elements",
238
+ "relationType",
239
+ "twoWay",
240
+ "twoWayKey",
241
+ "onDelete",
242
+ "relatedCollection",
243
+ ];
244
+
245
+ return attributesToCheck.every((attr) => {
246
+ // Check if both objects have the attribute
247
+ const dbHasAttr = attr in databaseAttribute;
248
+ const configHasAttr = attr in configAttribute;
249
249
 
250
250
  // If both have the attribute, compare values
251
- if (dbHasAttr && configHasAttr) {
252
- const dbValue = databaseAttribute[attr as keyof typeof databaseAttribute];
253
- const configValue = configAttribute[attr as keyof typeof configAttribute];
251
+ if (dbHasAttr && configHasAttr) {
252
+ const dbValue = databaseAttribute[attr as keyof typeof databaseAttribute];
253
+ const configValue = configAttribute[attr as keyof typeof configAttribute];
254
254
 
255
255
  // Consider undefined and null as equivalent
256
256
  if (
@@ -260,8 +260,19 @@ const attributesSame = (
260
260
  return true;
261
261
  }
262
262
 
263
- return dbValue === configValue;
264
- }
263
+ // Normalize booleans: treat undefined and false as equivalent
264
+ if (typeof dbValue === "boolean" || typeof configValue === "boolean") {
265
+ return Boolean(dbValue) === Boolean(configValue);
266
+ }
267
+ // For numeric comparisons, compare numbers if both are numeric-like
268
+ if (
269
+ (typeof dbValue === "number" || (typeof dbValue === "string" && dbValue !== "" && !isNaN(Number(dbValue)))) &&
270
+ (typeof configValue === "number" || (typeof configValue === "string" && configValue !== "" && !isNaN(Number(configValue))))
271
+ ) {
272
+ return Number(dbValue) === Number(configValue);
273
+ }
274
+ return dbValue === configValue;
275
+ }
265
276
 
266
277
  // If neither has the attribute, consider it the same
267
278
  if (!dbHasAttr && !configHasAttr) {
@@ -269,15 +280,23 @@ const attributesSame = (
269
280
  }
270
281
 
271
282
  // If one has the attribute and the other doesn't, check if it's undefined or null
272
- if (dbHasAttr && !configHasAttr) {
273
- const dbValue = databaseAttribute[attr as keyof typeof databaseAttribute];
274
- return dbValue === undefined || dbValue === null;
275
- }
276
-
277
- if (!dbHasAttr && configHasAttr) {
278
- const configValue = configAttribute[attr as keyof typeof configAttribute];
279
- return configValue === undefined || configValue === null;
280
- }
283
+ if (dbHasAttr && !configHasAttr) {
284
+ const dbValue = databaseAttribute[attr as keyof typeof databaseAttribute];
285
+ // Consider default-false booleans as equal to missing in config
286
+ if (typeof dbValue === "boolean") {
287
+ return dbValue === false; // missing in config equals false in db
288
+ }
289
+ return dbValue === undefined || dbValue === null;
290
+ }
291
+
292
+ if (!dbHasAttr && configHasAttr) {
293
+ const configValue = configAttribute[attr as keyof typeof configAttribute];
294
+ // Consider default-false booleans as equal to missing in db
295
+ if (typeof configValue === "boolean") {
296
+ return configValue === false; // missing in db equals false in config
297
+ }
298
+ return configValue === undefined || configValue === null;
299
+ }
281
300
 
282
301
  // If we reach here, the attributes are different
283
302
  return false;
@@ -287,14 +306,14 @@ const attributesSame = (
287
306
  /**
288
307
  * Enhanced attribute creation with proper status monitoring and retry logic
289
308
  */
290
- export const createOrUpdateAttributeWithStatusCheck = async (
291
- db: Databases,
292
- dbId: string,
293
- collection: Models.Collection,
294
- attribute: Attribute,
295
- retryCount: number = 0,
296
- maxRetries: number = 5
297
- ): Promise<boolean> => {
309
+ export const createOrUpdateAttributeWithStatusCheck = async (
310
+ db: Databases,
311
+ dbId: string,
312
+ collection: Models.Collection,
313
+ attribute: Attribute,
314
+ retryCount: number = 0,
315
+ maxRetries: number = 5
316
+ ): Promise<boolean> => {
298
317
  console.log(
299
318
  chalk.blue(
300
319
  `Creating/updating attribute '${attribute.key}' (attempt ${
@@ -303,20 +322,31 @@ export const createOrUpdateAttributeWithStatusCheck = async (
303
322
  )
304
323
  );
305
324
 
306
- try {
307
- // First, try to create/update the attribute using existing logic
308
- await createOrUpdateAttribute(db, dbId, collection, attribute);
309
-
310
- // Now wait for the attribute to become available
311
- const success = await waitForAttributeAvailable(
312
- db,
313
- dbId,
314
- collection.$id,
315
- attribute.key,
316
- 60000, // 1 minute timeout
317
- retryCount,
318
- maxRetries
319
- );
325
+ try {
326
+ // First, try to create/update the attribute using existing logic
327
+ const result = await createOrUpdateAttribute(db, dbId, collection, attribute);
328
+
329
+ // If the attribute was queued (relationship dependency unresolved),
330
+ // skip status polling and retry logic — the queue will handle it later.
331
+ if (result === "queued") {
332
+ console.log(
333
+ chalk.yellow(
334
+ `⏭️ Deferred relationship attribute '${attribute.key}' — queued for later once dependencies are available`
335
+ )
336
+ );
337
+ return true;
338
+ }
339
+
340
+ // Now wait for the attribute to become available
341
+ const success = await waitForAttributeAvailable(
342
+ db,
343
+ dbId,
344
+ collection.$id,
345
+ attribute.key,
346
+ 60000, // 1 minute timeout
347
+ retryCount,
348
+ maxRetries
349
+ );
320
350
 
321
351
  if (success) {
322
352
  return true;
@@ -440,12 +470,12 @@ export const createOrUpdateAttributeWithStatusCheck = async (
440
470
  }
441
471
  };
442
472
 
443
- export const createOrUpdateAttribute = async (
444
- db: Databases,
445
- dbId: string,
446
- collection: Models.Collection,
447
- attribute: Attribute
448
- ): Promise<void> => {
473
+ export const createOrUpdateAttribute = async (
474
+ db: Databases,
475
+ dbId: string,
476
+ collection: Models.Collection,
477
+ attribute: Attribute
478
+ ): Promise<"queued" | "processed"> => {
449
479
  let action = "create";
450
480
  let foundAttribute: Attribute | undefined;
451
481
  const updateEnabled = true;
@@ -460,13 +490,13 @@ export const createOrUpdateAttribute = async (
460
490
  foundAttribute = undefined;
461
491
  }
462
492
 
463
- if (
464
- foundAttribute &&
465
- attributesSame(foundAttribute, attribute) &&
466
- updateEnabled
467
- ) {
468
- // No need to do anything, they are the same
469
- return;
493
+ if (
494
+ foundAttribute &&
495
+ attributesSame(foundAttribute, attribute) &&
496
+ updateEnabled
497
+ ) {
498
+ // No need to do anything, they are the same
499
+ return "processed";
470
500
  } else if (
471
501
  foundAttribute &&
472
502
  !attributesSame(foundAttribute, attribute) &&
@@ -484,71 +514,82 @@ export const createOrUpdateAttribute = async (
484
514
  !updateEnabled &&
485
515
  foundAttribute &&
486
516
  !attributesSame(foundAttribute, attribute)
487
- ) {
488
- await db.deleteAttribute(dbId, collection.$id, attribute.key);
489
- console.log(
490
- `Deleted attribute: ${attribute.key} to recreate it because they diff (update disabled temporarily)`
491
- );
492
- return;
493
- }
517
+ ) {
518
+ await db.deleteAttribute(dbId, collection.$id, attribute.key);
519
+ console.log(
520
+ `Deleted attribute: ${attribute.key} to recreate it because they diff (update disabled temporarily)`
521
+ );
522
+ return "processed";
523
+ }
494
524
 
495
525
  // console.log(`${action}-ing attribute: ${finalAttribute.key}`);
496
526
 
497
527
  // Relationship attribute logic with adjustments
498
528
  let collectionFoundViaRelatedCollection: Models.Collection | undefined;
499
529
  let relatedCollectionId: string | undefined;
500
- if (
501
- finalAttribute.type === "relationship" &&
502
- finalAttribute.relatedCollection
503
- ) {
504
- if (nameToIdMapping.has(finalAttribute.relatedCollection)) {
505
- relatedCollectionId = nameToIdMapping.get(
506
- finalAttribute.relatedCollection
507
- );
508
- try {
509
- collectionFoundViaRelatedCollection = await db.getCollection(
510
- dbId,
511
- relatedCollectionId!
512
- );
513
- } catch (e) {
514
- // console.log(
515
- // `Collection not found: ${finalAttribute.relatedCollection} when nameToIdMapping was set`
516
- // );
517
- collectionFoundViaRelatedCollection = undefined;
518
- }
519
- } else {
520
- const collectionsPulled = await db.listCollections(dbId, [
521
- Query.equal("name", finalAttribute.relatedCollection),
522
- ]);
523
- if (collectionsPulled.total > 0) {
524
- collectionFoundViaRelatedCollection = collectionsPulled.collections[0];
525
- relatedCollectionId = collectionFoundViaRelatedCollection.$id;
526
- nameToIdMapping.set(
527
- finalAttribute.relatedCollection,
528
- relatedCollectionId
529
- );
530
- }
531
- }
532
- // ONLY queue relationship attributes that have actual unresolved dependencies
533
- if (!(relatedCollectionId && collectionFoundViaRelatedCollection)) {
534
- console.log(
535
- chalk.yellow(
536
- `⏳ Queueing relationship attribute '${finalAttribute.key}' - related collection '${finalAttribute.relatedCollection}' not found yet`
537
- )
538
- );
539
- enqueueOperation({
540
- type: "attribute",
541
- collectionId: collection.$id,
542
- collection: collection,
543
- attribute,
544
- dependencies: [finalAttribute.relatedCollection],
545
- });
546
- return;
547
- }
548
- }
549
- finalAttribute = parseAttribute(finalAttribute);
550
- // console.log(`Final Attribute: ${JSON.stringify(finalAttribute)}`);
551
- switch (finalAttribute.type) {
530
+ if (
531
+ finalAttribute.type === "relationship" &&
532
+ finalAttribute.relatedCollection
533
+ ) {
534
+ // First try treating relatedCollection as an ID directly
535
+ try {
536
+ const byIdCollection = await db.getCollection(dbId, finalAttribute.relatedCollection);
537
+ collectionFoundViaRelatedCollection = byIdCollection;
538
+ relatedCollectionId = byIdCollection.$id;
539
+ // Cache by name for subsequent lookups
540
+ nameToIdMapping.set(byIdCollection.name, byIdCollection.$id);
541
+ } catch (_) {
542
+ // Not an ID or not found — fall back to name-based resolution below
543
+ }
544
+
545
+ if (!collectionFoundViaRelatedCollection && nameToIdMapping.has(finalAttribute.relatedCollection)) {
546
+ relatedCollectionId = nameToIdMapping.get(
547
+ finalAttribute.relatedCollection
548
+ );
549
+ try {
550
+ collectionFoundViaRelatedCollection = await db.getCollection(
551
+ dbId,
552
+ relatedCollectionId!
553
+ );
554
+ } catch (e) {
555
+ // console.log(
556
+ // `Collection not found: ${finalAttribute.relatedCollection} when nameToIdMapping was set`
557
+ // );
558
+ collectionFoundViaRelatedCollection = undefined;
559
+ }
560
+ } else if (!collectionFoundViaRelatedCollection) {
561
+ const collectionsPulled = await db.listCollections(dbId, [
562
+ Query.equal("name", finalAttribute.relatedCollection),
563
+ ]);
564
+ if (collectionsPulled.total > 0) {
565
+ collectionFoundViaRelatedCollection = collectionsPulled.collections[0];
566
+ relatedCollectionId = collectionFoundViaRelatedCollection.$id;
567
+ nameToIdMapping.set(
568
+ finalAttribute.relatedCollection,
569
+ relatedCollectionId
570
+ );
571
+ }
572
+ }
573
+ // ONLY queue relationship attributes that have actual unresolved dependencies
574
+ if (!(relatedCollectionId && collectionFoundViaRelatedCollection)) {
575
+ console.log(
576
+ chalk.yellow(
577
+ `⏳ Queueing relationship attribute '${finalAttribute.key}' - related collection '${finalAttribute.relatedCollection}' not found yet`
578
+ )
579
+ );
580
+ enqueueOperation({
581
+ type: "attribute",
582
+ collectionId: collection.$id,
583
+ collection: collection,
584
+ attribute,
585
+ dependencies: [finalAttribute.relatedCollection],
586
+ });
587
+ return "queued";
588
+ }
589
+ }
590
+ finalAttribute = parseAttribute(finalAttribute);
591
+ // console.log(`Final Attribute: ${JSON.stringify(finalAttribute)}`);
592
+ switch (finalAttribute.type) {
552
593
  case "string":
553
594
  if (action === "create") {
554
595
  await tryAwaitWithRetry(
@@ -624,27 +665,27 @@ export const createOrUpdateAttribute = async (
624
665
  finalAttribute.array || false
625
666
  )
626
667
  );
627
- } else {
628
- if (
629
- finalAttribute.min &&
630
- BigInt(finalAttribute.min) === BigInt(-9223372036854776000)
631
- ) {
632
- finalAttribute.min = undefined;
633
- }
634
- if (
635
- finalAttribute.max &&
636
- BigInt(finalAttribute.max) === BigInt(9223372036854776000)
637
- ) {
638
- finalAttribute.max = undefined;
639
- }
640
- const minValue =
641
- finalAttribute.min !== undefined && finalAttribute.min !== null
642
- ? parseInt(finalAttribute.min)
643
- : 9007199254740991;
644
- const maxValue =
645
- finalAttribute.max !== undefined && finalAttribute.max !== null
646
- ? parseInt(finalAttribute.max)
647
- : 9007199254740991;
668
+ } else {
669
+ if (
670
+ finalAttribute.min &&
671
+ BigInt(finalAttribute.min) === BigInt(-9223372036854776000)
672
+ ) {
673
+ finalAttribute.min = undefined;
674
+ }
675
+ if (
676
+ finalAttribute.max &&
677
+ BigInt(finalAttribute.max) === BigInt(9223372036854776000)
678
+ ) {
679
+ finalAttribute.max = undefined;
680
+ }
681
+ const minValue =
682
+ finalAttribute.min !== undefined && finalAttribute.min !== null
683
+ ? parseInt(finalAttribute.min)
684
+ : -9007199254740991;
685
+ const maxValue =
686
+ finalAttribute.max !== undefined && finalAttribute.max !== null
687
+ ? parseInt(finalAttribute.max)
688
+ : 9007199254740991;
648
689
  console.log(
649
690
  `DEBUG: Updating integer attribute '${
650
691
  finalAttribute.key
@@ -910,11 +951,12 @@ export const createOrUpdateAttribute = async (
910
951
  );
911
952
  }
912
953
  break;
913
- default:
914
- console.error("Invalid attribute type");
915
- break;
916
- }
917
- };
954
+ default:
955
+ console.error("Invalid attribute type");
956
+ break;
957
+ }
958
+ return "processed";
959
+ };
918
960
 
919
961
  /**
920
962
  * Enhanced collection attribute creation with proper status monitoring