appwrite-utils-cli 1.4.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +22 -1
  2. package/dist/adapters/TablesDBAdapter.js +7 -4
  3. package/dist/collections/attributes.d.ts +1 -1
  4. package/dist/collections/attributes.js +24 -6
  5. package/dist/collections/indexes.js +13 -3
  6. package/dist/collections/methods.d.ts +9 -0
  7. package/dist/collections/methods.js +268 -0
  8. package/dist/migrations/appwriteToX.d.ts +2 -2
  9. package/dist/migrations/comprehensiveTransfer.js +12 -0
  10. package/dist/migrations/dataLoader.d.ts +5 -5
  11. package/dist/migrations/relationships.d.ts +2 -2
  12. package/dist/shared/jsonSchemaGenerator.d.ts +1 -0
  13. package/dist/shared/jsonSchemaGenerator.js +6 -2
  14. package/dist/shared/operationQueue.js +14 -1
  15. package/dist/shared/schemaGenerator.d.ts +2 -1
  16. package/dist/shared/schemaGenerator.js +61 -78
  17. package/dist/storage/schemas.d.ts +8 -8
  18. package/dist/utils/loadConfigs.js +44 -19
  19. package/dist/utils/schemaStrings.d.ts +2 -1
  20. package/dist/utils/schemaStrings.js +61 -78
  21. package/dist/utils/setupFiles.js +19 -1
  22. package/dist/utils/versionDetection.d.ts +6 -0
  23. package/dist/utils/versionDetection.js +30 -0
  24. package/dist/utilsController.js +28 -4
  25. package/package.json +1 -1
  26. package/src/adapters/TablesDBAdapter.ts +20 -17
  27. package/src/collections/attributes.ts +122 -99
  28. package/src/collections/indexes.ts +36 -28
  29. package/src/collections/methods.ts +292 -19
  30. package/src/migrations/comprehensiveTransfer.ts +22 -8
  31. package/src/shared/jsonSchemaGenerator.ts +36 -29
  32. package/src/shared/operationQueue.ts +48 -33
  33. package/src/shared/schemaGenerator.ts +128 -134
  34. package/src/utils/loadConfigs.ts +48 -29
  35. package/src/utils/schemaStrings.ts +124 -130
  36. package/src/utils/setupFiles.ts +21 -5
  37. package/src/utils/versionDetection.ts +48 -21
  38. package/src/utilsController.ts +44 -24
@@ -7,12 +7,14 @@ import { ImportController } from "./migrations/importController.js";
7
7
  import { ImportDataActions } from "./migrations/importDataActions.js";
8
8
  import { setupMigrationDatabase, ensureDatabasesExist, wipeOtherDatabases, ensureCollectionsExist, } from "./databases/setup.js";
9
9
  import { createOrUpdateCollections, wipeDatabase, generateSchemas, fetchAllCollections, wipeCollection, } from "./collections/methods.js";
10
+ import { wipeAllTables, wipeTableRows } from "./collections/methods.js";
10
11
  import { backupDatabase, ensureDatabaseConfigBucketsExist, initOrGetBackupStorage, wipeDocumentStorage, } from "./storage/methods.js";
11
12
  import path from "path";
12
13
  import { converterFunctions, validationRules, } from "appwrite-utils";
13
14
  import { afterImportActions } from "./migrations/afterImportActions.js";
14
15
  import { transferDatabaseLocalToLocal, transferDatabaseLocalToRemote, transferStorageLocalToLocal, transferStorageLocalToRemote, transferUsersLocalToRemote, } from "./migrations/transfer.js";
15
16
  import { getClient } from "./utils/getClientFromConfig.js";
17
+ import { getAdapterFromConfig } from "./utils/getClientFromConfig.js";
16
18
  import { fetchAllDatabases } from "./databases/methods.js";
17
19
  import { listFunctions, updateFunctionSpecifications, } from "./functions/methods.js";
18
20
  import chalk from "chalk";
@@ -291,9 +293,20 @@ export class UtilsController {
291
293
  }
