appwrite-utils-cli 1.6.1 ā 1.6.3
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/dist/cli/commands/databaseCommands.js +23 -15
- package/dist/collections/attributes.d.ts +1 -1
- package/dist/collections/attributes.js +61 -36
- package/dist/collections/indexes.js +3 -17
- package/dist/collections/methods.js +38 -0
- package/dist/interactiveCLI.js +6 -12
- package/dist/utils/yamlConverter.d.ts +2 -0
- package/dist/utils/yamlConverter.js +21 -4
- package/package.json +1 -1
- package/src/cli/commands/databaseCommands.ts +34 -20
- package/src/collections/attributes.ts +77 -119
- package/src/collections/indexes.ts +4 -19
- package/src/collections/methods.ts +46 -0
- package/src/interactiveCLI.ts +7 -11
- package/src/utils/yamlConverter.ts +28 -2
@@ -14,23 +14,31 @@ export const databaseCommands = {
|
|
14
14
|
MessageFormatter.warning("No databases selected. Skipping database sync.", { prefix: "Database" });
|
15
15
|
return;
|
16
16
|
}
|
17
|
-
const collections = await cli.selectCollectionsAndTables(databases[0], cli.controller.database, chalk.blue("Select local collections/tables to push:"), true, // multiSelect
|
18
|
-
true // prefer local
|
19
|
-
// shouldFilterByDatabase removed - user will be prompted interactively
|
20
|
-
);
|
21
|
-
const { syncFunctions } = await inquirer.prompt([
|
22
|
-
{
|
23
|
-
type: "confirm",
|
24
|
-
name: "syncFunctions",
|
25
|
-
message: "Do you want to push local functions to remote?",
|
26
|
-
default: false,
|
27
|
-
},
|
28
|
-
]);
|
29
17
|
try {
|
30
|
-
//
|
31
|
-
|
32
|
-
|
18
|
+
// Loop through each database and prompt for collections specific to that database
|
19
|
+
for (const database of databases) {
|
20
|
+
MessageFormatter.info(`\nš¦ Configuring push for database: ${database.name}`, { prefix: "Database" });
|
21
|
+
const collections = await cli.selectCollectionsAndTables(database, cli.controller.database, chalk.blue(`Select collections/tables to push to "${database.name}":`), true, // multiSelect
|
22
|
+
true // prefer local
|
23
|
+
);
|
24
|
+
if (collections.length === 0) {
|
25
|
+
MessageFormatter.warning(`No collections selected for database "${database.name}". Skipping.`, { prefix: "Database" });
|
26
|
+
continue;
|
27
|
+
}
|
28
|
+
// Push selected collections to this specific database
|
29
|
+
await cli.controller.syncDb([database], collections);
|
30
|
+
MessageFormatter.success(`Pushed ${collections.length} collection(s) to database "${database.name}"`, { prefix: "Database" });
|
31
|
+
}
|
32
|
+
MessageFormatter.success("\nā
All database configurations pushed successfully!", { prefix: "Database" });
|
33
33
|
// Then handle functions if requested
|
34
|
+
const { syncFunctions } = await inquirer.prompt([
|
35
|
+
{
|
36
|
+
type: "confirm",
|
37
|
+
name: "syncFunctions",
|
38
|
+
message: "Do you want to push local functions to remote?",
|
39
|
+
default: false,
|
40
|
+
},
|
41
|
+
]);
|
34
42
|
if (syncFunctions && cli.controller.config?.functions?.length) {
|
35
43
|
const functions = await cli.selectFunctions(chalk.blue("Select local functions to push:"), true, true // prefer local
|
36
44
|
);
|
@@ -5,7 +5,7 @@ import type { DatabaseAdapter } from "../adapters/DatabaseAdapter.js";
|
|
5
5
|
* Enhanced attribute creation with proper status monitoring and retry logic
|
6
6
|
*/
|
7
7
|
export declare const createOrUpdateAttributeWithStatusCheck: (db: Databases | DatabaseAdapter, dbId: string, collection: Models.Collection, attribute: Attribute, retryCount?: number, maxRetries?: number) => Promise<boolean>;
|
8
|
-
export declare const createOrUpdateAttribute: (db: Databases | DatabaseAdapter, dbId: string, collection: Models.Collection, attribute: Attribute) => Promise<"queued" | "processed">;
|
8
|
+
export declare const createOrUpdateAttribute: (db: Databases | DatabaseAdapter, dbId: string, collection: Models.Collection, attribute: Attribute) => Promise<"queued" | "processed" | "error">;
|
9
9
|
/**
|
10
10
|
* Enhanced collection attribute creation with proper status monitoring
|
11
11
|
*/
|
@@ -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;
|
@@ -296,32 +296,32 @@ const updateLegacyAttribute = async (db, dbId, collectionId, attribute) => {
|
|
296
296
|
const { min: normalizedMin, max: normalizedMax } = normalizeMinMaxValues(attribute);
|
297
297
|
switch (attribute.type) {
|
298
298
|
case "string":
|
299
|
-
await db.updateStringAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined
|
299
|
+
await db.updateStringAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined ? attribute.xdefault : null, attribute.size);
|
300
300
|
break;
|
301
301
|
case "integer":
|
302
|
-
await db.updateIntegerAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined
|
302
|
+
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
303
|
break;
|
304
304
|
case "double":
|
305
305
|
case "float":
|
306
|
-
await db.updateFloatAttribute(dbId, collectionId, attribute.key, attribute.required || false, normalizedMin !== undefined ? Number(normalizedMin) : undefined, normalizedMax !== undefined ? Number(normalizedMax) : undefined, attribute.
|
306
|
+
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
307
|
break;
|
308
308
|
case "boolean":
|
309
|
-
await db.updateBooleanAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.
|
309
|
+
await db.updateBooleanAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined ? attribute.xdefault : null);
|
310
310
|
break;
|
311
311
|
case "datetime":
|
312
|
-
await db.updateDatetimeAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.
|
312
|
+
await db.updateDatetimeAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined ? attribute.xdefault : null);
|
313
313
|
break;
|
314
314
|
case "email":
|
315
|
-
await db.updateEmailAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.
|
315
|
+
await db.updateEmailAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined ? attribute.xdefault : null);
|
316
316
|
break;
|
317
317
|
case "ip":
|
318
|
-
await db.updateIpAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.
|
318
|
+
await db.updateIpAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined ? attribute.xdefault : null);
|
319
319
|
break;
|
320
320
|
case "url":
|
321
|
-
await db.updateUrlAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.
|
321
|
+
await db.updateUrlAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined ? attribute.xdefault : null);
|
322
322
|
break;
|
323
323
|
case "enum":
|
324
|
-
await db.updateEnumAttribute(dbId, collectionId, attribute.key, attribute.elements || [], attribute.required || false, attribute.
|
324
|
+
await db.updateEnumAttribute(dbId, collectionId, attribute.key, attribute.elements || [], attribute.required || false, !attribute.required && attribute.xdefault !== undefined ? attribute.xdefault : null);
|
325
325
|
break;
|
326
326
|
case "relationship":
|
327
327
|
await db.updateRelationshipAttribute(dbId, collectionId, attribute.key, attribute.onDelete);
|
@@ -348,12 +348,8 @@ retryCount = 0, maxRetries = 5) => {
|
|
348
348
|
// Calculate exponential backoff: 2s, 4s, 8s, 16s, 30s (capped at 30s)
|
349
349
|
if (retryCount > 0) {
|
350
350
|
const exponentialDelay = calculateExponentialBackoff(retryCount);
|
351
|
-
MessageFormatter.info(chalk.blue(`Waiting for attribute '${attributeKey}' to become available (retry ${retryCount}, backoff: ${exponentialDelay}ms)...`));
|
352
351
|
await delay(exponentialDelay);
|
353
352
|
}
|
354
|
-
else {
|
355
|
-
MessageFormatter.info(chalk.blue(`Waiting for attribute '${attributeKey}' to become available...`));
|
356
|
-
}
|
357
353
|
while (Date.now() - startTime < maxWaitTime) {
|
358
354
|
try {
|
359
355
|
const collection = isDatabaseAdapter(db)
|
@@ -364,7 +360,6 @@ retryCount = 0, maxRetries = 5) => {
|
|
364
360
|
MessageFormatter.error(`Attribute '${attributeKey}' not found`);
|
365
361
|
return false;
|
366
362
|
}
|
367
|
-
MessageFormatter.info(chalk.gray(`Attribute '${attributeKey}' status: ${attribute.status}`));
|
368
363
|
const statusInfo = {
|
369
364
|
attributeKey,
|
370
365
|
status: attribute.status,
|
@@ -376,15 +371,12 @@ retryCount = 0, maxRetries = 5) => {
|
|
376
371
|
};
|
377
372
|
switch (attribute.status) {
|
378
373
|
case "available":
|
379
|
-
MessageFormatter.info(chalk.green(`ā
Attribute '${attributeKey}' is now available`));
|
380
374
|
logger.info(`Attribute '${attributeKey}' became available`, statusInfo);
|
381
375
|
return true;
|
382
376
|
case "failed":
|
383
|
-
MessageFormatter.info(chalk.red(`ā Attribute '${attributeKey}' failed: ${attribute.error}`));
|
384
377
|
logger.error(`Attribute '${attributeKey}' failed`, statusInfo);
|
385
378
|
return false;
|
386
379
|
case "stuck":
|
387
|
-
MessageFormatter.info(chalk.yellow(`ā ļø Attribute '${attributeKey}' is stuck, will retry...`));
|
388
380
|
logger.warn(`Attribute '${attributeKey}' is stuck`, statusInfo);
|
389
381
|
return false;
|
390
382
|
case "processing":
|
@@ -549,7 +541,6 @@ const attributesSame = (databaseAttribute, configAttribute) => {
|
|
549
541
|
* Enhanced attribute creation with proper status monitoring and retry logic
|
550
542
|
*/
|
551
543
|
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
544
|
try {
|
554
545
|
// First, try to create/update the attribute using existing logic
|
555
546
|
const result = await createOrUpdateAttribute(db, dbId, collection, attribute);
|
@@ -559,6 +550,11 @@ export const createOrUpdateAttributeWithStatusCheck = async (db, dbId, collectio
|
|
559
550
|
MessageFormatter.info(chalk.yellow(`āļø Deferred relationship attribute '${attribute.key}' ā queued for later once dependencies are available`));
|
560
551
|
return true;
|
561
552
|
}
|
553
|
+
// If collection creation failed, return false to indicate failure
|
554
|
+
if (result === "error") {
|
555
|
+
MessageFormatter.error(`Failed to create collection for attribute '${attribute.key}'`);
|
556
|
+
return false;
|
557
|
+
}
|
562
558
|
// Now wait for the attribute to become available
|
563
559
|
const success = await waitForAttributeAvailable(db, dbId, collection.$id, attribute.key, 60000, // 1 minute timeout
|
564
560
|
retryCount, maxRetries);
|
@@ -724,6 +720,45 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
724
720
|
}
|
725
721
|
}
|
726
722
|
finalAttribute = parseAttribute(finalAttribute);
|
723
|
+
// Ensure collection/table exists - create it if it doesn't
|
724
|
+
try {
|
725
|
+
await (isDatabaseAdapter(db)
|
726
|
+
? db.getTable({ databaseId: dbId, tableId: collection.$id })
|
727
|
+
: db.getCollection(dbId, collection.$id));
|
728
|
+
}
|
729
|
+
catch (error) {
|
730
|
+
// Collection doesn't exist - create it
|
731
|
+
if (error.code === 404 ||
|
732
|
+
(error instanceof Error && (error.message.includes('collection_not_found') ||
|
733
|
+
error.message.includes('Collection with the requested ID could not be found')))) {
|
734
|
+
MessageFormatter.info(`Collection '${collection.name}' doesn't exist, creating it first...`);
|
735
|
+
try {
|
736
|
+
if (isDatabaseAdapter(db)) {
|
737
|
+
await db.createTable({
|
738
|
+
databaseId: dbId,
|
739
|
+
id: collection.$id,
|
740
|
+
name: collection.name,
|
741
|
+
permissions: collection.$permissions || [],
|
742
|
+
documentSecurity: collection.documentSecurity ?? false,
|
743
|
+
enabled: collection.enabled ?? true
|
744
|
+
});
|
745
|
+
}
|
746
|
+
else {
|
747
|
+
await db.createCollection(dbId, collection.$id, collection.name, collection.$permissions || [], collection.documentSecurity ?? false, collection.enabled ?? true);
|
748
|
+
}
|
749
|
+
MessageFormatter.success(`Created collection '${collection.name}'`);
|
750
|
+
await delay(500); // Wait for collection to be ready
|
751
|
+
}
|
752
|
+
catch (createError) {
|
753
|
+
MessageFormatter.error(`Failed to create collection '${collection.name}'`, createError instanceof Error ? createError : new Error(String(createError)));
|
754
|
+
return "error";
|
755
|
+
}
|
756
|
+
}
|
757
|
+
else {
|
758
|
+
// Other error - re-throw
|
759
|
+
throw error;
|
760
|
+
}
|
761
|
+
}
|
727
762
|
// Use adapter-based attribute creation/update
|
728
763
|
if (action === "create") {
|
729
764
|
await tryAwaitWithRetry(async () => await createAttributeViaAdapter(db, dbId, collection.$id, finalAttribute));
|
@@ -737,7 +772,6 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
737
772
|
* Enhanced collection attribute creation with proper status monitoring
|
738
773
|
*/
|
739
774
|
export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId, collection, attributes) => {
|
740
|
-
MessageFormatter.info(chalk.green(`Creating/Updating attributes for collection: ${collection.name} with status monitoring`));
|
741
775
|
const existingAttributes =
|
742
776
|
// @ts-expect-error
|
743
777
|
collection.attributes.map((attr) => parseAttribute(attr)) || [];
|
@@ -775,7 +809,6 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId,
|
|
775
809
|
}
|
776
810
|
}
|
777
811
|
// 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
812
|
let currentCollection = collection;
|
780
813
|
try {
|
781
814
|
currentCollection = isDatabaseAdapter(db)
|
@@ -799,28 +832,25 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId,
|
|
799
832
|
const attributesToProcess = attributes.filter((attribute) => {
|
800
833
|
// Skip if already processed in this session
|
801
834
|
if (isAttributeProcessed(currentCollection.$id, attribute.key)) {
|
802
|
-
MessageFormatter.info(chalk.gray(`āļø Attribute '${attribute.key}' already processed in this session (skipping)`));
|
803
835
|
return false;
|
804
836
|
}
|
805
837
|
const existing = existingAttributesMap.get(attribute.key);
|
806
838
|
if (!existing) {
|
807
|
-
MessageFormatter.info(`ā
|
839
|
+
MessageFormatter.info(`ā ${attribute.key}`);
|
808
840
|
return true;
|
809
841
|
}
|
810
842
|
const needsUpdate = !attributesSame(existing, attribute);
|
811
843
|
if (needsUpdate) {
|
812
|
-
MessageFormatter.info(`š
|
844
|
+
MessageFormatter.info(`š ${attribute.key}`);
|
813
845
|
}
|
814
846
|
else {
|
815
|
-
MessageFormatter.info(chalk.gray(`ā
|
847
|
+
MessageFormatter.info(chalk.gray(`ā
${attribute.key}`));
|
816
848
|
}
|
817
849
|
return needsUpdate;
|
818
850
|
});
|
819
851
|
if (attributesToProcess.length === 0) {
|
820
|
-
MessageFormatter.info(chalk.green(`ā
All ${attributes.length} attributes are already up to date for collection: ${collection.name}`));
|
821
852
|
return true;
|
822
853
|
}
|
823
|
-
MessageFormatter.info(chalk.blue(`Creating ${attributesToProcess.length} attributes sequentially with status monitoring...`));
|
824
854
|
let remainingAttributes = [...attributesToProcess];
|
825
855
|
let overallRetryCount = 0;
|
826
856
|
const maxOverallRetries = 3;
|
@@ -828,12 +858,9 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId,
|
|
828
858
|
overallRetryCount < maxOverallRetries) {
|
829
859
|
const attributesToProcessThisRound = [...remainingAttributes];
|
830
860
|
remainingAttributes = []; // Reset for next iteration
|
831
|
-
MessageFormatter.info(chalk.blue(`\n=== Attempt ${overallRetryCount + 1}/${maxOverallRetries} - Processing ${attributesToProcessThisRound.length} attributes ===`));
|
832
861
|
for (const attribute of attributesToProcessThisRound) {
|
833
|
-
MessageFormatter.info(chalk.blue(`\n--- Processing attribute: ${attribute.key} ---`));
|
834
862
|
const success = await createOrUpdateAttributeWithStatusCheck(db, dbId, currentCollection, attribute);
|
835
863
|
if (success) {
|
836
|
-
MessageFormatter.info(chalk.green(`ā
Successfully created attribute: ${attribute.key}`));
|
837
864
|
// Mark this specific attribute as processed
|
838
865
|
markAttributeProcessed(currentCollection.$id, attribute.key);
|
839
866
|
// Get updated collection data for next iteration
|
@@ -849,27 +876,25 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId,
|
|
849
876
|
await delay(1000);
|
850
877
|
}
|
851
878
|
else {
|
852
|
-
MessageFormatter.info(chalk.red(`ā
|
879
|
+
MessageFormatter.info(chalk.red(`ā ${attribute.key}`));
|
853
880
|
remainingAttributes.push(attribute); // Add back to retry list
|
854
881
|
}
|
855
882
|
}
|
856
883
|
if (remainingAttributes.length === 0) {
|
857
|
-
MessageFormatter.info(chalk.green(`\nā
Successfully created all ${attributesToProcess.length} attributes for collection: ${collection.name}`));
|
858
884
|
return true;
|
859
885
|
}
|
860
886
|
overallRetryCount++;
|
861
887
|
if (overallRetryCount < maxOverallRetries) {
|
862
|
-
MessageFormatter.info(chalk.yellow(
|
888
|
+
MessageFormatter.info(chalk.yellow(`ā³ Retrying ${remainingAttributes.length} failed attributes...`));
|
863
889
|
await delay(5000);
|
864
890
|
// Refresh collection data before retry
|
865
891
|
try {
|
866
892
|
currentCollection = isDatabaseAdapter(db)
|
867
893
|
? (await db.getTable({ databaseId: dbId, tableId: collection.$id })).data
|
868
894
|
: await db.getCollection(dbId, collection.$id);
|
869
|
-
MessageFormatter.info(`Refreshed collection data for retry`);
|
870
895
|
}
|
871
896
|
catch (error) {
|
872
|
-
|
897
|
+
// Silently continue if refresh fails
|
873
898
|
}
|
874
899
|
}
|
875
900
|
}
|
@@ -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) {
|
package/dist/interactiveCLI.js
CHANGED
@@ -253,19 +253,13 @@ export class InteractiveCLI {
|
|
253
253
|
...configCollections.filter((c) => !remoteCollections.some((rc) => rc.name === c.name)),
|
254
254
|
];
|
255
255
|
if (shouldFilterByDatabase) {
|
256
|
-
//
|
256
|
+
// Show collections that EITHER exist in the remote database OR have matching local databaseId metadata
|
257
257
|
allCollections = allCollections.filter((c) => {
|
258
|
-
//
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
// - Collections without databaseId are kept (backward compatibility)
|
264
|
-
// - Tables with databaseId must match the selected database
|
265
|
-
// - Tables without databaseId are kept (fallback for misconfigured tables)
|
266
|
-
if (!c.databaseId)
|
267
|
-
return true;
|
268
|
-
return c.databaseId === database.$id;
|
258
|
+
// Include if it exists remotely in this database
|
259
|
+
const existsInRemoteDb = remoteCollections.some((rc) => rc.name === c.name);
|
260
|
+
// Include if local metadata claims it belongs to this database
|
261
|
+
const hasMatchingLocalMetadata = c.databaseId === database.$id;
|
262
|
+
return existsInRemoteDb || hasMatchingLocalMetadata;
|
269
263
|
});
|
270
264
|
}
|
271
265
|
// Filter out system tables (those starting with underscore)
|
@@ -14,6 +14,7 @@ export interface YamlCollectionData {
|
|
14
14
|
size?: number;
|
15
15
|
required?: boolean;
|
16
16
|
array?: boolean;
|
17
|
+
encrypt?: boolean;
|
17
18
|
default?: any;
|
18
19
|
min?: number;
|
19
20
|
max?: number;
|
@@ -31,6 +32,7 @@ export interface YamlCollectionData {
|
|
31
32
|
size?: number;
|
32
33
|
required?: boolean;
|
33
34
|
array?: boolean;
|
35
|
+
encrypt?: boolean;
|
34
36
|
default?: any;
|
35
37
|
min?: number;
|
36
38
|
max?: number;
|
@@ -1,4 +1,6 @@
|
|
1
1
|
import yaml from "js-yaml";
|
2
|
+
// Threshold for treating min/max values as undefined (1 trillion)
|
3
|
+
const MIN_MAX_THRESHOLD = 1_000_000_000_000;
|
2
4
|
/**
|
3
5
|
* Converts a Collection object to YAML format with proper schema reference
|
4
6
|
* Supports both collection and table terminology based on configuration
|
@@ -37,12 +39,27 @@ export function collectionToYaml(collection, config = {
|
|
37
39
|
yamlAttr.required = attr.required;
|
38
40
|
if (attr.array !== undefined)
|
39
41
|
yamlAttr.array = attr.array;
|
42
|
+
// Always include encrypt field for string attributes (default to false)
|
43
|
+
if (attr.type === 'string') {
|
44
|
+
yamlAttr.encrypt = ('encrypted' in attr && attr.encrypted === true) ? true : false;
|
45
|
+
}
|
40
46
|
if ('xdefault' in attr && attr.xdefault !== undefined)
|
41
47
|
yamlAttr.default = attr.xdefault;
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
48
|
+
// Normalize min/max values - filter out extreme database values
|
49
|
+
if ('min' in attr && attr.min !== undefined) {
|
50
|
+
const minValue = Number(attr.min);
|
51
|
+
// Only include min if it's within reasonable range (< 1 trillion)
|
52
|
+
if (Math.abs(minValue) < MIN_MAX_THRESHOLD) {
|
53
|
+
yamlAttr.min = attr.min;
|
54
|
+
}
|
55
|
+
}
|
56
|
+
if ('max' in attr && attr.max !== undefined) {
|
57
|
+
const maxValue = Number(attr.max);
|
58
|
+
// Only include max if it's within reasonable range (< 1 trillion)
|
59
|
+
if (Math.abs(maxValue) < MIN_MAX_THRESHOLD) {
|
60
|
+
yamlAttr.max = attr.max;
|
61
|
+
}
|
62
|
+
}
|
46
63
|
if ('elements' in attr && attr.elements !== undefined)
|
47
64
|
yamlAttr.elements = attr.elements;
|
48
65
|
if ('relatedCollection' in attr && attr.relatedCollection !== undefined)
|
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.6.
|
4
|
+
"version": "1.6.3",
|
5
5
|
"main": "src/main.ts",
|
6
6
|
"type": "module",
|
7
7
|
"repository": {
|
@@ -23,30 +23,44 @@ export const databaseCommands = {
|
|
23
23
|
return;
|
24
24
|
}
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
(
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
26
|
+
try {
|
27
|
+
// Loop through each database and prompt for collections specific to that database
|
28
|
+
for (const database of databases) {
|
29
|
+
MessageFormatter.info(`\nš¦ Configuring push for database: ${database.name}`, { prefix: "Database" });
|
30
|
+
|
31
|
+
const collections = await (cli as any).selectCollectionsAndTables(
|
32
|
+
database,
|
33
|
+
(cli as any).controller!.database!,
|
34
|
+
chalk.blue(`Select collections/tables to push to "${database.name}":`),
|
35
|
+
true, // multiSelect
|
36
|
+
true // prefer local
|
37
|
+
);
|
34
38
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
message: "Do you want to push local functions to remote?",
|
40
|
-
default: false,
|
41
|
-
},
|
42
|
-
]);
|
39
|
+
if (collections.length === 0) {
|
40
|
+
MessageFormatter.warning(`No collections selected for database "${database.name}". Skipping.`, { prefix: "Database" });
|
41
|
+
continue;
|
42
|
+
}
|
43
43
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
44
|
+
// Push selected collections to this specific database
|
45
|
+
await (cli as any).controller!.syncDb([database], collections);
|
46
|
+
MessageFormatter.success(
|
47
|
+
`Pushed ${collections.length} collection(s) to database "${database.name}"`,
|
48
|
+
{ prefix: "Database" }
|
49
|
+
);
|
50
|
+
}
|
51
|
+
|
52
|
+
MessageFormatter.success("\nā
All database configurations pushed successfully!", { prefix: "Database" });
|
48
53
|
|
49
54
|
// Then handle functions if requested
|
55
|
+
const { syncFunctions } = await inquirer.prompt([
|
56
|
+
{
|
57
|
+
type: "confirm",
|
58
|
+
name: "syncFunctions",
|
59
|
+
message: "Do you want to push local functions to remote?",
|
60
|
+
default: false,
|
61
|
+
},
|
62
|
+
]);
|
63
|
+
|
50
64
|
if (syncFunctions && (cli as any).controller!.config?.functions?.length) {
|
51
65
|
const functions = await (cli as any).selectFunctions(
|
52
66
|
chalk.blue("Select local functions to push:"),
|
@@ -17,8 +17,8 @@ import { logger } from "../shared/logging.js";
|
|
17
17
|
import { MessageFormatter } from "../shared/messageFormatter.js";
|
18
18
|
import { isDatabaseAdapter } from "../utils/typeGuards.js";
|
19
19
|
|
20
|
-
// Threshold for treating min/max values as undefined (
|
21
|
-
const MIN_MAX_THRESHOLD =
|
20
|
+
// Threshold for treating min/max values as undefined (1 trillion)
|
21
|
+
const MIN_MAX_THRESHOLD = 1_000_000_000_000;
|
22
22
|
|
23
23
|
// Extreme values that Appwrite may return, which should be treated as undefined
|
24
24
|
const EXTREME_MIN_INTEGER = -9223372036854776000;
|
@@ -434,7 +434,7 @@ const updateLegacyAttribute = async (
|
|
434
434
|
collectionId,
|
435
435
|
attribute.key,
|
436
436
|
attribute.required || false,
|
437
|
-
(attribute as any).xdefault !== undefined
|
437
|
+
!attribute.required && (attribute as any).xdefault !== undefined ? (attribute as any).xdefault : null,
|
438
438
|
attribute.size
|
439
439
|
);
|
440
440
|
break;
|
@@ -444,7 +444,7 @@ const updateLegacyAttribute = async (
|
|
444
444
|
collectionId,
|
445
445
|
attribute.key,
|
446
446
|
attribute.required || false,
|
447
|
-
(attribute as any).xdefault !== undefined
|
447
|
+
!attribute.required && (attribute as any).xdefault !== undefined ? (attribute as any).xdefault : null,
|
448
448
|
normalizedMin !== undefined ? parseInt(String(normalizedMin)) : undefined,
|
449
449
|
normalizedMax !== undefined ? parseInt(String(normalizedMax)) : undefined
|
450
450
|
);
|
@@ -458,7 +458,7 @@ const updateLegacyAttribute = async (
|
|
458
458
|
attribute.required || false,
|
459
459
|
normalizedMin !== undefined ? Number(normalizedMin) : undefined,
|
460
460
|
normalizedMax !== undefined ? Number(normalizedMax) : undefined,
|
461
|
-
attribute.
|
461
|
+
!attribute.required && (attribute as any).xdefault !== undefined ? (attribute as any).xdefault : null
|
462
462
|
);
|
463
463
|
break;
|
464
464
|
case "boolean":
|
@@ -467,7 +467,7 @@ const updateLegacyAttribute = async (
|
|
467
467
|
collectionId,
|
468
468
|
attribute.key,
|
469
469
|
attribute.required || false,
|
470
|
-
attribute.
|
470
|
+
!attribute.required && (attribute as any).xdefault !== undefined ? (attribute as any).xdefault : null
|
471
471
|
);
|
472
472
|
break;
|
473
473
|
case "datetime":
|
@@ -476,7 +476,7 @@ const updateLegacyAttribute = async (
|
|
476
476
|
collectionId,
|
477
477
|
attribute.key,
|
478
478
|
attribute.required || false,
|
479
|
-
attribute.
|
479
|
+
!attribute.required && (attribute as any).xdefault !== undefined ? (attribute as any).xdefault : null
|
480
480
|
);
|
481
481
|
break;
|
482
482
|
case "email":
|
@@ -485,7 +485,7 @@ const updateLegacyAttribute = async (
|
|
485
485
|
collectionId,
|
486
486
|
attribute.key,
|
487
487
|
attribute.required || false,
|
488
|
-
attribute.
|
488
|
+
!attribute.required && (attribute as any).xdefault !== undefined ? (attribute as any).xdefault : null
|
489
489
|
);
|
490
490
|
break;
|
491
491
|
case "ip":
|
@@ -494,7 +494,7 @@ const updateLegacyAttribute = async (
|
|
494
494
|
collectionId,
|
495
495
|
attribute.key,
|
496
496
|
attribute.required || false,
|
497
|
-
attribute.
|
497
|
+
!attribute.required && (attribute as any).xdefault !== undefined ? (attribute as any).xdefault : null
|
498
498
|
);
|
499
499
|
break;
|
500
500
|
case "url":
|
@@ -503,7 +503,7 @@ const updateLegacyAttribute = async (
|
|
503
503
|
collectionId,
|
504
504
|
attribute.key,
|
505
505
|
attribute.required || false,
|
506
|
-
attribute.
|
506
|
+
!attribute.required && (attribute as any).xdefault !== undefined ? (attribute as any).xdefault : null
|
507
507
|
);
|
508
508
|
break;
|
509
509
|
case "enum":
|
@@ -513,7 +513,7 @@ const updateLegacyAttribute = async (
|
|
513
513
|
attribute.key,
|
514
514
|
(attribute as any).elements || [],
|
515
515
|
attribute.required || false,
|
516
|
-
attribute.
|
516
|
+
!attribute.required && (attribute as any).xdefault !== undefined ? (attribute as any).xdefault : null
|
517
517
|
);
|
518
518
|
break;
|
519
519
|
case "relationship":
|
@@ -569,18 +569,7 @@ const waitForAttributeAvailable = async (
|
|
569
569
|
// Calculate exponential backoff: 2s, 4s, 8s, 16s, 30s (capped at 30s)
|
570
570
|
if (retryCount > 0) {
|
571
571
|
const exponentialDelay = calculateExponentialBackoff(retryCount);
|
572
|
-
MessageFormatter.info(
|
573
|
-
chalk.blue(
|
574
|
-
`Waiting for attribute '${attributeKey}' to become available (retry ${retryCount}, backoff: ${exponentialDelay}ms)...`
|
575
|
-
)
|
576
|
-
);
|
577
572
|
await delay(exponentialDelay);
|
578
|
-
} else {
|
579
|
-
MessageFormatter.info(
|
580
|
-
chalk.blue(
|
581
|
-
`Waiting for attribute '${attributeKey}' to become available...`
|
582
|
-
)
|
583
|
-
);
|
584
573
|
}
|
585
574
|
|
586
575
|
while (Date.now() - startTime < maxWaitTime) {
|
@@ -597,10 +586,6 @@ const waitForAttributeAvailable = async (
|
|
597
586
|
return false;
|
598
587
|
}
|
599
588
|
|
600
|
-
MessageFormatter.info(
|
601
|
-
chalk.gray(`Attribute '${attributeKey}' status: ${attribute.status}`)
|
602
|
-
);
|
603
|
-
|
604
589
|
const statusInfo = {
|
605
590
|
attributeKey,
|
606
591
|
status: attribute.status,
|
@@ -613,27 +598,14 @@ const waitForAttributeAvailable = async (
|
|
613
598
|
|
614
599
|
switch (attribute.status) {
|
615
600
|
case "available":
|
616
|
-
MessageFormatter.info(
|
617
|
-
chalk.green(`ā
Attribute '${attributeKey}' is now available`)
|
618
|
-
);
|
619
601
|
logger.info(`Attribute '${attributeKey}' became available`, statusInfo);
|
620
602
|
return true;
|
621
603
|
|
622
604
|
case "failed":
|
623
|
-
MessageFormatter.info(
|
624
|
-
chalk.red(
|
625
|
-
`ā Attribute '${attributeKey}' failed: ${attribute.error}`
|
626
|
-
)
|
627
|
-
);
|
628
605
|
logger.error(`Attribute '${attributeKey}' failed`, statusInfo);
|
629
606
|
return false;
|
630
607
|
|
631
608
|
case "stuck":
|
632
|
-
MessageFormatter.info(
|
633
|
-
chalk.yellow(
|
634
|
-
`ā ļø Attribute '${attributeKey}' is stuck, will retry...`
|
635
|
-
)
|
636
|
-
);
|
637
609
|
logger.warn(`Attribute '${attributeKey}' is stuck`, statusInfo);
|
638
610
|
return false;
|
639
611
|
|
@@ -890,14 +862,6 @@ export const createOrUpdateAttributeWithStatusCheck = async (
|
|
890
862
|
retryCount: number = 0,
|
891
863
|
maxRetries: number = 5
|
892
864
|
): Promise<boolean> => {
|
893
|
-
MessageFormatter.info(
|
894
|
-
chalk.blue(
|
895
|
-
`Creating/updating attribute '${attribute.key}' (attempt ${
|
896
|
-
retryCount + 1
|
897
|
-
}/${maxRetries + 1})`
|
898
|
-
)
|
899
|
-
);
|
900
|
-
|
901
865
|
try {
|
902
866
|
// First, try to create/update the attribute using existing logic
|
903
867
|
const result = await createOrUpdateAttribute(db, dbId, collection, attribute);
|
@@ -913,6 +877,12 @@ export const createOrUpdateAttributeWithStatusCheck = async (
|
|
913
877
|
return true;
|
914
878
|
}
|
915
879
|
|
880
|
+
// If collection creation failed, return false to indicate failure
|
881
|
+
if (result === "error") {
|
882
|
+
MessageFormatter.error(`Failed to create collection for attribute '${attribute.key}'`);
|
883
|
+
return false;
|
884
|
+
}
|
885
|
+
|
916
886
|
// Now wait for the attribute to become available
|
917
887
|
const success = await waitForAttributeAvailable(
|
918
888
|
db,
|
@@ -1059,7 +1029,7 @@ export const createOrUpdateAttribute = async (
|
|
1059
1029
|
dbId: string,
|
1060
1030
|
collection: Models.Collection,
|
1061
1031
|
attribute: Attribute
|
1062
|
-
): Promise<"queued" | "processed"> => {
|
1032
|
+
): Promise<"queued" | "processed" | "error"> => {
|
1063
1033
|
let action = "create";
|
1064
1034
|
let foundAttribute: Attribute | undefined;
|
1065
1035
|
const updateEnabled = true;
|
@@ -1069,7 +1039,7 @@ export const createOrUpdateAttribute = async (
|
|
1069
1039
|
(attr: any) => attr.key === attribute.key
|
1070
1040
|
) as unknown as any;
|
1071
1041
|
foundAttribute = parseAttribute(collectionAttr);
|
1072
|
-
|
1042
|
+
|
1073
1043
|
} catch (error) {
|
1074
1044
|
foundAttribute = undefined;
|
1075
1045
|
}
|
@@ -1110,7 +1080,7 @@ export const createOrUpdateAttribute = async (
|
|
1110
1080
|
return "processed";
|
1111
1081
|
}
|
1112
1082
|
|
1113
|
-
|
1083
|
+
|
1114
1084
|
|
1115
1085
|
// Relationship attribute logic with adjustments
|
1116
1086
|
let collectionFoundViaRelatedCollection: Models.Collection | undefined;
|
@@ -1181,7 +1151,57 @@ export const createOrUpdateAttribute = async (
|
|
1181
1151
|
}
|
1182
1152
|
}
|
1183
1153
|
finalAttribute = parseAttribute(finalAttribute);
|
1184
|
-
|
1154
|
+
|
1155
|
+
// Ensure collection/table exists - create it if it doesn't
|
1156
|
+
try {
|
1157
|
+
await (isDatabaseAdapter(db)
|
1158
|
+
? db.getTable({ databaseId: dbId, tableId: collection.$id })
|
1159
|
+
: db.getCollection(dbId, collection.$id));
|
1160
|
+
} catch (error) {
|
1161
|
+
// Collection doesn't exist - create it
|
1162
|
+
if ((error as any).code === 404 ||
|
1163
|
+
(error instanceof Error && (
|
1164
|
+
error.message.includes('collection_not_found') ||
|
1165
|
+
error.message.includes('Collection with the requested ID could not be found')
|
1166
|
+
))) {
|
1167
|
+
|
1168
|
+
MessageFormatter.info(`Collection '${collection.name}' doesn't exist, creating it first...`);
|
1169
|
+
|
1170
|
+
try {
|
1171
|
+
if (isDatabaseAdapter(db)) {
|
1172
|
+
await db.createTable({
|
1173
|
+
databaseId: dbId,
|
1174
|
+
id: collection.$id,
|
1175
|
+
name: collection.name,
|
1176
|
+
permissions: collection.$permissions || [],
|
1177
|
+
documentSecurity: collection.documentSecurity ?? false,
|
1178
|
+
enabled: collection.enabled ?? true
|
1179
|
+
});
|
1180
|
+
} else {
|
1181
|
+
await db.createCollection(
|
1182
|
+
dbId,
|
1183
|
+
collection.$id,
|
1184
|
+
collection.name,
|
1185
|
+
collection.$permissions || [],
|
1186
|
+
collection.documentSecurity ?? false,
|
1187
|
+
collection.enabled ?? true
|
1188
|
+
);
|
1189
|
+
}
|
1190
|
+
|
1191
|
+
MessageFormatter.success(`Created collection '${collection.name}'`);
|
1192
|
+
await delay(500); // Wait for collection to be ready
|
1193
|
+
} catch (createError) {
|
1194
|
+
MessageFormatter.error(
|
1195
|
+
`Failed to create collection '${collection.name}'`,
|
1196
|
+
createError instanceof Error ? createError : new Error(String(createError))
|
1197
|
+
);
|
1198
|
+
return "error";
|
1199
|
+
}
|
1200
|
+
} else {
|
1201
|
+
// Other error - re-throw
|
1202
|
+
throw error;
|
1203
|
+
}
|
1204
|
+
}
|
1185
1205
|
|
1186
1206
|
// Use adapter-based attribute creation/update
|
1187
1207
|
if (action === "create") {
|
@@ -1205,12 +1225,6 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
|
|
1205
1225
|
collection: Models.Collection,
|
1206
1226
|
attributes: Attribute[]
|
1207
1227
|
): Promise<boolean> => {
|
1208
|
-
MessageFormatter.info(
|
1209
|
-
chalk.green(
|
1210
|
-
`Creating/Updating attributes for collection: ${collection.name} with status monitoring`
|
1211
|
-
)
|
1212
|
-
);
|
1213
|
-
|
1214
1228
|
const existingAttributes: Attribute[] =
|
1215
1229
|
// @ts-expect-error
|
1216
1230
|
collection.attributes.map((attr) => parseAttribute(attr)) || [];
|
@@ -1265,12 +1279,6 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
|
|
1265
1279
|
}
|
1266
1280
|
|
1267
1281
|
// First, get fresh collection data and determine which attributes actually need processing
|
1268
|
-
MessageFormatter.info(
|
1269
|
-
chalk.blue(
|
1270
|
-
`Analyzing ${attributes.length} attributes to determine which need processing...`
|
1271
|
-
)
|
1272
|
-
);
|
1273
|
-
|
1274
1282
|
let currentCollection = collection;
|
1275
1283
|
try {
|
1276
1284
|
currentCollection = isDatabaseAdapter(db)
|
@@ -1301,44 +1309,28 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
|
|
1301
1309
|
const attributesToProcess = attributes.filter((attribute) => {
|
1302
1310
|
// Skip if already processed in this session
|
1303
1311
|
if (isAttributeProcessed(currentCollection.$id, attribute.key)) {
|
1304
|
-
MessageFormatter.info(
|
1305
|
-
chalk.gray(`āļø Attribute '${attribute.key}' already processed in this session (skipping)`)
|
1306
|
-
);
|
1307
1312
|
return false;
|
1308
1313
|
}
|
1309
1314
|
|
1310
1315
|
const existing = existingAttributesMap.get(attribute.key);
|
1311
1316
|
if (!existing) {
|
1312
|
-
MessageFormatter.info(`ā
|
1317
|
+
MessageFormatter.info(`ā ${attribute.key}`);
|
1313
1318
|
return true;
|
1314
1319
|
}
|
1315
1320
|
|
1316
1321
|
const needsUpdate = !attributesSame(existing, attribute);
|
1317
1322
|
if (needsUpdate) {
|
1318
|
-
MessageFormatter.info(`š
|
1323
|
+
MessageFormatter.info(`š ${attribute.key}`);
|
1319
1324
|
} else {
|
1320
|
-
MessageFormatter.info(
|
1321
|
-
chalk.gray(`ā
Unchanged attribute: ${attribute.key} (skipping)`)
|
1322
|
-
);
|
1325
|
+
MessageFormatter.info(chalk.gray(`ā
${attribute.key}`));
|
1323
1326
|
}
|
1324
1327
|
return needsUpdate;
|
1325
1328
|
});
|
1326
1329
|
|
1327
1330
|
if (attributesToProcess.length === 0) {
|
1328
|
-
MessageFormatter.info(
|
1329
|
-
chalk.green(
|
1330
|
-
`ā
All ${attributes.length} attributes are already up to date for collection: ${collection.name}`
|
1331
|
-
)
|
1332
|
-
);
|
1333
1331
|
return true;
|
1334
1332
|
}
|
1335
1333
|
|
1336
|
-
MessageFormatter.info(
|
1337
|
-
chalk.blue(
|
1338
|
-
`Creating ${attributesToProcess.length} attributes sequentially with status monitoring...`
|
1339
|
-
)
|
1340
|
-
);
|
1341
|
-
|
1342
1334
|
let remainingAttributes = [...attributesToProcess];
|
1343
1335
|
let overallRetryCount = 0;
|
1344
1336
|
const maxOverallRetries = 3;
|
@@ -1350,21 +1342,7 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
|
|
1350
1342
|
const attributesToProcessThisRound = [...remainingAttributes];
|
1351
1343
|
remainingAttributes = []; // Reset for next iteration
|
1352
1344
|
|
1353
|
-
MessageFormatter.info(
|
1354
|
-
chalk.blue(
|
1355
|
-
`\n=== Attempt ${
|
1356
|
-
overallRetryCount + 1
|
1357
|
-
}/${maxOverallRetries} - Processing ${
|
1358
|
-
attributesToProcessThisRound.length
|
1359
|
-
} attributes ===`
|
1360
|
-
)
|
1361
|
-
);
|
1362
|
-
|
1363
1345
|
for (const attribute of attributesToProcessThisRound) {
|
1364
|
-
MessageFormatter.info(
|
1365
|
-
chalk.blue(`\n--- Processing attribute: ${attribute.key} ---`)
|
1366
|
-
);
|
1367
|
-
|
1368
1346
|
const success = await createOrUpdateAttributeWithStatusCheck(
|
1369
1347
|
db,
|
1370
1348
|
dbId,
|
@@ -1373,10 +1351,6 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
|
|
1373
1351
|
);
|
1374
1352
|
|
1375
1353
|
if (success) {
|
1376
|
-
MessageFormatter.info(
|
1377
|
-
chalk.green(`ā
Successfully created attribute: ${attribute.key}`)
|
1378
|
-
);
|
1379
|
-
|
1380
1354
|
// Mark this specific attribute as processed
|
1381
1355
|
markAttributeProcessed(currentCollection.$id, attribute.key);
|
1382
1356
|
|
@@ -1394,21 +1368,12 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
|
|
1394
1368
|
// Add delay between successful attributes
|
1395
1369
|
await delay(1000);
|
1396
1370
|
} else {
|
1397
|
-
MessageFormatter.info(
|
1398
|
-
chalk.red(
|
1399
|
-
`ā Failed to create attribute: ${attribute.key}, will retry in next round`
|
1400
|
-
)
|
1401
|
-
);
|
1371
|
+
MessageFormatter.info(chalk.red(`ā ${attribute.key}`));
|
1402
1372
|
remainingAttributes.push(attribute); // Add back to retry list
|
1403
1373
|
}
|
1404
1374
|
}
|
1405
1375
|
|
1406
1376
|
if (remainingAttributes.length === 0) {
|
1407
|
-
MessageFormatter.info(
|
1408
|
-
chalk.green(
|
1409
|
-
`\nā
Successfully created all ${attributesToProcess.length} attributes for collection: ${collection.name}`
|
1410
|
-
)
|
1411
|
-
);
|
1412
1377
|
return true;
|
1413
1378
|
}
|
1414
1379
|
|
@@ -1416,9 +1381,7 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
|
|
1416
1381
|
|
1417
1382
|
if (overallRetryCount < maxOverallRetries) {
|
1418
1383
|
MessageFormatter.info(
|
1419
|
-
chalk.yellow(
|
1420
|
-
`\nā³ Waiting 5 seconds before retrying ${attributesToProcess.length} failed attributes...`
|
1421
|
-
)
|
1384
|
+
chalk.yellow(`ā³ Retrying ${remainingAttributes.length} failed attributes...`)
|
1422
1385
|
);
|
1423
1386
|
await delay(5000);
|
1424
1387
|
|
@@ -1427,13 +1390,8 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
|
|
1427
1390
|
currentCollection = isDatabaseAdapter(db)
|
1428
1391
|
? (await db.getTable({ databaseId: dbId, tableId: collection.$id })).data as Models.Collection
|
1429
1392
|
: await db.getCollection(dbId, collection.$id);
|
1430
|
-
MessageFormatter.info(`Refreshed collection data for retry`);
|
1431
1393
|
} catch (error) {
|
1432
|
-
|
1433
|
-
chalk.yellow(
|
1434
|
-
`Warning: Could not refresh collection data for retry: ${error}`
|
1435
|
-
)
|
1436
|
-
);
|
1394
|
+
// Silently continue if refresh fails
|
1437
1395
|
}
|
1438
1396
|
}
|
1439
1397
|
}
|
@@ -35,12 +35,9 @@ const waitForIndexAvailable = async (
|
|
35
35
|
// Calculate exponential backoff: 2s, 4s, 8s, 16s, 30s (capped at 30s)
|
36
36
|
if (retryCount > 0) {
|
37
37
|
const exponentialDelay = calculateExponentialBackoff(retryCount);
|
38
|
-
MessageFormatter.info(`Waiting for index '${indexKey}' to become available (retry ${retryCount}, backoff: ${exponentialDelay}ms)...`);
|
39
38
|
await delay(exponentialDelay);
|
40
|
-
} else {
|
41
|
-
MessageFormatter.info(`Waiting for index '${indexKey}' to become available...`);
|
42
39
|
}
|
43
|
-
|
40
|
+
|
44
41
|
while (Date.now() - startTime < maxWaitTime) {
|
45
42
|
try {
|
46
43
|
const indexList = await (isLegacyDatabases(db)
|
@@ -56,15 +53,8 @@ const waitForIndexAvailable = async (
|
|
56
53
|
return false;
|
57
54
|
}
|
58
55
|
|
59
|
-
if (isLegacyDatabases(db)) {
|
60
|
-
MessageFormatter.debug(`Index '${indexKey}' status: ${(index as any).status}`);
|
61
|
-
} else {
|
62
|
-
MessageFormatter.debug(`Index '${indexKey}' detected (TablesDB)`);
|
63
|
-
}
|
64
|
-
|
65
56
|
switch (index.status) {
|
66
57
|
case 'available':
|
67
|
-
MessageFormatter.success(`Index '${indexKey}' is now available (type: ${index.type}, attributes: [${index.attributes.join(', ')}])`);
|
68
58
|
return true;
|
69
59
|
|
70
60
|
case 'failed':
|
@@ -229,11 +219,7 @@ export const createOrUpdateIndexesWithStatusCheck = async (
|
|
229
219
|
const remainingIndexes = [...indexesToProcess];
|
230
220
|
indexesToProcess = []; // Reset for next iteration
|
231
221
|
|
232
|
-
MessageFormatter.info(`\n=== Attempt ${overallRetryCount + 1}/${maxOverallRetries} - Processing ${remainingIndexes.length} indexes ===`);
|
233
|
-
|
234
222
|
for (const index of remainingIndexes) {
|
235
|
-
MessageFormatter.info(`\n--- Processing index: ${index.key} (type: ${index.type}, attributes: [${index.attributes.join(', ')}]) ---`);
|
236
|
-
|
237
223
|
const success = await createOrUpdateIndexWithStatusCheck(
|
238
224
|
dbId,
|
239
225
|
db,
|
@@ -243,25 +229,24 @@ export const createOrUpdateIndexesWithStatusCheck = async (
|
|
243
229
|
);
|
244
230
|
|
245
231
|
if (success) {
|
246
|
-
MessageFormatter.
|
232
|
+
MessageFormatter.info(`ā
${index.key} (${index.type})`);
|
247
233
|
|
248
234
|
// Add delay between successful indexes
|
249
235
|
await delay(1000);
|
250
236
|
} else {
|
251
|
-
MessageFormatter.
|
237
|
+
MessageFormatter.info(`ā ${index.key} (${index.type})`);
|
252
238
|
indexesToProcess.push(index); // Add back to retry list
|
253
239
|
}
|
254
240
|
}
|
255
241
|
|
256
242
|
if (indexesToProcess.length === 0) {
|
257
|
-
MessageFormatter.success(`\nSuccessfully created all ${indexes.length} indexes for collection '${collectionId}'`);
|
258
243
|
return true;
|
259
244
|
}
|
260
245
|
|
261
246
|
overallRetryCount++;
|
262
247
|
|
263
248
|
if (overallRetryCount < maxOverallRetries) {
|
264
|
-
MessageFormatter.warning(
|
249
|
+
MessageFormatter.warning(`ā³ Retrying ${indexesToProcess.length} failed indexes...`);
|
265
250
|
await delay(5000);
|
266
251
|
}
|
267
252
|
}
|
@@ -518,6 +518,52 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
518
518
|
}
|
519
519
|
}
|
520
520
|
|
521
|
+
// Wait for all attributes to become available before creating indexes
|
522
|
+
const allAttrKeys = [
|
523
|
+
...nonRel.map((a: any) => a.key),
|
524
|
+
...rels.filter((a: any) => a.relatedCollection).map((a: any) => a.key)
|
525
|
+
];
|
526
|
+
|
527
|
+
if (allAttrKeys.length > 0) {
|
528
|
+
for (const attrKey of allAttrKeys) {
|
529
|
+
const maxWait = 60000; // 60 seconds
|
530
|
+
const startTime = Date.now();
|
531
|
+
let lastStatus = '';
|
532
|
+
|
533
|
+
while (Date.now() - startTime < maxWait) {
|
534
|
+
try {
|
535
|
+
const tableData = await adapter.getTable({ databaseId, tableId });
|
536
|
+
const attrs = (tableData as any).attributes || [];
|
537
|
+
const attr = attrs.find((a: any) => a.key === attrKey);
|
538
|
+
|
539
|
+
if (attr) {
|
540
|
+
if (attr.status === 'available') {
|
541
|
+
break; // Attribute is ready
|
542
|
+
}
|
543
|
+
if (attr.status === 'failed' || attr.status === 'stuck') {
|
544
|
+
throw new Error(`Attribute ${attrKey} failed to create: ${attr.error || 'unknown error'}`);
|
545
|
+
}
|
546
|
+
// Still processing, continue waiting
|
547
|
+
lastStatus = attr.status;
|
548
|
+
}
|
549
|
+
|
550
|
+
await delay(2000); // Check every 2 seconds
|
551
|
+
} catch (e) {
|
552
|
+
// If we can't check status, assume it's processing and continue
|
553
|
+
await delay(2000);
|
554
|
+
}
|
555
|
+
}
|
556
|
+
|
557
|
+
// Timeout check
|
558
|
+
if (Date.now() - startTime >= maxWait) {
|
559
|
+
MessageFormatter.warning(
|
560
|
+
`Attribute ${attrKey} did not become available within ${maxWait / 1000}s (last status: ${lastStatus}). Proceeding anyway.`,
|
561
|
+
{ prefix: 'Attributes' }
|
562
|
+
);
|
563
|
+
}
|
564
|
+
}
|
565
|
+
}
|
566
|
+
|
521
567
|
// Indexes
|
522
568
|
const idxs = (indexes || []) as any[];
|
523
569
|
for (const idx of idxs) {
|
package/src/interactiveCLI.ts
CHANGED
@@ -324,19 +324,15 @@ export class InteractiveCLI {
|
|
324
324
|
];
|
325
325
|
|
326
326
|
if (shouldFilterByDatabase) {
|
327
|
-
//
|
327
|
+
// Show collections that EITHER exist in the remote database OR have matching local databaseId metadata
|
328
328
|
allCollections = allCollections.filter((c: any) => {
|
329
|
-
//
|
330
|
-
|
331
|
-
|
332
|
-
|
329
|
+
// Include if it exists remotely in this database
|
330
|
+
const existsInRemoteDb = remoteCollections.some((rc) => rc.name === c.name);
|
331
|
+
|
332
|
+
// Include if local metadata claims it belongs to this database
|
333
|
+
const hasMatchingLocalMetadata = c.databaseId === database.$id;
|
333
334
|
|
334
|
-
|
335
|
-
// - Collections without databaseId are kept (backward compatibility)
|
336
|
-
// - Tables with databaseId must match the selected database
|
337
|
-
// - Tables without databaseId are kept (fallback for misconfigured tables)
|
338
|
-
if (!c.databaseId) return true;
|
339
|
-
return c.databaseId === database.$id;
|
335
|
+
return existsInRemoteDb || hasMatchingLocalMetadata;
|
340
336
|
});
|
341
337
|
}
|
342
338
|
|
@@ -1,6 +1,9 @@
|
|
1
1
|
import yaml from "js-yaml";
|
2
2
|
import type { Collection, CollectionCreate } from "appwrite-utils";
|
3
3
|
|
4
|
+
// Threshold for treating min/max values as undefined (1 trillion)
|
5
|
+
const MIN_MAX_THRESHOLD = 1_000_000_000_000;
|
6
|
+
|
4
7
|
export interface YamlCollectionData {
|
5
8
|
name: string;
|
6
9
|
id?: string;
|
@@ -16,6 +19,7 @@ export interface YamlCollectionData {
|
|
16
19
|
size?: number;
|
17
20
|
required?: boolean;
|
18
21
|
array?: boolean;
|
22
|
+
encrypt?: boolean;
|
19
23
|
default?: any;
|
20
24
|
min?: number;
|
21
25
|
max?: number;
|
@@ -34,6 +38,7 @@ export interface YamlCollectionData {
|
|
34
38
|
size?: number;
|
35
39
|
required?: boolean;
|
36
40
|
array?: boolean;
|
41
|
+
encrypt?: boolean;
|
37
42
|
default?: any;
|
38
43
|
min?: number;
|
39
44
|
max?: number;
|
@@ -105,9 +110,30 @@ export function collectionToYaml(
|
|
105
110
|
if ('size' in attr && attr.size !== undefined) yamlAttr.size = attr.size;
|
106
111
|
if (attr.required !== undefined) yamlAttr.required = attr.required;
|
107
112
|
if (attr.array !== undefined) yamlAttr.array = attr.array;
|
113
|
+
|
114
|
+
// Always include encrypt field for string attributes (default to false)
|
115
|
+
if (attr.type === 'string') {
|
116
|
+
yamlAttr.encrypt = ('encrypted' in attr && attr.encrypted === true) ? true : false;
|
117
|
+
}
|
118
|
+
|
108
119
|
if ('xdefault' in attr && attr.xdefault !== undefined) yamlAttr.default = attr.xdefault;
|
109
|
-
|
110
|
-
|
120
|
+
|
121
|
+
// Normalize min/max values - filter out extreme database values
|
122
|
+
if ('min' in attr && attr.min !== undefined) {
|
123
|
+
const minValue = Number(attr.min);
|
124
|
+
// Only include min if it's within reasonable range (< 1 trillion)
|
125
|
+
if (Math.abs(minValue) < MIN_MAX_THRESHOLD) {
|
126
|
+
yamlAttr.min = attr.min;
|
127
|
+
}
|
128
|
+
}
|
129
|
+
|
130
|
+
if ('max' in attr && attr.max !== undefined) {
|
131
|
+
const maxValue = Number(attr.max);
|
132
|
+
// Only include max if it's within reasonable range (< 1 trillion)
|
133
|
+
if (Math.abs(maxValue) < MIN_MAX_THRESHOLD) {
|
134
|
+
yamlAttr.max = attr.max;
|
135
|
+
}
|
136
|
+
}
|
111
137
|
if ('elements' in attr && attr.elements !== undefined) yamlAttr.elements = attr.elements;
|
112
138
|
if ('relatedCollection' in attr && attr.relatedCollection !== undefined) yamlAttr.relatedCollection = attr.relatedCollection;
|
113
139
|
if ('relationType' in attr && attr.relationType !== undefined) yamlAttr.relationType = attr.relationType;
|