appwrite-utils-cli 1.6.2 → 1.6.4

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 (62) hide show
  1. package/CONFIG_TODO.md +1189 -0
  2. package/SERVICE_IMPLEMENTATION_REPORT.md +462 -0
  3. package/dist/cli/commands/configCommands.js +7 -1
  4. package/dist/cli/commands/databaseCommands.js +23 -15
  5. package/dist/collections/attributes.d.ts +1 -1
  6. package/dist/collections/attributes.js +163 -66
  7. package/dist/collections/indexes.js +3 -17
  8. package/dist/collections/methods.js +38 -0
  9. package/dist/config/ConfigManager.d.ts +445 -0
  10. package/dist/config/ConfigManager.js +625 -0
  11. package/dist/config/index.d.ts +8 -0
  12. package/dist/config/index.js +7 -0
  13. package/dist/config/services/ConfigDiscoveryService.d.ts +126 -0
  14. package/dist/config/services/ConfigDiscoveryService.js +374 -0
  15. package/dist/config/services/ConfigLoaderService.d.ts +105 -0
  16. package/dist/config/services/ConfigLoaderService.js +410 -0
  17. package/dist/config/services/ConfigMergeService.d.ts +208 -0
  18. package/dist/config/services/ConfigMergeService.js +307 -0
  19. package/dist/config/services/ConfigValidationService.d.ts +214 -0
  20. package/dist/config/services/ConfigValidationService.js +310 -0
  21. package/dist/config/services/SessionAuthService.d.ts +225 -0
  22. package/dist/config/services/SessionAuthService.js +456 -0
  23. package/dist/config/services/__tests__/ConfigMergeService.test.d.ts +1 -0
  24. package/dist/config/services/__tests__/ConfigMergeService.test.js +271 -0
  25. package/dist/config/services/index.d.ts +13 -0
  26. package/dist/config/services/index.js +10 -0
  27. package/dist/interactiveCLI.js +8 -6
  28. package/dist/main.js +2 -2
  29. package/dist/migrations/yaml/YamlImportConfigLoader.d.ts +1 -1
  30. package/dist/shared/operationQueue.js +1 -1
  31. package/dist/utils/ClientFactory.d.ts +87 -0
  32. package/dist/utils/ClientFactory.js +164 -0
  33. package/dist/utils/getClientFromConfig.js +4 -3
  34. package/dist/utils/helperFunctions.d.ts +1 -0
  35. package/dist/utils/helperFunctions.js +21 -5
  36. package/dist/utils/yamlConverter.d.ts +2 -0
  37. package/dist/utils/yamlConverter.js +21 -4
  38. package/dist/utilsController.d.ts +18 -15
  39. package/dist/utilsController.js +83 -131
  40. package/package.json +1 -1
  41. package/src/cli/commands/configCommands.ts +8 -1
  42. package/src/cli/commands/databaseCommands.ts +34 -20
  43. package/src/collections/attributes.ts +195 -150
  44. package/src/collections/indexes.ts +4 -19
  45. package/src/collections/methods.ts +46 -0
  46. package/src/config/ConfigManager.ts +808 -0
  47. package/src/config/index.ts +10 -0
  48. package/src/config/services/ConfigDiscoveryService.ts +463 -0
  49. package/src/config/services/ConfigLoaderService.ts +560 -0
  50. package/src/config/services/ConfigMergeService.ts +386 -0
  51. package/src/config/services/ConfigValidationService.ts +394 -0
  52. package/src/config/services/SessionAuthService.ts +565 -0
  53. package/src/config/services/__tests__/ConfigMergeService.test.ts +351 -0
  54. package/src/config/services/index.ts +29 -0
  55. package/src/interactiveCLI.ts +9 -7
  56. package/src/main.ts +2 -2
  57. package/src/shared/operationQueue.ts +1 -1
  58. package/src/utils/ClientFactory.ts +186 -0
  59. package/src/utils/getClientFromConfig.ts +4 -3
  60. package/src/utils/helperFunctions.ts +27 -7
  61. package/src/utils/yamlConverter.ts +28 -2
  62. package/src/utilsController.ts +99 -187
