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.
@@ -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
- // First sync databases and collections
31
- await cli.controller.syncDb(databases, collections);
32
- MessageFormatter.success("Database and collections pushed successfully", { prefix: "Database" });
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 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;
@@ -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 && !attribute.required ? attribute.xdefault : undefined, attribute.size);
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 && !attribute.required ? attribute.xdefault : undefined, normalizedMin !== undefined ? parseInt(String(normalizedMin)) : undefined, normalizedMax !== undefined ? parseInt(String(normalizedMax)) : 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.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined);
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.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined);
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.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined);
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.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined);
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.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined);
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.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined);
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.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined);
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(`āž• New attribute: ${attribute.key}`);
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(`šŸ”„ Changed attribute: ${attribute.key}`);
844
+ MessageFormatter.info(`šŸ”„ ${attribute.key}`);
813
845
  }
814
846
  else {
815
- MessageFormatter.info(chalk.gray(`āœ… Unchanged attribute: ${attribute.key} (skipping)`));
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(`āŒ Failed to create attribute: ${attribute.key}, will retry in next round`));
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(`\nā³ Waiting 5 seconds before retrying ${attributesToProcess.length} failed attributes...`));
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
- MessageFormatter.info(chalk.yellow(`Warning: Could not refresh collection data for retry: ${error}`));
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.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) {
@@ -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
- // Enhanced filtering for tables with optional databaseId
256
+ // Show collections that EITHER exist in the remote database OR have matching local databaseId metadata
257
257
  allCollections = allCollections.filter((c) => {
258
- // For remote collections, they should match the selected database
259
- if (remoteCollections.some((rc) => rc.name === c.name)) {
260
- return c.databaseId === database.$id;
261
- }
262
- // For local collections/tables:
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
- if ('min' in attr && attr.min !== undefined)
43
- yamlAttr.min = attr.min;
44
- if ('max' in attr && attr.max !== undefined)
45
- yamlAttr.max = attr.max;
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.1",
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
- const collections = await (cli as any).selectCollectionsAndTables(
27
- databases[0],
28
- (cli as any).controller!.database!,
29
- chalk.blue("Select local collections/tables to push:"),
30
- true, // multiSelect
31
- true // prefer local
32
- // shouldFilterByDatabase removed - user will be prompted interactively
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
- const { syncFunctions } = await inquirer.prompt([
36
- {
37
- type: "confirm",
38
- name: "syncFunctions",
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
- try {
45
- // First sync databases and collections
46
- await (cli as any).controller!.syncDb(databases, collections);
47
- MessageFormatter.success("Database and collections pushed successfully", { prefix: "Database" });
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 (10 billion)
21
- const MIN_MAX_THRESHOLD = 10_000_000_000;
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 && !attribute.required ? (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 && !attribute.required ? (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.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined
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.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined
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.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined
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.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined
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.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined
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.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined
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.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined
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(`āž• New attribute: ${attribute.key}`);
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(`šŸ”„ Changed attribute: ${attribute.key}`);
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
- MessageFormatter.info(
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.success(`Successfully created index: ${index.key} (type: ${index.type})`);
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.error(`Failed to create index: ${index.key} (type: ${index.type}), will retry in next round`);
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(`\nWaiting 5 seconds before retrying ${indexesToProcess.length} failed indexes...`);
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) {
@@ -324,19 +324,15 @@ export class InteractiveCLI {
324
324
  ];
325
325
 
326
326
  if (shouldFilterByDatabase) {
327
- // Enhanced filtering for tables with optional databaseId
327
+ // Show collections that EITHER exist in the remote database OR have matching local databaseId metadata
328
328
  allCollections = allCollections.filter((c: any) => {
329
- // For remote collections, they should match the selected database
330
- if (remoteCollections.some((rc) => rc.name === c.name)) {
331
- return c.databaseId === database.$id;
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
- // For local collections/tables:
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
- if ('min' in attr && attr.min !== undefined) yamlAttr.min = attr.min;
110
- if ('max' in attr && attr.max !== undefined) yamlAttr.max = attr.max;
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;