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.
- package/CONFIG_TODO.md +1189 -0
- package/SERVICE_IMPLEMENTATION_REPORT.md +462 -0
- package/dist/cli/commands/configCommands.js +7 -1
- package/dist/cli/commands/databaseCommands.js +23 -15
- package/dist/collections/attributes.d.ts +1 -1
- package/dist/collections/attributes.js +163 -66
- package/dist/collections/indexes.js +3 -17
- package/dist/collections/methods.js +38 -0
- package/dist/config/ConfigManager.d.ts +445 -0
- package/dist/config/ConfigManager.js +625 -0
- package/dist/config/index.d.ts +8 -0
- package/dist/config/index.js +7 -0
- package/dist/config/services/ConfigDiscoveryService.d.ts +126 -0
- package/dist/config/services/ConfigDiscoveryService.js +374 -0
- package/dist/config/services/ConfigLoaderService.d.ts +105 -0
- package/dist/config/services/ConfigLoaderService.js +410 -0
- package/dist/config/services/ConfigMergeService.d.ts +208 -0
- package/dist/config/services/ConfigMergeService.js +307 -0
- package/dist/config/services/ConfigValidationService.d.ts +214 -0
- package/dist/config/services/ConfigValidationService.js +310 -0
- package/dist/config/services/SessionAuthService.d.ts +225 -0
- package/dist/config/services/SessionAuthService.js +456 -0
- package/dist/config/services/__tests__/ConfigMergeService.test.d.ts +1 -0
- package/dist/config/services/__tests__/ConfigMergeService.test.js +271 -0
- package/dist/config/services/index.d.ts +13 -0
- package/dist/config/services/index.js +10 -0
- package/dist/interactiveCLI.js +8 -6
- package/dist/main.js +2 -2
- package/dist/migrations/yaml/YamlImportConfigLoader.d.ts +1 -1
- package/dist/shared/operationQueue.js +1 -1
- package/dist/utils/ClientFactory.d.ts +87 -0
- package/dist/utils/ClientFactory.js +164 -0
- package/dist/utils/getClientFromConfig.js +4 -3
- package/dist/utils/helperFunctions.d.ts +1 -0
- package/dist/utils/helperFunctions.js +21 -5
- package/dist/utils/yamlConverter.d.ts +2 -0
- package/dist/utils/yamlConverter.js +21 -4
- package/dist/utilsController.d.ts +18 -15
- package/dist/utilsController.js +83 -131
- package/package.json +1 -1
- package/src/cli/commands/configCommands.ts +8 -1
- package/src/cli/commands/databaseCommands.ts +34 -20
- package/src/collections/attributes.ts +195 -150
- package/src/collections/indexes.ts +4 -19
- package/src/collections/methods.ts +46 -0
- package/src/config/ConfigManager.ts +808 -0
- package/src/config/index.ts +10 -0
- package/src/config/services/ConfigDiscoveryService.ts +463 -0
- package/src/config/services/ConfigLoaderService.ts +560 -0
- package/src/config/services/ConfigMergeService.ts +386 -0
- package/src/config/services/ConfigValidationService.ts +394 -0
- package/src/config/services/SessionAuthService.ts +565 -0
- package/src/config/services/__tests__/ConfigMergeService.test.ts +351 -0
- package/src/config/services/index.ts +29 -0
- package/src/interactiveCLI.ts +9 -7
- package/src/main.ts +2 -2
- package/src/shared/operationQueue.ts +1 -1
- package/src/utils/ClientFactory.ts +186 -0
- package/src/utils/getClientFromConfig.ts +4 -3
- package/src/utils/helperFunctions.ts +27 -7
- package/src/utils/yamlConverter.ts +28 -2
- 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
|
-
const MIN_MAX_THRESHOLD =
|
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
|
-
|
125
|
-
|
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
|
-
|
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
|
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
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
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
|
-
|
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
|
-
|
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(`➕
|
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(`🔄
|
916
|
+
MessageFormatter.info(`🔄 ${attribute.key}`);
|
813
917
|
}
|
814
918
|
else {
|
815
|
-
MessageFormatter.info(chalk.gray(`✅
|
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(`❌
|
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(
|
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
|
-
|
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.
|
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.
|
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(
|
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) {
|