@@ -6,8 +6,8 @@ import chalk from "chalk";
6
6
  import { logger } from "../shared/logging.js";
7
7
  import { MessageFormatter } from "../shared/messageFormatter.js";
8
8
  import { isDatabaseAdapter } from "../utils/typeGuards.js";
9
- // Threshold for treating min/max values as undefined (10 billion)
10
- const MIN_MAX_THRESHOLD = 10_000_000_000;
9
+ // Threshold for treating min/max values as undefined (1 trillion)
10
+ const MIN_MAX_THRESHOLD = 1_000_000_000_000;
11
11
  // Extreme values that Appwrite may return, which should be treated as undefined
12
12
  const EXTREME_MIN_INTEGER = -9223372036854776000;
13
13
  const EXTREME_MAX_INTEGER = 9223372036854776000;
@@ -121,11 +121,19 @@ const normalizeMinMaxValues = (attribute) => {
121
121
  * This is used when comparing database attributes with config attributes
122
122
  */
123
123
  const normalizeAttributeForComparison = (attribute) => {
124
- if (!hasMinMaxProperties(attribute)) {
125
- return attribute;
124
+ const normalized = { ...attribute };
125
+ // Normalize min/max for numeric types
126
+ if (hasMinMaxProperties(attribute)) {
127
+ const { min, max } = normalizeMinMaxValues(attribute);
128
+ normalized.min = min;
129
+ normalized.max = max;
130
+ }
131
+ // Remove xdefault if null/undefined to ensure consistent comparison
132
+ // Appwrite sets xdefault: null for required attributes, but config files omit it
133
+ if ('xdefault' in normalized && (normalized.xdefault === null || normalized.xdefault === undefined)) {
134
+ delete normalized.xdefault;
126
135
  }
127
- const { min, max } = normalizeMinMaxValues(attribute);
128
- return { ...attribute, min, max };
136
+ return normalized;
129
137
  };
130
138
  /**
131
139
  * Helper function to create an attribute using either the adapter or legacy API
@@ -296,32 +304,32 @@ const updateLegacyAttribute = async (db, dbId, collectionId, attribute) => {
296
304
  const { min: normalizedMin, max: normalizedMax } = normalizeMinMaxValues(attribute);
297
305
  switch (attribute.type) {
298
306
  case "string":
299
- await db.updateStringAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && !attribute.required ? attribute.xdefault : undefined, attribute.size);
307
+ await db.updateStringAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined ? attribute.xdefault : null, attribute.size);
300
308
  break;
301
309
  case "integer":
302
- await db.updateIntegerAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && !attribute.required ? attribute.xdefault : undefined, normalizedMin !== undefined ? parseInt(String(normalizedMin)) : undefined, normalizedMax !== undefined ? parseInt(String(normalizedMax)) : undefined);
310
+ await db.updateIntegerAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined ? attribute.xdefault : null, normalizedMin !== undefined ? parseInt(String(normalizedMin)) : undefined, normalizedMax !== undefined ? parseInt(String(normalizedMax)) : undefined);
303
311
  break;
304
312
  case "double":
305
313
  case "float":
306
- await db.updateFloatAttribute(dbId, collectionId, attribute.key, attribute.required || false, normalizedMin !== undefined ? Number(normalizedMin) : undefined, normalizedMax !== undefined ? Number(normalizedMax) : undefined, attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined);
314
+ await db.updateFloatAttribute(dbId, collectionId, attribute.key, attribute.required || false, normalizedMin !== undefined ? Number(normalizedMin) : undefined, normalizedMax !== undefined ? Number(normalizedMax) : undefined, !attribute.required && attribute.xdefault !== undefined ? attribute.xdefault : null);
307
315
  break;
308
316
  case "boolean":
309
- await db.updateBooleanAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined);
317
+ await db.updateBooleanAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined ? attribute.xdefault : null);
310
318
  break;
311
319
  case "datetime":
312
- await db.updateDatetimeAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined);
320
+ await db.updateDatetimeAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined ? attribute.xdefault : null);
313
321
  break;
314
322
  case "email":
315
- await db.updateEmailAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined);
323
+ await db.updateEmailAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined ? attribute.xdefault : null);
316
324
  break;
317
325
  case "ip":
318
- await db.updateIpAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined);
326
+ await db.updateIpAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined ? attribute.xdefault : null);
319
327
  break;
320
328
  case "url":
321
- await db.updateUrlAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined);
329
+ await db.updateUrlAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined ? attribute.xdefault : null);
322
330
  break;
323
331
  case "enum":
324
- await db.updateEnumAttribute(dbId, collectionId, attribute.key, attribute.elements || [], attribute.required || false, attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined);
332
+ await db.updateEnumAttribute(dbId, collectionId, attribute.key, attribute.elements || [], attribute.required || false, !attribute.required && attribute.xdefault !== undefined ? attribute.xdefault : null);
325
333
  break;
326
334
  case "relationship":
327
335
  await db.updateRelationshipAttribute(dbId, collectionId, attribute.key, attribute.onDelete);
@@ -348,12 +356,8 @@ retryCount = 0, maxRetries = 5) => {
348
356
  // Calculate exponential backoff: 2s, 4s, 8s, 16s, 30s (capped at 30s)
349
357
  if (retryCount > 0) {
350
358
  const exponentialDelay = calculateExponentialBackoff(retryCount);
351
- MessageFormatter.info(chalk.blue(`Waiting for attribute '${attributeKey}' to become available (retry ${retryCount}, backoff: ${exponentialDelay}ms)...`));
352
359
  await delay(exponentialDelay);
353
360
  }
354
- else {
355
- MessageFormatter.info(chalk.blue(`Waiting for attribute '${attributeKey}' to become available...`));
356
- }
357
361
  while (Date.now() - startTime < maxWaitTime) {
358
362
  try {
359
363
  const collection = isDatabaseAdapter(db)
@@ -364,7 +368,6 @@ retryCount = 0, maxRetries = 5) => {
364
368
  MessageFormatter.error(`Attribute '${attributeKey}' not found`);
365
369
  return false;
366
370
  }
367
- MessageFormatter.info(chalk.gray(`Attribute '${attributeKey}' status: ${attribute.status}`));
368
371
  const statusInfo = {
369
372
  attributeKey,
370
373
  status: attribute.status,
@@ -376,15 +379,12 @@ retryCount = 0, maxRetries = 5) => {
376
379
  };
377
380
  switch (attribute.status) {
378
381
  case "available":
379
- MessageFormatter.info(chalk.green(`✅ Attribute '${attributeKey}' is now available`));
380
382
  logger.info(`Attribute '${attributeKey}' became available`, statusInfo);
381
383
  return true;
382
384
  case "failed":
383
- MessageFormatter.info(chalk.red(`❌ Attribute '${attributeKey}' failed: ${attribute.error}`));
384
385
  logger.error(`Attribute '${attributeKey}' failed`, statusInfo);
385
386
  return false;
386
387
  case "stuck":
387
- MessageFormatter.info(chalk.yellow(`⚠️ Attribute '${attributeKey}' is stuck, will retry...`));
388
388
  logger.warn(`Attribute '${attributeKey}' is stuck`, statusInfo);
389
389
  return false;
390
390
  case "processing":
@@ -475,28 +475,46 @@ const deleteAndRecreateCollection = async (db, dbId, collection, retryCount) =>
475
475
  return null;
476
476
  }
477
477
  };
478
+ /**
479
+ * Get the fields that should be compared for a specific attribute type
480
+ * Only returns fields that are valid for the given type to avoid false positives
481
+ */
482
+ const getComparableFields = (type) => {
483
+ const baseFields = ["key", "type", "array", "required", "xdefault"];
484
+ switch (type) {
485
+ case "string":
486
+ return [...baseFields, "size", "encrypted"];
487
+ case "integer":
488
+ case "double":
489
+ case "float":
490
+ return [...baseFields, "min", "max"];
491
+ case "enum":
492
+ return [...baseFields, "elements"];
493
+ case "relationship":
494
+ return [...baseFields, "relationType", "twoWay", "twoWayKey", "onDelete", "relatedCollection"];
495
+ case "boolean":
496
+ case "datetime":
497
+ case "email":
498
+ case "ip":
499
+ case "url":
500
+ return baseFields;
501
+ default:
502
+ // Fallback to all fields for unknown types
503
+ return [
504
+ "key", "type", "array", "encrypted", "required", "size",
505
+ "min", "max", "xdefault", "elements", "relationType",
506
+ "twoWay", "twoWayKey", "onDelete", "relatedCollection"
507
+ ];
508
+ }
509
+ };
478
510
  const attributesSame = (databaseAttribute, configAttribute) => {
479
511
  // Normalize both attributes for comparison (handle extreme database values)
480
512
  const normalizedDbAttr = normalizeAttributeForComparison(databaseAttribute);
481
513
  const normalizedConfigAttr = normalizeAttributeForComparison(configAttribute);
482
- const attributesToCheck = [
483
- "key",
484
- "type",
485
- "array",
486
- "encrypted",
487
- "required",
488
- "size",
489
- "min",
490
- "max",
491
- "xdefault",
492
- "elements",
493
- "relationType",
494
- "twoWay",
495
- "twoWayKey",
496
- "onDelete",
497
- "relatedCollection",
498
- ];
499
- return attributesToCheck.every((attr) => {
514
+ // Use type-specific field list to avoid false positives from irrelevant fields
515
+ const attributesToCheck = getComparableFields(normalizedConfigAttr.type);
516
+ const differences = [];
517
+ const result = attributesToCheck.every((attr) => {
500
518
  // Check if both objects have the attribute
501
519
  const dbHasAttr = attr in normalizedDbAttr;
502
520
  const configHasAttr = attr in normalizedConfigAttr;
@@ -511,14 +529,35 @@ const attributesSame = (databaseAttribute, configAttribute) => {
511
529
  }
512
530
  // Normalize booleans: treat undefined and false as equivalent
513
531
  if (typeof dbValue === "boolean" || typeof configValue === "boolean") {
514
- return Boolean(dbValue) === Boolean(configValue);
532
+ const boolMatch = Boolean(dbValue) === Boolean(configValue);
533
+ if (!boolMatch) {
534
+ differences.push(`${attr}: db=${dbValue} config=${configValue}`);
535
+ }
536
+ return boolMatch;
515
537
  }
516
538
  // For numeric comparisons, compare numbers if both are numeric-like
517
539
  if ((typeof dbValue === "number" || (typeof dbValue === "string" && dbValue !== "" && !isNaN(Number(dbValue)))) &&
518
540
  (typeof configValue === "number" || (typeof configValue === "string" && configValue !== "" && !isNaN(Number(configValue))))) {
519
- return Number(dbValue) === Number(configValue);
541
+ const numMatch = Number(dbValue) === Number(configValue);
542
+ if (!numMatch) {
543
+ differences.push(`${attr}: db=${dbValue} config=${configValue}`);
544
+ }
545
+ return numMatch;
546
+ }
547
+ // For array comparisons (e.g., enum elements), use order-independent equality
548
+ if (Array.isArray(dbValue) && Array.isArray(configValue)) {
549
+ const arrayMatch = dbValue.length === configValue.length &&
550
+ dbValue.every((val) => configValue.includes(val));
551
+ if (!arrayMatch) {
552
+ differences.push(`${attr}: db=${JSON.stringify(dbValue)} config=${JSON.stringify(configValue)}`);
553
+ }
554
+ return arrayMatch;
555
+ }
556
+ const match = dbValue === configValue;
557
+ if (!match) {
558
+ differences.push(`${attr}: db=${JSON.stringify(dbValue)} config=${JSON.stringify(configValue)}`);
520
559
  }
521
- return dbValue === configValue;
560
+ return match;
522
561
  }
523
562
  // If neither has the attribute, consider it the same
524
563
  if (!dbHasAttr && !configHasAttr) {
@@ -529,27 +568,51 @@ const attributesSame = (databaseAttribute, configAttribute) => {
529
568
  const dbValue = normalizedDbAttr[attr];
530
569
  // Consider default-false booleans as equal to missing in config
531
570
  if (typeof dbValue === "boolean") {
532
- return dbValue === false; // missing in config equals false in db
571
+ const match = dbValue === false; // missing in config equals false in db
572
+ if (!match) {
573
+ differences.push(`${attr}: db=${dbValue} config=<missing>`);
574
+ }
575
+ return match;
576
+ }
577
+ const match = dbValue === undefined || dbValue === null;
578
+ if (!match) {
579
+ differences.push(`${attr}: db=${JSON.stringify(dbValue)} config=<missing>`);
533
580
  }
534
- return dbValue === undefined || dbValue === null;
581
+ return match;
535
582
  }
536
583
  if (!dbHasAttr && configHasAttr) {
537
584
  const configValue = normalizedConfigAttr[attr];
538
585
  // Consider default-false booleans as equal to missing in db
539
586
  if (typeof configValue === "boolean") {
540
- return configValue === false; // missing in db equals false in config
587
+ const match = configValue === false; // missing in db equals false in config
588
+ if (!match) {
589
+ differences.push(`${attr}: db=<missing> config=${configValue}`);
590
+ }
591
+ return match;
541
592
  }
542
- return configValue === undefined || configValue === null;
593
+ const match = configValue === undefined || configValue === null;
594
+ if (!match) {
595
+ differences.push(`${attr}: db=<missing> config=${JSON.stringify(configValue)}`);
596
+ }
597
+ return match;
543
598
  }
544
599
  // If we reach here, the attributes are different
600
+ differences.push(`${attr}: unexpected comparison state`);
545
601
  return false;
546
602
  });
603
+ // Log differences if any were found
604
+ if (differences.length > 0) {
605
+ logger.debug(`Attribute '${normalizedDbAttr.key}' comparison found differences:`, {
606
+ differences,
607
+ operation: 'attributesSame'
608
+ });
609
+ }
610
+ return result;
547
611
  };
548
612
  /**
549
613
  * Enhanced attribute creation with proper status monitoring and retry logic
550
614
  */
551
615
  export const createOrUpdateAttributeWithStatusCheck = async (db, dbId, collection, attribute, retryCount = 0, maxRetries = 5) => {
552
- MessageFormatter.info(chalk.blue(`Creating/updating attribute '${attribute.key}' (attempt ${retryCount + 1}/${maxRetries + 1})`));
553
616
  try {
554
617
  // First, try to create/update the attribute using existing logic
555
618
  const result = await createOrUpdateAttribute(db, dbId, collection, attribute);
@@ -559,6 +622,11 @@ export const createOrUpdateAttributeWithStatusCheck = async (db, dbId, collectio
559
622
  MessageFormatter.info(chalk.yellow(`⏭️ Deferred relationship attribute '${attribute.key}' — queued for later once dependencies are available`));
560
623
  return true;
561
624
  }
625
+ // If collection creation failed, return false to indicate failure
626
+ if (result === "error") {
627
+ MessageFormatter.error(`Failed to create collection for attribute '${attribute.key}'`);
628
+ return false;
629
+ }
562
630
  // Now wait for the attribute to become available
563
631
  const success = await waitForAttributeAvailable(db, dbId, collection.$id, attribute.key, 60000, // 1 minute timeout
564
632
  retryCount, maxRetries);
@@ -724,6 +792,45 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
724
792
  }
725
793
  }
726
794
  finalAttribute = parseAttribute(finalAttribute);
795
+ // Ensure collection/table exists - create it if it doesn't
796
+ try {
797
+ await (isDatabaseAdapter(db)
798
+ ? db.getTable({ databaseId: dbId, tableId: collection.$id })
799
+ : db.getCollection(dbId, collection.$id));
800
+ }
801
+ catch (error) {
802
+ // Collection doesn't exist - create it
803
+ if (error.code === 404 ||
804
+ (error instanceof Error && (error.message.includes('collection_not_found') ||
805
+ error.message.includes('Collection with the requested ID could not be found')))) {
806
+ MessageFormatter.info(`Collection '${collection.name}' doesn't exist, creating it first...`);
807
+ try {
808
+ if (isDatabaseAdapter(db)) {
809
+ await db.createTable({
810
+ databaseId: dbId,
811
+ id: collection.$id,
812
+ name: collection.name,
813
+ permissions: collection.$permissions || [],
814
+ documentSecurity: collection.documentSecurity ?? false,
815
+ enabled: collection.enabled ?? true
816
+ });
817
+ }
818
+ else {
819
+ await db.createCollection(dbId, collection.$id, collection.name, collection.$permissions || [], collection.documentSecurity ?? false, collection.enabled ?? true);
820
+ }
821
+ MessageFormatter.success(`Created collection '${collection.name}'`);
822
+ await delay(500); // Wait for collection to be ready
823
+ }
824
+ catch (createError) {
825
+ MessageFormatter.error(`Failed to create collection '${collection.name}'`, createError instanceof Error ? createError : new Error(String(createError)));
826
+ return "error";
827
+ }
828
+ }
829
+ else {
830
+ // Other error - re-throw
831
+ throw error;
832
+ }
833
+ }
727
834
  // Use adapter-based attribute creation/update
728
835
  if (action === "create") {
729
836
  await tryAwaitWithRetry(async () => await createAttributeViaAdapter(db, dbId, collection.$id, finalAttribute));
@@ -737,7 +844,6 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
737
844
  * Enhanced collection attribute creation with proper status monitoring
738
845
  */
739
846
  export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId, collection, attributes) => {
740
- MessageFormatter.info(chalk.green(`Creating/Updating attributes for collection: ${collection.name} with status monitoring`));
741
847
  const existingAttributes =
742
848
  // @ts-expect-error
743
849
  collection.attributes.map((attr) => parseAttribute(attr)) || [];
@@ -775,7 +881,6 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId,
775
881
  }
776
882
  }
777
883
  // First, get fresh collection data and determine which attributes actually need processing
778
- MessageFormatter.info(chalk.blue(`Analyzing ${attributes.length} attributes to determine which need processing...`));
779
884
  let currentCollection = collection;
780
885
  try {
781
886
  currentCollection = isDatabaseAdapter(db)
@@ -799,28 +904,25 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId,
799
904
  const attributesToProcess = attributes.filter((attribute) => {
800
905
  // Skip if already processed in this session
801
906
  if (isAttributeProcessed(currentCollection.$id, attribute.key)) {
802
- MessageFormatter.info(chalk.gray(`⏭️ Attribute '${attribute.key}' already processed in this session (skipping)`));
803
907
  return false;
804
908
  }
805
909
  const existing = existingAttributesMap.get(attribute.key);
806
910
  if (!existing) {
807
- MessageFormatter.info(`➕ New attribute: ${attribute.key}`);
911
+ MessageFormatter.info(`➕ ${attribute.key}`);
808
912
  return true;
809
913
  }
810
- const needsUpdate = !attributesSame(existing, attribute);
914
+ const needsUpdate = !attributesSame(existing, parseAttribute(attribute));
811
915
  if (needsUpdate) {
812
- MessageFormatter.info(`🔄 Changed attribute: ${attribute.key}`);
916
+ MessageFormatter.info(`🔄 ${attribute.key}`);
813
917
  }
814
918
  else {
815
- MessageFormatter.info(chalk.gray(`✅ Unchanged attribute: ${attribute.key} (skipping)`));
919
+ MessageFormatter.info(chalk.gray(`✅ ${attribute.key}`));
816
920
  }
817
921
  return needsUpdate;
818
922
  });
819
923
  if (attributesToProcess.length === 0) {
820
- MessageFormatter.info(chalk.green(`✅ All ${attributes.length} attributes are already up to date for collection: ${collection.name}`));
821
924
  return true;
822
925
  }
823
- MessageFormatter.info(chalk.blue(`Creating ${attributesToProcess.length} attributes sequentially with status monitoring...`));
824
926
  let remainingAttributes = [...attributesToProcess];
825
927
  let overallRetryCount = 0;
826
928
  const maxOverallRetries = 3;
@@ -828,12 +930,9 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId,
828
930
  overallRetryCount < maxOverallRetries) {
829
931
  const attributesToProcessThisRound = [...remainingAttributes];
830
932
  remainingAttributes = []; // Reset for next iteration
831
- MessageFormatter.info(chalk.blue(`\n=== Attempt ${overallRetryCount + 1}/${maxOverallRetries} - Processing ${attributesToProcessThisRound.length} attributes ===`));
832
933
  for (const attribute of attributesToProcessThisRound) {
833
- MessageFormatter.info(chalk.blue(`\n--- Processing attribute: ${attribute.key} ---`));
834
934
  const success = await createOrUpdateAttributeWithStatusCheck(db, dbId, currentCollection, attribute);
835
935
  if (success) {
836
- MessageFormatter.info(chalk.green(`✅ Successfully created attribute: ${attribute.key}`));
837
936
  // Mark this specific attribute as processed
838
937
  markAttributeProcessed(currentCollection.$id, attribute.key);
839
938
  // Get updated collection data for next iteration
@@ -849,27 +948,25 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId,
849
948
  await delay(1000);
850
949
  }
851
950
  else {
852
- MessageFormatter.info(chalk.red(`❌ Failed to create attribute: ${attribute.key}, will retry in next round`));
951
+ MessageFormatter.info(chalk.red(`❌ ${attribute.key}`));
853
952
  remainingAttributes.push(attribute); // Add back to retry list
854
953
  }
855
954
  }
856
955
  if (remainingAttributes.length === 0) {
857
- MessageFormatter.info(chalk.green(`\n✅ Successfully created all ${attributesToProcess.length} attributes for collection: ${collection.name}`));
858
956
  return true;
859
957
  }
860
958
  overallRetryCount++;
861
959
  if (overallRetryCount < maxOverallRetries) {
862
- MessageFormatter.info(chalk.yellow(`\n⏳ Waiting 5 seconds before retrying ${attributesToProcess.length} failed attributes...`));
960
+ MessageFormatter.info(chalk.yellow(`⏳ Retrying ${remainingAttributes.length} failed attributes...`));
863
961
  await delay(5000);
864
962
  // Refresh collection data before retry
865
963
  try {
866
964
  currentCollection = isDatabaseAdapter(db)
867
965
  ? (await db.getTable({ databaseId: dbId, tableId: collection.$id })).data
868
966
  : await db.getCollection(dbId, collection.$id);
869
- MessageFormatter.info(`Refreshed collection data for retry`);
870
967
  }
871
968
  catch (error) {
872
- MessageFormatter.info(chalk.yellow(`Warning: Could not refresh collection data for retry: ${error}`));
969
+ // Silently continue if refresh fails
873
970
  }
874
971
  }
875
972
  }
@@ -13,12 +13,8 @@ retryCount = 0, maxRetries = 5) => {
13
13
  // Calculate exponential backoff: 2s, 4s, 8s, 16s, 30s (capped at 30s)
14
14
  if (retryCount > 0) {
15
15
  const exponentialDelay = calculateExponentialBackoff(retryCount);
16
- MessageFormatter.info(`Waiting for index '${indexKey}' to become available (retry ${retryCount}, backoff: ${exponentialDelay}ms)...`);
17
16
  await delay(exponentialDelay);
18
17
  }
19
- else {
20
- MessageFormatter.info(`Waiting for index '${indexKey}' to become available...`);
21
- }
22
18
  while (Date.now() - startTime < maxWaitTime) {
23
19
  try {
24
20
  const indexList = await (isLegacyDatabases(db)
@@ -32,15 +28,8 @@ retryCount = 0, maxRetries = 5) => {
32
28
  MessageFormatter.error(`Index '${indexKey}' not found in database '${dbId}' collection '${collectionId}'`);
33
29
  return false;
34
30
  }
35
- if (isLegacyDatabases(db)) {
36
- MessageFormatter.debug(`Index '${indexKey}' status: ${index.status}`);
37
- }
38
- else {
39
- MessageFormatter.debug(`Index '${indexKey}' detected (TablesDB)`);
40
- }
41
31
  switch (index.status) {
42
32
  case 'available':
43
- MessageFormatter.success(`Index '${indexKey}' is now available (type: ${index.type}, attributes: [${index.attributes.join(', ')}])`);
44
33
  return true;
45
34
  case 'failed':
46
35
  MessageFormatter.error(`Index '${indexKey}' failed: ${index.error} (type: ${index.type}, attributes: [${index.attributes.join(', ')}])`);
@@ -139,27 +128,24 @@ export const createOrUpdateIndexesWithStatusCheck = async (dbId, db, collectionI
139
128
  while (indexesToProcess.length > 0 && overallRetryCount < maxOverallRetries) {
140
129
  const remainingIndexes = [...indexesToProcess];
141
130
  indexesToProcess = []; // Reset for next iteration
142
- MessageFormatter.info(`\n=== Attempt ${overallRetryCount + 1}/${maxOverallRetries} - Processing ${remainingIndexes.length} indexes ===`);
143
131
  for (const index of remainingIndexes) {
144
- MessageFormatter.info(`\n--- Processing index: ${index.key} (type: ${index.type}, attributes: [${index.attributes.join(', ')}]) ---`);
145
132
  const success = await createOrUpdateIndexWithStatusCheck(dbId, db, collectionId, collection, index);
146
133
  if (success) {
147
- MessageFormatter.success(`Successfully created index: ${index.key} (type: ${index.type})`);
134
+ MessageFormatter.info(`✅ ${index.key} (${index.type})`);
148
135
  // Add delay between successful indexes
149
136
  await delay(1000);
150
137
  }
151
138
  else {
152
- MessageFormatter.error(`Failed to create index: ${index.key} (type: ${index.type}), will retry in next round`);
139
+ MessageFormatter.info(`❌ ${index.key} (${index.type})`);
153
140
  indexesToProcess.push(index); // Add back to retry list
154
141
  }
155
142
  }
156
143
  if (indexesToProcess.length === 0) {
157
- MessageFormatter.success(`\nSuccessfully created all ${indexes.length} indexes for collection '${collectionId}'`);
158
144
  return true;
159
145
  }
160
146
  overallRetryCount++;
161
147
  if (overallRetryCount < maxOverallRetries) {
162
- MessageFormatter.warning(`\nWaiting 5 seconds before retrying ${indexesToProcess.length} failed indexes...`);
148
+ MessageFormatter.warning(`⏳ Retrying ${indexesToProcess.length} failed indexes...`);
163
149
  await delay(5000);
164
150
  }
165
151
  }
@@ -387,6 +387,44 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
387
387
  relQueue.push({ tableId, attr: attr });
388
388
  }
389
389
  }
390
+ // Wait for all attributes to become available before creating indexes
391
+ const allAttrKeys = [
392
+ ...nonRel.map((a) => a.key),
393
+ ...rels.filter((a) => a.relatedCollection).map((a) => a.key)
394
+ ];
395
+ if (allAttrKeys.length > 0) {
396
+ for (const attrKey of allAttrKeys) {
397
+ const maxWait = 60000; // 60 seconds
398
+ const startTime = Date.now();
399
+ let lastStatus = '';
400
+ while (Date.now() - startTime < maxWait) {
401
+ try {
402
+ const tableData = await adapter.getTable({ databaseId, tableId });
403
+ const attrs = tableData.attributes || [];
404
+ const attr = attrs.find((a) => a.key === attrKey);
405
+ if (attr) {
406
+ if (attr.status === 'available') {
407
+ break; // Attribute is ready
408
+ }
409
+ if (attr.status === 'failed' || attr.status === 'stuck') {
410
+ throw new Error(`Attribute ${attrKey} failed to create: ${attr.error || 'unknown error'}`);
411
+ }
412
+ // Still processing, continue waiting
413
+ lastStatus = attr.status;
414
+ }
415
+ await delay(2000); // Check every 2 seconds
416
+ }
417
+ catch (e) {
418
+ // If we can't check status, assume it's processing and continue
419
+ await delay(2000);
420
+ }
421
+ }
422
+ // Timeout check
423
+ if (Date.now() - startTime >= maxWait) {
424
+ MessageFormatter.warning(`Attribute ${attrKey} did not become available within ${maxWait / 1000}s (last status: ${lastStatus}). Proceeding anyway.`, { prefix: 'Attributes' });
425
+ }
426
+ }
427
+ }
390
428
  // Indexes
391
429
  const idxs = (indexes || []);
392
430
  for (const idx of idxs) {