appwrite-utils-cli 1.0.9 → 1.1.1

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.
@@ -1,4 +1,4 @@
1
- import { converterFunctions, tryAwaitWithRetry } from "appwrite-utils";
1
+ import { converterFunctions, tryAwaitWithRetry, parseAttribute } from "appwrite-utils";
2
2
  import {
3
3
  Client,
4
4
  Databases,
@@ -204,7 +204,10 @@ export class ComprehensiveTransfer {
204
204
  return;
205
205
  }
206
206
 
207
- const transferTasks = sourceDatabases.databases.map(db =>
207
+ // Phase 1: Create all databases and collections (structure only)
208
+ MessageFormatter.info("Phase 1: Creating database structures (databases, collections, attributes, indexes)", { prefix: "Transfer" });
209
+
210
+ const structureCreationTasks = sourceDatabases.databases.map(db =>
208
211
  this.limit(async () => {
209
212
  try {
210
213
  // Check if database exists in target
@@ -216,32 +219,189 @@ export class ComprehensiveTransfer {
216
219
  MessageFormatter.success(`Created database: ${db.name}`, { prefix: "Transfer" });
217
220
  }
218
221
 
219
- // Transfer database content
220
- await transferDatabaseLocalToRemote(
221
- this.sourceDatabases,
222
- this.options.targetEndpoint,
223
- this.options.targetProject,
224
- this.options.targetKey,
225
- db.$id,
226
- db.$id
227
- );
222
+ // Create collections, attributes, and indexes WITHOUT transferring documents
223
+ await this.createDatabaseStructure(db.$id);
224
+
225
+ MessageFormatter.success(`Database structure created: ${db.name}`, { prefix: "Transfer" });
226
+ } catch (error) {
227
+ MessageFormatter.error(`Database structure creation failed for ${db.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
228
+ this.results.databases.failed++;
229
+ }
230
+ })
231
+ );
232
+
233
+ await Promise.all(structureCreationTasks);
234
+
235
+ // Phase 2: Transfer all documents after all structures are created
236
+ MessageFormatter.info("Phase 2: Transferring documents to all collections", { prefix: "Transfer" });
237
+
238
+ const documentTransferTasks = sourceDatabases.databases.map(db =>
239
+ this.limit(async () => {
240
+ try {
241
+ // Transfer documents for this database
242
+ await this.transferDatabaseDocuments(db.$id);
228
243
 
229
244
  this.results.databases.transferred++;
230
- MessageFormatter.success(`Database ${db.name} transferred successfully`, { prefix: "Transfer" });
245
+ MessageFormatter.success(`Database documents transferred: ${db.name}`, { prefix: "Transfer" });
231
246
  } catch (error) {
232
- MessageFormatter.error(`Database ${db.name} transfer failed`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
247
+ MessageFormatter.error(`Document transfer failed for ${db.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
233
248
  this.results.databases.failed++;
234
249
  }
235
250
  })
236
251
  );
237
252
 
238
- await Promise.all(transferTasks);
253
+ await Promise.all(documentTransferTasks);
239
254
  MessageFormatter.success("Database transfer phase completed", { prefix: "Transfer" });
240
255
  } catch (error) {
241
256
  MessageFormatter.error("Database transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
242
257
  }
243
258
  }
244
259
 
260
+ /**
261
+ * Phase 1: Create database structure (collections, attributes, indexes) without transferring documents
262
+ */
263
+ private async createDatabaseStructure(dbId: string): Promise<void> {
264
+ MessageFormatter.info(`Creating database structure for ${dbId}`, { prefix: "Transfer" });
265
+
266
+ try {
267
+ // Get all collections from source database
268
+ const sourceCollections = await this.fetchAllCollections(dbId, this.sourceDatabases);
269
+ MessageFormatter.info(`Found ${sourceCollections.length} collections in source database ${dbId}`, { prefix: "Transfer" });
270
+
271
+ // Process each collection
272
+ for (const collection of sourceCollections) {
273
+ MessageFormatter.info(`Processing collection: ${collection.name} (${collection.$id})`, { prefix: "Transfer" });
274
+
275
+ try {
276
+ // Create or update collection in target
277
+ let targetCollection: Models.Collection;
278
+ const existingCollection = await tryAwaitWithRetry(async () =>
279
+ this.targetDatabases.listCollections(dbId, [Query.equal("$id", collection.$id)])
280
+ );
281
+
282
+ if (existingCollection.collections.length > 0) {
283
+ targetCollection = existingCollection.collections[0];
284
+ MessageFormatter.info(`Collection ${collection.name} exists in target database`, { prefix: "Transfer" });
285
+
286
+ // Update collection if needed
287
+ if (
288
+ targetCollection.name !== collection.name ||
289
+ JSON.stringify(targetCollection.$permissions) !== JSON.stringify(collection.$permissions) ||
290
+ targetCollection.documentSecurity !== collection.documentSecurity ||
291
+ targetCollection.enabled !== collection.enabled
292
+ ) {
293
+ targetCollection = await tryAwaitWithRetry(async () =>
294
+ this.targetDatabases.updateCollection(
295
+ dbId,
296
+ collection.$id,
297
+ collection.name,
298
+ collection.$permissions,
299
+ collection.documentSecurity,
300
+ collection.enabled
301
+ )
302
+ );
303
+ MessageFormatter.success(`Collection ${collection.name} updated`, { prefix: "Transfer" });
304
+ }
305
+ } else {
306
+ MessageFormatter.info(`Creating collection ${collection.name} in target database...`, { prefix: "Transfer" });
307
+ targetCollection = await tryAwaitWithRetry(async () =>
308
+ this.targetDatabases.createCollection(
309
+ dbId,
310
+ collection.$id,
311
+ collection.name,
312
+ collection.$permissions,
313
+ collection.documentSecurity,
314
+ collection.enabled
315
+ )
316
+ );
317
+ MessageFormatter.success(`Collection ${collection.name} created`, { prefix: "Transfer" });
318
+ }
319
+
320
+ // Handle attributes with enhanced status checking
321
+ MessageFormatter.info(`Creating attributes for collection ${collection.name} with enhanced monitoring...`, { prefix: "Transfer" });
322
+
323
+ const attributesToCreate = collection.attributes.map(attr => parseAttribute(attr as any));
324
+
325
+ const attributesSuccess = await this.createCollectionAttributesWithStatusCheck(
326
+ this.targetDatabases,
327
+ dbId,
328
+ targetCollection,
329
+ attributesToCreate
330
+ );
331
+
332
+ if (!attributesSuccess) {
333
+ MessageFormatter.error(`Failed to create some attributes for collection ${collection.name}`, undefined, { prefix: "Transfer" });
334
+ // Continue with the transfer even if some attributes failed
335
+ } else {
336
+ MessageFormatter.success(`All attributes created successfully for collection ${collection.name}`, { prefix: "Transfer" });
337
+ }
338
+
339
+ // Handle indexes with enhanced status checking
340
+ MessageFormatter.info(`Creating indexes for collection ${collection.name} with enhanced monitoring...`, { prefix: "Transfer" });
341
+
342
+ const indexesSuccess = await this.createCollectionIndexesWithStatusCheck(
343
+ dbId,
344
+ this.targetDatabases,
345
+ targetCollection.$id,
346
+ targetCollection,
347
+ collection.indexes as any
348
+ );
349
+
350
+ if (!indexesSuccess) {
351
+ MessageFormatter.error(`Failed to create some indexes for collection ${collection.name}`, undefined, { prefix: "Transfer" });
352
+ // Continue with the transfer even if some indexes failed
353
+ } else {
354
+ MessageFormatter.success(`All indexes created successfully for collection ${collection.name}`, { prefix: "Transfer" });
355
+ }
356
+
357
+ MessageFormatter.success(`Structure complete for collection ${collection.name}`, { prefix: "Transfer" });
358
+ } catch (error) {
359
+ MessageFormatter.error(`Error processing collection ${collection.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
360
+ }
361
+ }
362
+ } catch (error) {
363
+ MessageFormatter.error(`Failed to create database structure for ${dbId}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
364
+ throw error;
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Phase 2: Transfer documents to all collections in the database
370
+ */
371
+ private async transferDatabaseDocuments(dbId: string): Promise<void> {
372
+ MessageFormatter.info(`Transferring documents for database ${dbId}`, { prefix: "Transfer" });
373
+
374
+ try {
375
+ // Get all collections from source database
376
+ const sourceCollections = await this.fetchAllCollections(dbId, this.sourceDatabases);
377
+ MessageFormatter.info(`Transferring documents for ${sourceCollections.length} collections in database ${dbId}`, { prefix: "Transfer" });
378
+
379
+ // Process each collection
380
+ for (const collection of sourceCollections) {
381
+ MessageFormatter.info(`Transferring documents for collection: ${collection.name} (${collection.$id})`, { prefix: "Transfer" });
382
+
383
+ try {
384
+ // Transfer documents
385
+ await this.transferDocumentsBetweenDatabases(
386
+ this.sourceDatabases,
387
+ this.targetDatabases,
388
+ dbId,
389
+ dbId,
390
+ collection.$id,
391
+ collection.$id
392
+ );
393
+
394
+ MessageFormatter.success(`Documents transferred for collection ${collection.name}`, { prefix: "Transfer" });
395
+ } catch (error) {
396
+ MessageFormatter.error(`Error transferring documents for collection ${collection.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
397
+ }
398
+ }
399
+ } catch (error) {
400
+ MessageFormatter.error(`Failed to transfer documents for database ${dbId}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
401
+ throw error;
402
+ }
403
+ }
404
+
245
405
  private async transferAllBuckets(): Promise<void> {
246
406
  MessageFormatter.info("Starting bucket transfer phase", { prefix: "Transfer" });
247
407
 
@@ -483,6 +643,180 @@ export class ComprehensiveTransfer {
483
643
  }
484
644
  }
485
645
 
646
+ /**
647
+ * Helper method to fetch all collections from a database
648
+ */
649
+ private async fetchAllCollections(dbId: string, databases: Databases): Promise<Models.Collection[]> {
650
+ const collections: Models.Collection[] = [];
651
+ let lastId: string | undefined;
652
+
653
+ while (true) {
654
+ const queries = [Query.limit(100)];
655
+ if (lastId) {
656
+ queries.push(Query.cursorAfter(lastId));
657
+ }
658
+
659
+ const result = await tryAwaitWithRetry(async () => databases.listCollections(dbId, queries));
660
+
661
+ if (result.collections.length === 0) {
662
+ break;
663
+ }
664
+
665
+ collections.push(...result.collections);
666
+
667
+ if (result.collections.length < 100) {
668
+ break;
669
+ }
670
+
671
+ lastId = result.collections[result.collections.length - 1].$id;
672
+ }
673
+
674
+ return collections;
675
+ }
676
+
677
+ /**
678
+ * Helper method to parse attribute objects (simplified version of parseAttribute)
679
+ */
680
+ private parseAttribute(attr: any): any {
681
+ // This is a simplified version - in production you'd use the actual parseAttribute from appwrite-utils
682
+ return {
683
+ key: attr.key,
684
+ type: attr.type,
685
+ size: attr.size,
686
+ required: attr.required,
687
+ array: attr.array,
688
+ default: attr.default,
689
+ format: attr.format,
690
+ elements: attr.elements,
691
+ min: attr.min,
692
+ max: attr.max,
693
+ relatedCollection: attr.relatedCollection,
694
+ relationType: attr.relationType,
695
+ twoWay: attr.twoWay,
696
+ twoWayKey: attr.twoWayKey,
697
+ onDelete: attr.onDelete,
698
+ side: attr.side
699
+ };
700
+ }
701
+
702
+ /**
703
+ * Helper method to create collection attributes with status checking
704
+ */
705
+ private async createCollectionAttributesWithStatusCheck(
706
+ databases: Databases,
707
+ dbId: string,
708
+ collection: Models.Collection,
709
+ attributes: any[]
710
+ ): Promise<boolean> {
711
+ // Import the enhanced attribute creation function
712
+ const { createUpdateCollectionAttributesWithStatusCheck } = await import("../collections/attributes.js");
713
+
714
+ return await createUpdateCollectionAttributesWithStatusCheck(
715
+ databases,
716
+ dbId,
717
+ collection,
718
+ attributes
719
+ );
720
+ }
721
+
722
+ /**
723
+ * Helper method to create collection indexes with status checking
724
+ */
725
+ private async createCollectionIndexesWithStatusCheck(
726
+ dbId: string,
727
+ databases: Databases,
728
+ collectionId: string,
729
+ collection: Models.Collection,
730
+ indexes: any[]
731
+ ): Promise<boolean> {
732
+ // Import the enhanced index creation function
733
+ const { createOrUpdateIndexesWithStatusCheck } = await import("../collections/indexes.js");
734
+
735
+ return await createOrUpdateIndexesWithStatusCheck(
736
+ dbId,
737
+ databases,
738
+ collectionId,
739
+ collection,
740
+ indexes
741
+ );
742
+ }
743
+
744
+ /**
745
+ * Helper method to transfer documents between databases
746
+ */
747
+ private async transferDocumentsBetweenDatabases(
748
+ sourceDb: Databases,
749
+ targetDb: Databases,
750
+ sourceDbId: string,
751
+ targetDbId: string,
752
+ sourceCollectionId: string,
753
+ targetCollectionId: string
754
+ ): Promise<void> {
755
+ MessageFormatter.info(`Transferring documents from ${sourceCollectionId} to ${targetCollectionId}`, { prefix: "Transfer" });
756
+
757
+ let lastId: string | undefined;
758
+ let totalTransferred = 0;
759
+
760
+ while (true) {
761
+ const queries = [Query.limit(50)]; // Smaller batch size for better performance
762
+ if (lastId) {
763
+ queries.push(Query.cursorAfter(lastId));
764
+ }
765
+
766
+ const documents = await tryAwaitWithRetry(async () =>
767
+ sourceDb.listDocuments(sourceDbId, sourceCollectionId, queries)
768
+ );
769
+
770
+ if (documents.documents.length === 0) {
771
+ break;
772
+ }
773
+
774
+ // Transfer documents with rate limiting
775
+ const transferTasks = documents.documents.map(doc =>
776
+ this.limit(async () => {
777
+ try {
778
+ // Check if document already exists
779
+ try {
780
+ await targetDb.getDocument(targetDbId, targetCollectionId, doc.$id);
781
+ MessageFormatter.info(`Document ${doc.$id} already exists, skipping`, { prefix: "Transfer" });
782
+ return;
783
+ } catch (error) {
784
+ // Document doesn't exist, proceed with creation
785
+ }
786
+
787
+ // Create document in target
788
+ const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
789
+
790
+ await tryAwaitWithRetry(async () =>
791
+ targetDb.createDocument(
792
+ targetDbId,
793
+ targetCollectionId,
794
+ doc.$id,
795
+ docData,
796
+ doc.$permissions
797
+ )
798
+ );
799
+
800
+ totalTransferred++;
801
+ MessageFormatter.success(`Transferred document ${doc.$id}`, { prefix: "Transfer" });
802
+ } catch (error) {
803
+ MessageFormatter.error(`Failed to transfer document ${doc.$id}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
804
+ }
805
+ })
806
+ );
807
+
808
+ await Promise.all(transferTasks);
809
+
810
+ if (documents.documents.length < 50) {
811
+ break;
812
+ }
813
+
814
+ lastId = documents.documents[documents.documents.length - 1].$id;
815
+ }
816
+
817
+ MessageFormatter.info(`Transferred ${totalTransferred} documents from ${sourceCollectionId} to ${targetCollectionId}`, { prefix: "Transfer" });
818
+ }
819
+
486
820
  private printSummary(): void {
487
821
  const duration = Math.round((Date.now() - this.startTime) / 1000);
488
822
 
@@ -13,6 +13,7 @@ import { getAppwriteClient } from "../utils/helperFunctions.js";
13
13
  import {
14
14
  createOrUpdateAttribute,
15
15
  createUpdateCollectionAttributes,
16
+ createUpdateCollectionAttributesWithStatusCheck,
16
17
  } from "../collections/attributes.js";
17
18
  import { parseAttribute } from "appwrite-utils";
18
19
  import chalk from "chalk";
@@ -22,6 +23,7 @@ import { ProgressManager } from "../shared/progressManager.js";
22
23
  import {
23
24
  createOrUpdateIndex,
24
25
  createOrUpdateIndexes,
26
+ createOrUpdateIndexesWithStatusCheck,
25
27
  } from "../collections/indexes.js";
26
28
  import { getClient } from "../utils/getClientFromConfig.js";
27
29
 
@@ -305,44 +307,23 @@ export const transferDatabaseLocalToLocal = async (
305
307
  );
306
308
  }
307
309
 
308
- // Handle attributes
309
- const existingAttributes = await tryAwaitWithRetry(
310
- async () =>
311
- await localDb.listAttributes(targetDbId, targetCollection.$id)
310
+ // Handle attributes with enhanced status checking
311
+ console.log(chalk.blue(`Creating attributes for collection ${collection.name} with enhanced monitoring...`));
312
+
313
+ const allAttributes = collection.attributes.map(attr => parseAttribute(attr as any));
314
+ const attributeSuccess = await createUpdateCollectionAttributesWithStatusCheck(
315
+ localDb,
316
+ targetDbId,
317
+ targetCollection,
318
+ allAttributes
312
319
  );
313
-
314
- for (const attribute of collection.attributes) {
315
- const parsedAttribute = parseAttribute(attribute as any);
316
- const existingAttribute = existingAttributes.attributes.find(
317
- (attr: any) => attr.key === parsedAttribute.key
318
- );
319
-
320
- if (!existingAttribute) {
321
- await tryAwaitWithRetry(async () =>
322
- createOrUpdateAttribute(
323
- localDb,
324
- targetDbId,
325
- targetCollection,
326
- parsedAttribute
327
- )
328
- );
329
- console.log(chalk.green(`Attribute ${parsedAttribute.key} created`));
330
- } else {
331
- console.log(
332
- chalk.blue(
333
- `Attribute ${parsedAttribute.key} exists, checking for updates...`
334
- )
335
- );
336
- await tryAwaitWithRetry(async () =>
337
- createOrUpdateAttribute(
338
- localDb,
339
- targetDbId,
340
- targetCollection,
341
- parsedAttribute
342
- )
343
- );
344
- }
320
+
321
+ if (!attributeSuccess) {
322
+ console.log(chalk.red(`❌ Failed to create all attributes for collection ${collection.name}, skipping to next collection`));
323
+ continue;
345
324
  }
325
+
326
+ console.log(chalk.green(`✅ All attributes created successfully for collection ${collection.name}`));
346
327
 
347
328
  // Handle indexes
348
329
  const existingIndexes = await tryAwaitWithRetry(
@@ -474,73 +455,41 @@ export const transferDatabaseLocalToRemote = async (
474
455
  );
475
456
  }
476
457
 
477
- // Handle attributes
478
- const existingAttributes = await tryAwaitWithRetry(
479
- async () => await remoteDb.listAttributes(toDbId, targetCollection.$id)
458
+ // Handle attributes with enhanced status checking
459
+ console.log(chalk.blue(`Creating attributes for collection ${collection.name} with enhanced monitoring...`));
460
+
461
+ const attributesToCreate = collection.attributes.map(attr => parseAttribute(attr as any));
462
+
463
+ const attributesSuccess = await createUpdateCollectionAttributesWithStatusCheck(
464
+ remoteDb,
465
+ toDbId,
466
+ targetCollection,
467
+ attributesToCreate
480
468
  );
481
-
482
- for (const attribute of collection.attributes) {
483
- const parsedAttribute = parseAttribute(attribute as any);
484
- const existingAttribute = existingAttributes.attributes.find(
485
- (attr: any) => attr.key === parsedAttribute.key
486
- );
487
-
488
- if (!existingAttribute) {
489
- await tryAwaitWithRetry(async () =>
490
- createOrUpdateAttribute(
491
- remoteDb,
492
- toDbId,
493
- targetCollection,
494
- parsedAttribute
495
- )
496
- );
497
- console.log(chalk.green(`Attribute ${parsedAttribute.key} created`));
498
- } else {
499
- console.log(
500
- chalk.blue(
501
- `Attribute ${parsedAttribute.key} exists, checking for updates...`
502
- )
503
- );
504
- await tryAwaitWithRetry(async () =>
505
- createOrUpdateAttribute(
506
- remoteDb,
507
- toDbId,
508
- targetCollection,
509
- parsedAttribute
510
- )
511
- );
512
- }
469
+
470
+ if (!attributesSuccess) {
471
+ console.log(chalk.red(`Failed to create some attributes for collection ${collection.name}`));
472
+ // Continue with the transfer even if some attributes failed
473
+ } else {
474
+ console.log(chalk.green(`All attributes created successfully for collection ${collection.name}`));
513
475
  }
514
476
 
515
- // Handle indexes
516
- const existingIndexes = await tryAwaitWithRetry(
517
- async () => await remoteDb.listIndexes(toDbId, targetCollection.$id)
477
+ // Handle indexes with enhanced status checking
478
+ console.log(chalk.blue(`Creating indexes for collection ${collection.name} with enhanced monitoring...`));
479
+
480
+ const indexesSuccess = await createOrUpdateIndexesWithStatusCheck(
481
+ toDbId,
482
+ remoteDb,
483
+ targetCollection.$id,
484
+ targetCollection,
485
+ collection.indexes as any
518
486
  );
519
-
520
- for (const index of collection.indexes) {
521
- const existingIndex = existingIndexes.indexes.find(
522
- (idx) => idx.key === index.key
523
- );
524
-
525
- if (!existingIndex) {
526
- await createOrUpdateIndex(
527
- toDbId,
528
- remoteDb,
529
- targetCollection.$id,
530
- index as any
531
- );
532
- console.log(chalk.green(`Index ${index.key} created`));
533
- } else {
534
- console.log(
535
- chalk.blue(`Index ${index.key} exists, checking for updates...`)
536
- );
537
- await createOrUpdateIndex(
538
- toDbId,
539
- remoteDb,
540
- targetCollection.$id,
541
- index as any
542
- );
543
- }
487
+
488
+ if (!indexesSuccess) {
489
+ console.log(chalk.red(`Failed to create some indexes for collection ${collection.name}`));
490
+ // Continue with the transfer even if some indexes failed
491
+ } else {
492
+ console.log(chalk.green(`All indexes created successfully for collection ${collection.name}`));
544
493
  }
545
494
 
546
495
  // Transfer documents