292
294
  async wipeDatabase(database, wipeBucket = false) {
293
295
  await this.init();
294
- if (!this.database)
296
+ if (!this.database || !this.config)
295
297
  throw new Error("Database not initialized");
296
- await wipeDatabase(this.database, database.$id);
298
+ try {
299
+ const { adapter, apiMode } = await getAdapterFromConfig(this.config);
300
+ if (apiMode === 'tablesdb') {
301
+ await wipeAllTables(adapter, database.$id);
302
+ }
303
+ else {
304
+ await wipeDatabase(this.database, database.$id);
305
+ }
306
+ }
307
+ catch {
308
+ await wipeDatabase(this.database, database.$id);
309
+ }
297
310
  if (wipeBucket) {
298
311
  await this.wipeBucketFromDatabase(database);
299
312
  }
@@ -323,9 +336,20 @@ export class UtilsController {
323
336
  }
324
337
  async wipeCollection(database, collection) {
325
338
  await this.init();
326
- if (!this.database)
339
+ if (!this.database || !this.config)
327
340
  throw new Error("Database not initialized");
328
- await wipeCollection(this.database, database.$id, collection.$id);
341
+ try {
342
+ const { adapter, apiMode } = await getAdapterFromConfig(this.config);
343
+ if (apiMode === 'tablesdb') {
344
+ await wipeTableRows(adapter, database.$id, collection.$id);
345
+ }
346
+ else {
347
+ await wipeCollection(this.database, database.$id, collection.$id);
348
+ }
349
+ }
350
+ catch {
351
+ await wipeCollection(this.database, database.$id, collection.$id);
352
+ }
329
353
  }
330
354
  async wipeDocumentStorage(bucketId) {
331
355
  await this.init();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "appwrite-utils-cli",
3
3
  "description": "Appwrite Utility Functions to help with database management, data conversion, data import, migrations, and much more. Meant to be used as a CLI tool, I do not recommend installing this in frontend environments.",
4
- "version": "1.4.1",
4
+ "version": "1.5.0",
5
5
  "main": "src/main.ts",
6
6
  "type": "module",
7
7
  "repository": {
@@ -246,12 +246,13 @@ export class TablesDBAdapter extends BaseAdapter {
246
246
  }
247
247
 
248
248
  // Attribute Operations
249
- async createAttribute(params: CreateAttributeParams): Promise<ApiResponse> {
250
- try {
251
- // TablesDB may have different method names for attribute operations
252
- const result = await this.tablesDB.createAttribute(params);
253
- return { data: result };
254
- } catch (error) {
249
+ async createAttribute(params: CreateAttributeParams): Promise<ApiResponse> {
250
+ try {
251
+ // Prefer createColumn if available, fallback to createAttribute
252
+ const fn = this.tablesDB.createColumn || this.tablesDB.createAttribute;
253
+ const result = await fn.call(this.tablesDB, params);
254
+ return { data: result };
255
+ } catch (error) {
255
256
  throw new AdapterError(
256
257
  `Failed to create attribute: ${error instanceof Error ? error.message : 'Unknown error'}`,
257
258
  'CREATE_ATTRIBUTE_FAILED',
@@ -260,11 +261,12 @@ export class TablesDBAdapter extends BaseAdapter {
260
261
  }
261
262
  }
262
263
 
263
- async updateAttribute(params: UpdateAttributeParams): Promise<ApiResponse> {
264
- try {
265
- const result = await this.tablesDB.updateAttribute(params);
266
- return { data: result };
267
- } catch (error) {
264
+ async updateAttribute(params: UpdateAttributeParams): Promise<ApiResponse> {
265
+ try {
266
+ const fn = this.tablesDB.updateColumn || this.tablesDB.updateAttribute;
267
+ const result = await fn.call(this.tablesDB, params);
268
+ return { data: result };
269
+ } catch (error) {
268
270
  throw new AdapterError(
269
271
  `Failed to update attribute: ${error instanceof Error ? error.message : 'Unknown error'}`,
270
272
  'UPDATE_ATTRIBUTE_FAILED',
@@ -273,11 +275,12 @@ export class TablesDBAdapter extends BaseAdapter {
273
275
  }
274
276
  }
275
277
 
276
- async deleteAttribute(params: DeleteAttributeParams): Promise<ApiResponse> {
277
- try {
278
- const result = await this.tablesDB.deleteAttribute(params);
279
- return { data: result };
280
- } catch (error) {
278
+ async deleteAttribute(params: DeleteAttributeParams): Promise<ApiResponse> {
279
+ try {
280
+ const fn = this.tablesDB.deleteColumn || this.tablesDB.deleteAttribute;
281
+ const result = await fn.call(this.tablesDB, params);
282
+ return { data: result };
283
+ } catch (error) {
281
284
  throw new AdapterError(
282
285
  `Failed to delete attribute: ${error instanceof Error ? error.message : 'Unknown error'}`,
283
286
  'DELETE_ATTRIBUTE_FAILED',
@@ -426,4 +429,4 @@ export class TablesDBAdapter extends BaseAdapter {
426
429
  );
427
430
  }
428
431
  }
429
- }
432
+ }
@@ -287,14 +287,14 @@ const attributesSame = (
287
287
  /**
288
288
  * Enhanced attribute creation with proper status monitoring and retry logic
289
289
  */
290
- export const createOrUpdateAttributeWithStatusCheck = async (
291
- db: Databases,
292
- dbId: string,
293
- collection: Models.Collection,
294
- attribute: Attribute,
295
- retryCount: number = 0,
296
- maxRetries: number = 5
297
- ): Promise<boolean> => {
290
+ export const createOrUpdateAttributeWithStatusCheck = async (
291
+ db: Databases,
292
+ dbId: string,
293
+ collection: Models.Collection,
294
+ attribute: Attribute,
295
+ retryCount: number = 0,
296
+ maxRetries: number = 5
297
+ ): Promise<boolean> => {
298
298
  console.log(
299
299
  chalk.blue(
300
300
  `Creating/updating attribute '${attribute.key}' (attempt ${
@@ -303,20 +303,31 @@ export const createOrUpdateAttributeWithStatusCheck = async (
303
303
  )
304
304
  );
305
305
 
306
- try {
307
- // First, try to create/update the attribute using existing logic
308
- await createOrUpdateAttribute(db, dbId, collection, attribute);
309
-
310
- // Now wait for the attribute to become available
311
- const success = await waitForAttributeAvailable(
312
- db,
313
- dbId,
314
- collection.$id,
315
- attribute.key,
316
- 60000, // 1 minute timeout
317
- retryCount,
318
- maxRetries
319
- );
306
+ try {
307
+ // First, try to create/update the attribute using existing logic
308
+ const result = await createOrUpdateAttribute(db, dbId, collection, attribute);
309
+
310
+ // If the attribute was queued (relationship dependency unresolved),
311
+ // skip status polling and retry logic — the queue will handle it later.
312
+ if (result === "queued") {
313
+ console.log(
314
+ chalk.yellow(
315
+ `⏭️ Deferred relationship attribute '${attribute.key}' — queued for later once dependencies are available`
316
+ )
317
+ );
318
+ return true;
319
+ }
320
+
321
+ // Now wait for the attribute to become available
322
+ const success = await waitForAttributeAvailable(
323
+ db,
324
+ dbId,
325
+ collection.$id,
326
+ attribute.key,
327
+ 60000, // 1 minute timeout
328
+ retryCount,
329
+ maxRetries
330
+ );
320
331
 
321
332
  if (success) {
322
333
  return true;
@@ -440,12 +451,12 @@ export const createOrUpdateAttributeWithStatusCheck = async (
440
451
  }
441
452
  };
442
453
 
443
- export const createOrUpdateAttribute = async (
444
- db: Databases,
445
- dbId: string,
446
- collection: Models.Collection,
447
- attribute: Attribute
448
- ): Promise<void> => {
454
+ export const createOrUpdateAttribute = async (
455
+ db: Databases,
456
+ dbId: string,
457
+ collection: Models.Collection,
458
+ attribute: Attribute
459
+ ): Promise<"queued" | "processed"> => {
449
460
  let action = "create";
450
461
  let foundAttribute: Attribute | undefined;
451
462
  const updateEnabled = true;
@@ -460,13 +471,13 @@ export const createOrUpdateAttribute = async (
460
471
  foundAttribute = undefined;
461
472
  }
462
473
 
463
- if (
464
- foundAttribute &&
465
- attributesSame(foundAttribute, attribute) &&
466
- updateEnabled
467
- ) {
468
- // No need to do anything, they are the same
469
- return;
474
+ if (
475
+ foundAttribute &&
476
+ attributesSame(foundAttribute, attribute) &&
477
+ updateEnabled
478
+ ) {
479
+ // No need to do anything, they are the same
480
+ return "processed";
470
481
  } else if (
471
482
  foundAttribute &&
472
483
  !attributesSame(foundAttribute, attribute) &&
@@ -484,71 +495,82 @@ export const createOrUpdateAttribute = async (
484
495
  !updateEnabled &&
485
496
  foundAttribute &&
486
497
  !attributesSame(foundAttribute, attribute)
487
- ) {
488
- await db.deleteAttribute(dbId, collection.$id, attribute.key);
489
- console.log(
490
- `Deleted attribute: ${attribute.key} to recreate it because they diff (update disabled temporarily)`
491
- );
492
- return;
493
- }
498
+ ) {
499
+ await db.deleteAttribute(dbId, collection.$id, attribute.key);
500
+ console.log(
501
+ `Deleted attribute: ${attribute.key} to recreate it because they diff (update disabled temporarily)`
502
+ );
503
+ return "processed";
504
+ }
494
505
 
495
506
  // console.log(`${action}-ing attribute: ${finalAttribute.key}`);
496
507
 
497
508
  // Relationship attribute logic with adjustments
498
509
  let collectionFoundViaRelatedCollection: Models.Collection | undefined;
499
510
  let relatedCollectionId: string | undefined;
500
- if (
501
- finalAttribute.type === "relationship" &&
502
- finalAttribute.relatedCollection
503
- ) {
504
- if (nameToIdMapping.has(finalAttribute.relatedCollection)) {
505
- relatedCollectionId = nameToIdMapping.get(
506
- finalAttribute.relatedCollection
507
- );
508
- try {
509
- collectionFoundViaRelatedCollection = await db.getCollection(
510
- dbId,
511
- relatedCollectionId!
512
- );
513
- } catch (e) {
514
- // console.log(
515
- // `Collection not found: ${finalAttribute.relatedCollection} when nameToIdMapping was set`
516
- // );
517
- collectionFoundViaRelatedCollection = undefined;
518
- }
519
- } else {
520
- const collectionsPulled = await db.listCollections(dbId, [
521
- Query.equal("name", finalAttribute.relatedCollection),
522
- ]);
523
- if (collectionsPulled.total > 0) {
524
- collectionFoundViaRelatedCollection = collectionsPulled.collections[0];
525
- relatedCollectionId = collectionFoundViaRelatedCollection.$id;
526
- nameToIdMapping.set(
527
- finalAttribute.relatedCollection,
528
- relatedCollectionId
529
- );
530
- }
531
- }
532
- // ONLY queue relationship attributes that have actual unresolved dependencies
533
- if (!(relatedCollectionId && collectionFoundViaRelatedCollection)) {
534
- console.log(
535
- chalk.yellow(
536
- `⏳ Queueing relationship attribute '${finalAttribute.key}' - related collection '${finalAttribute.relatedCollection}' not found yet`
537
- )
538
- );
539
- enqueueOperation({
540
- type: "attribute",
541
- collectionId: collection.$id,
542
- collection: collection,
543
- attribute,
544
- dependencies: [finalAttribute.relatedCollection],
545
- });
546
- return;
547
- }
548
- }
549
- finalAttribute = parseAttribute(finalAttribute);
550
- // console.log(`Final Attribute: ${JSON.stringify(finalAttribute)}`);
551
- switch (finalAttribute.type) {
511
+ if (
512
+ finalAttribute.type === "relationship" &&
513
+ finalAttribute.relatedCollection
514
+ ) {
515
+ // First try treating relatedCollection as an ID directly
516
+ try {
517
+ const byIdCollection = await db.getCollection(dbId, finalAttribute.relatedCollection);
518
+ collectionFoundViaRelatedCollection = byIdCollection;
519
+ relatedCollectionId = byIdCollection.$id;
520
+ // Cache by name for subsequent lookups
521
+ nameToIdMapping.set(byIdCollection.name, byIdCollection.$id);
522
+ } catch (_) {
523
+ // Not an ID or not found — fall back to name-based resolution below
524
+ }
525
+
526
+ if (!collectionFoundViaRelatedCollection && nameToIdMapping.has(finalAttribute.relatedCollection)) {
527
+ relatedCollectionId = nameToIdMapping.get(
528
+ finalAttribute.relatedCollection
529
+ );
530
+ try {
531
+ collectionFoundViaRelatedCollection = await db.getCollection(
532
+ dbId,
533
+ relatedCollectionId!
534
+ );
535
+ } catch (e) {
536
+ // console.log(
537
+ // `Collection not found: ${finalAttribute.relatedCollection} when nameToIdMapping was set`
538
+ // );
539
+ collectionFoundViaRelatedCollection = undefined;
540
+ }
541
+ } else if (!collectionFoundViaRelatedCollection) {
542
+ const collectionsPulled = await db.listCollections(dbId, [
543
+ Query.equal("name", finalAttribute.relatedCollection),
544
+ ]);
545
+ if (collectionsPulled.total > 0) {
546
+ collectionFoundViaRelatedCollection = collectionsPulled.collections[0];
547
+ relatedCollectionId = collectionFoundViaRelatedCollection.$id;
548
+ nameToIdMapping.set(
549
+ finalAttribute.relatedCollection,
550
+ relatedCollectionId
551
+ );
552
+ }
553
+ }
554
+ // ONLY queue relationship attributes that have actual unresolved dependencies
555
+ if (!(relatedCollectionId && collectionFoundViaRelatedCollection)) {
556
+ console.log(
557
+ chalk.yellow(
558
+ `⏳ Queueing relationship attribute '${finalAttribute.key}' - related collection '${finalAttribute.relatedCollection}' not found yet`
559
+ )
560
+ );
561
+ enqueueOperation({
562
+ type: "attribute",
563
+ collectionId: collection.$id,
564
+ collection: collection,
565
+ attribute,
566
+ dependencies: [finalAttribute.relatedCollection],
567
+ });
568
+ return "queued";
569
+ }
570
+ }
571
+ finalAttribute = parseAttribute(finalAttribute);
572
+ // console.log(`Final Attribute: ${JSON.stringify(finalAttribute)}`);
573
+ switch (finalAttribute.type) {
552
574
  case "string":
553
575
  if (action === "create") {
554
576
  await tryAwaitWithRetry(
@@ -910,11 +932,12 @@ export const createOrUpdateAttribute = async (
910
932
  );
911
933
  }
912
934
  break;
913
- default:
914
- console.error("Invalid attribute type");
915
- break;
916
- }
917
- };
935
+ default:
936
+ console.error("Invalid attribute type");
937
+ break;
938
+ }
939
+ return "processed";
940
+ };
918
941
 
919
942
  /**
920
943
  * Enhanced collection attribute creation with proper status monitoring
@@ -1,5 +1,6 @@
1
- import { indexSchema, type Index } from "appwrite-utils";
2
- import { Databases, IndexType, Query, type Models } from "node-appwrite";
1
+ import { indexSchema, type Index } from "appwrite-utils";
2
+ import { Databases, IndexType, Query, type Models } from "node-appwrite";
3
+ import type { DatabaseAdapter } from "../adapters/DatabaseAdapter.js";
3
4
  import { delay, tryAwaitWithRetry } from "../utils/helperFunctions.js";
4
5
  import chalk from "chalk";
5
6
 
@@ -18,15 +19,15 @@ interface IndexWithStatus {
18
19
  /**
19
20
  * Wait for index to become available, with retry logic for stuck indexes and exponential backoff
20
21
  */
21
- const waitForIndexAvailable = async (
22
- db: Databases,
23
- dbId: string,
24
- collectionId: string,
25
- indexKey: string,
26
- maxWaitTime: number = 60000, // 1 minute
27
- retryCount: number = 0,
28
- maxRetries: number = 5
29
- ): Promise<boolean> => {
22
+ const waitForIndexAvailable = async (
23
+ db: Databases | DatabaseAdapter,
24
+ dbId: string,
25
+ collectionId: string,
26
+ indexKey: string,
27
+ maxWaitTime: number = 60000, // 1 minute
28
+ retryCount: number = 0,
29
+ maxRetries: number = 5
30
+ ): Promise<boolean> => {
30
31
  const startTime = Date.now();
31
32
  let checkInterval = 2000; // Start with 2 seconds
32
33
 
@@ -41,22 +42,29 @@ const waitForIndexAvailable = async (
41
42
 
42
43
  while (Date.now() - startTime < maxWaitTime) {
43
44
  try {
44
- const indexList = await db.listIndexes(dbId, collectionId);
45
- const index = indexList.indexes.find(
46
- (idx: any) => idx.key === indexKey
47
- ) as IndexWithStatus | undefined;
45
+ const indexList = await (db instanceof Databases
46
+ ? db.listIndexes(dbId, collectionId)
47
+ : (db as DatabaseAdapter).listIndexes({ databaseId: dbId, tableId: collectionId }));
48
+ const indexes: any[] = (db instanceof Databases)
49
+ ? (indexList as any).indexes
50
+ : ((indexList as any).data || (indexList as any).indexes || []);
51
+ const index = indexes.find((idx: any) => idx.key === indexKey) as IndexWithStatus | undefined;
48
52
 
49
53
  if (!index) {
50
54
  console.log(chalk.red(`Index '${indexKey}' not found`));
51
55
  return false;
52
56
  }
53
57
 
54
- console.log(chalk.gray(`Index '${indexKey}' status: ${index.status}`));
58
+ if (db instanceof Databases) {
59
+ console.log(chalk.gray(`Index '${indexKey}' status: ${(index as any).status}`));
60
+ } else {
61
+ console.log(chalk.gray(`Index '${indexKey}' detected (TablesDB)`));
62
+ }
55
63
 
56
64
  switch (index.status) {
57
- case 'available':
58
- console.log(chalk.green(`✅ Index '${indexKey}' is now available`));
59
- return true;
65
+ case 'available':
66
+ console.log(chalk.green(`✅ Index '${indexKey}' is now available`));
67
+ return true;
60
68
 
61
69
  case 'failed':
62
70
  console.log(chalk.red(`❌ Index '${indexKey}' failed: ${index.error}`));
@@ -66,9 +74,9 @@ const waitForIndexAvailable = async (
66
74
  console.log(chalk.yellow(`⚠️ Index '${indexKey}' is stuck, will retry...`));
67
75
  return false;
68
76
 
69
- case 'processing':
70
- // Continue waiting
71
- break;
77
+ case 'processing':
78
+ // Continue waiting
79
+ break;
72
80
 
73
81
  case 'deleting':
74
82
  console.log(chalk.yellow(`Index '${indexKey}' is being deleted`));
@@ -79,12 +87,12 @@ const waitForIndexAvailable = async (
79
87
  break;
80
88
  }
81
89
 
82
- await delay(checkInterval);
83
- } catch (error) {
84
- console.log(chalk.red(`Error checking index status: ${error}`));
85
- return false;
86
- }
87
- }
90
+ await delay(checkInterval);
91
+ } catch (error) {
92
+ console.log(chalk.red(`Error checking index status: ${error}`));
93
+ return false;
94
+ }
95
+ }
88
96
 
89
97
  // Timeout reached
90
98
  console.log(chalk.yellow(`⏰ Timeout waiting for index '${indexKey}' (${maxWaitTime}ms)`));