appwrite-utils-cli 1.1.0 → 1.1.2

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.
@@ -164,16 +164,38 @@ export const createOrUpdateAttributeWithStatusCheck = async (db, dbId, collectio
164
164
  if (success) {
165
165
  return true;
166
166
  }
167
- // If not successful and we have retries left, delete collection and try again
167
+ // If not successful and we have retries left, delete specific attribute and try again
168
168
  if (retryCount < maxRetries) {
169
- console.log(chalk.yellow(`Attribute '${attribute.key}' failed/stuck, retrying...`));
170
- // Get fresh collection data
171
- const freshCollection = await db.getCollection(dbId, collection.$id);
172
- // Delete and recreate collection
173
- const newCollection = await deleteAndRecreateCollection(db, dbId, freshCollection, retryCount + 1);
174
- if (newCollection) {
175
- // Retry with the new collection
176
- return await createOrUpdateAttributeWithStatusCheck(db, dbId, newCollection, attribute, retryCount + 1, maxRetries);
169
+ console.log(chalk.yellow(`Attribute '${attribute.key}' failed/stuck, deleting and retrying...`));
170
+ // Try to delete the specific stuck attribute instead of the entire collection
171
+ try {
172
+ await db.deleteAttribute(dbId, collection.$id, attribute.key);
173
+ console.log(chalk.yellow(`Deleted stuck attribute '${attribute.key}', will retry creation`));
174
+ // Wait a bit before retry
175
+ await delay(3000);
176
+ // Get fresh collection data
177
+ const freshCollection = await db.getCollection(dbId, collection.$id);
178
+ // Retry with the same collection (attribute should be gone now)
179
+ return await createOrUpdateAttributeWithStatusCheck(db, dbId, freshCollection, attribute, retryCount + 1, maxRetries);
180
+ }
181
+ catch (deleteError) {
182
+ console.log(chalk.red(`Failed to delete stuck attribute '${attribute.key}': ${deleteError}`));
183
+ // If attribute deletion fails, only then try collection recreation as last resort
184
+ if (retryCount >= maxRetries - 1) {
185
+ console.log(chalk.yellow(`Last resort: Recreating collection for attribute '${attribute.key}'`));
186
+ // Get fresh collection data
187
+ const freshCollection = await db.getCollection(dbId, collection.$id);
188
+ // Delete and recreate collection
189
+ const newCollection = await deleteAndRecreateCollection(db, dbId, freshCollection, retryCount + 1);
190
+ if (newCollection) {
191
+ // Retry with the new collection
192
+ return await createOrUpdateAttributeWithStatusCheck(db, dbId, newCollection, attribute, retryCount + 1, maxRetries);
193
+ }
194
+ }
195
+ else {
196
+ // Continue to next retry without collection recreation
197
+ return await createOrUpdateAttributeWithStatusCheck(db, dbId, collection, attribute, retryCount + 1, maxRetries);
198
+ }
177
199
  }
178
200
  }
179
201
  console.log(chalk.red(`❌ Failed to create attribute '${attribute.key}' after ${maxRetries + 1} attempts`));
@@ -437,32 +459,58 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId,
437
459
  await delay(500); // Longer delay for deletions
438
460
  }
439
461
  }
440
- // Create attributes ONE BY ONE with proper status checking
462
+ // Create attributes ONE BY ONE with proper status checking and persistent retry logic
441
463
  console.log(chalk.blue(`Creating ${attributes.length} attributes sequentially with status monitoring...`));
442
464
  let currentCollection = collection;
443
- const failedAttributes = [];
444
- for (const attribute of attributes) {
445
- console.log(chalk.blue(`\n--- Processing attribute: ${attribute.key} ---`));
446
- const success = await createOrUpdateAttributeWithStatusCheck(db, dbId, currentCollection, attribute);
447
- if (success) {
448
- console.log(chalk.green(`✅ Successfully created attribute: ${attribute.key}`));
449
- // Get updated collection data for next iteration
465
+ let attributesToProcess = [...attributes];
466
+ let overallRetryCount = 0;
467
+ const maxOverallRetries = 3;
468
+ while (attributesToProcess.length > 0 && overallRetryCount < maxOverallRetries) {
469
+ const remainingAttributes = [...attributesToProcess];
470
+ attributesToProcess = []; // Reset for next iteration
471
+ console.log(chalk.blue(`\n=== Attempt ${overallRetryCount + 1}/${maxOverallRetries} - Processing ${remainingAttributes.length} attributes ===`));
472
+ for (const attribute of remainingAttributes) {
473
+ console.log(chalk.blue(`\n--- Processing attribute: ${attribute.key} ---`));
474
+ const success = await createOrUpdateAttributeWithStatusCheck(db, dbId, currentCollection, attribute);
475
+ if (success) {
476
+ console.log(chalk.green(`✅ Successfully created attribute: ${attribute.key}`));
477
+ // Get updated collection data for next iteration
478
+ try {
479
+ currentCollection = await db.getCollection(dbId, collection.$id);
480
+ }
481
+ catch (error) {
482
+ console.log(chalk.yellow(`Warning: Could not refresh collection data: ${error}`));
483
+ }
484
+ // Add delay between successful attributes
485
+ await delay(1000);
486
+ }
487
+ else {
488
+ console.log(chalk.red(`❌ Failed to create attribute: ${attribute.key}, will retry in next round`));
489
+ attributesToProcess.push(attribute); // Add back to retry list
490
+ }
491
+ }
492
+ if (attributesToProcess.length === 0) {
493
+ console.log(chalk.green(`\n✅ Successfully created all ${attributes.length} attributes for collection: ${collection.name}`));
494
+ return true;
495
+ }
496
+ overallRetryCount++;
497
+ if (overallRetryCount < maxOverallRetries) {
498
+ console.log(chalk.yellow(`\n⏳ Waiting 5 seconds before retrying ${attributesToProcess.length} failed attributes...`));
499
+ await delay(5000);
500
+ // Refresh collection data before retry
450
501
  try {
451
502
  currentCollection = await db.getCollection(dbId, collection.$id);
503
+ console.log(chalk.blue(`Refreshed collection data for retry`));
452
504
  }
453
505
  catch (error) {
454
- console.log(chalk.yellow(`Warning: Could not refresh collection data: ${error}`));
506
+ console.log(chalk.yellow(`Warning: Could not refresh collection data for retry: ${error}`));
455
507
  }
456
- // Add delay between successful attributes
457
- await delay(1000);
458
- }
459
- else {
460
- console.log(chalk.red(`❌ Failed to create attribute: ${attribute.key}`));
461
- failedAttributes.push(attribute.key);
462
508
  }
463
509
  }
464
- if (failedAttributes.length > 0) {
465
- console.log(chalk.red(`\n❌ Failed to create ${failedAttributes.length} attributes: ${failedAttributes.join(', ')}`));
510
+ // If we get here, some attributes still failed after all retries
511
+ if (attributesToProcess.length > 0) {
512
+ console.log(chalk.red(`\n❌ Failed to create ${attributesToProcess.length} attributes after ${maxOverallRetries} attempts: ${attributesToProcess.map(a => a.key).join(', ')}`));
513
+ console.log(chalk.red(`This may indicate a fundamental issue with the attribute definitions or Appwrite instance`));
466
514
  return false;
467
515
  }
468
516
  console.log(chalk.green(`\n✅ Successfully created all ${attributes.length} attributes for collection: ${collection.name}`));
@@ -130,22 +130,40 @@ export const createOrUpdateIndexWithStatusCheck = async (dbId, db, collectionId,
130
130
  */
131
131
  export const createOrUpdateIndexesWithStatusCheck = async (dbId, db, collectionId, collection, indexes) => {
132
132
  console.log(chalk.blue(`Creating/updating ${indexes.length} indexes with status monitoring...`));
133
- const failedIndexes = [];
134
- for (const index of indexes) {
135
- console.log(chalk.blue(`\n--- Processing index: ${index.key} ---`));
136
- const success = await createOrUpdateIndexWithStatusCheck(dbId, db, collectionId, collection, index);
137
- if (success) {
138
- console.log(chalk.green(`✅ Successfully created index: ${index.key}`));
139
- // Add delay between successful indexes
140
- await delay(1000);
133
+ let indexesToProcess = [...indexes];
134
+ let overallRetryCount = 0;
135
+ const maxOverallRetries = 3;
136
+ while (indexesToProcess.length > 0 && overallRetryCount < maxOverallRetries) {
137
+ const remainingIndexes = [...indexesToProcess];
138
+ indexesToProcess = []; // Reset for next iteration
139
+ console.log(chalk.blue(`\n=== Attempt ${overallRetryCount + 1}/${maxOverallRetries} - Processing ${remainingIndexes.length} indexes ===`));
140
+ for (const index of remainingIndexes) {
141
+ console.log(chalk.blue(`\n--- Processing index: ${index.key} ---`));
142
+ const success = await createOrUpdateIndexWithStatusCheck(dbId, db, collectionId, collection, index);
143
+ if (success) {
144
+ console.log(chalk.green(`✅ Successfully created index: ${index.key}`));
145
+ // Add delay between successful indexes
146
+ await delay(1000);
147
+ }
148
+ else {
149
+ console.log(chalk.red(`❌ Failed to create index: ${index.key}, will retry in next round`));
150
+ indexesToProcess.push(index); // Add back to retry list
151
+ }
152
+ }
153
+ if (indexesToProcess.length === 0) {
154
+ console.log(chalk.green(`\n✅ Successfully created all ${indexes.length} indexes`));
155
+ return true;
141
156
  }
142
- else {
143
- console.log(chalk.red(`❌ Failed to create index: ${index.key}`));
144
- failedIndexes.push(index.key);
157
+ overallRetryCount++;
158
+ if (overallRetryCount < maxOverallRetries) {
159
+ console.log(chalk.yellow(`\n⏳ Waiting 5 seconds before retrying ${indexesToProcess.length} failed indexes...`));
160
+ await delay(5000);
145
161
  }
146
162
  }
147
- if (failedIndexes.length > 0) {
148
- console.log(chalk.red(`\n❌ Failed to create ${failedIndexes.length} indexes: ${failedIndexes.join(', ')}`));
163
+ // If we get here, some indexes still failed after all retries
164
+ if (indexesToProcess.length > 0) {
165
+ console.log(chalk.red(`\n❌ Failed to create ${indexesToProcess.length} indexes after ${maxOverallRetries} attempts: ${indexesToProcess.map(i => i.key).join(', ')}`));
166
+ console.log(chalk.red(`This may indicate a fundamental issue with the index definitions or Appwrite instance`));
149
167
  return false;
150
168
  }
151
169
  console.log(chalk.green(`\n✅ Successfully created all ${indexes.length} indexes`));
@@ -1516,6 +1516,8 @@ export class InteractiveCLI {
1516
1516
  async comprehensiveTransfer() {
1517
1517
  MessageFormatter.info("Starting comprehensive transfer configuration...", { prefix: "Transfer" });
1518
1518
  try {
1519
+ // Initialize controller to optionally load config if available (supports both YAML and TypeScript configs)
1520
+ await this.initControllerIfNeeded();
1519
1521
  // Check if user has an appwrite config for easier setup
1520
1522
  const hasAppwriteConfig = this.controller?.config?.appwriteEndpoint &&
1521
1523
  this.controller?.config?.appwriteProject &&
@@ -57,10 +57,38 @@ export declare class ComprehensiveTransfer {
57
57
  execute(): Promise<TransferResults>;
58
58
  private transferAllUsers;
59
59
  private transferAllDatabases;
60
+ /**
61
+ * Phase 1: Create database structure (collections, attributes, indexes) without transferring documents
62
+ */
63
+ private createDatabaseStructure;
64
+ /**
65
+ * Phase 2: Transfer documents to all collections in the database
66
+ */
67
+ private transferDatabaseDocuments;
60
68
  private transferAllBuckets;
61
69
  private transferBucketFiles;
62
70
  private validateAndDownloadFile;
63
71
  private transferAllFunctions;
64
72
  private downloadFunction;
73
+ /**
74
+ * Helper method to fetch all collections from a database
75
+ */
76
+ private fetchAllCollections;
77
+ /**
78
+ * Helper method to parse attribute objects (simplified version of parseAttribute)
79
+ */
80
+ private parseAttribute;
81
+ /**
82
+ * Helper method to create collection attributes with status checking
83
+ */
84
+ private createCollectionAttributesWithStatusCheck;
85
+ /**
86
+ * Helper method to create collection indexes with status checking
87
+ */
88
+ private createCollectionIndexesWithStatusCheck;
89
+ /**
90
+ * Helper method to transfer documents between databases
91
+ */
92
+ private transferDocumentsBetweenDatabases;
65
93
  private printSummary;
66
94
  }
@@ -1,4 +1,4 @@
1
- import { converterFunctions, tryAwaitWithRetry } from "appwrite-utils";
1
+ import { converterFunctions, tryAwaitWithRetry, parseAttribute } from "appwrite-utils";
2
2
  import { Client, Databases, Storage, Users, Functions, Query, } from "node-appwrite";
3
3
  import { InputFile } from "node-appwrite/file";
4
4
  import { MessageFormatter } from "../shared/messageFormatter.js";
@@ -132,7 +132,9 @@ export class ComprehensiveTransfer {
132
132
  MessageFormatter.info(`DRY RUN: Would transfer ${sourceDatabases.databases.length} databases`, { prefix: "Transfer" });
133
133
  return;
134
134
  }
135
- const transferTasks = sourceDatabases.databases.map(db => this.limit(async () => {
135
+ // Phase 1: Create all databases and collections (structure only)
136
+ MessageFormatter.info("Phase 1: Creating database structures (databases, collections, attributes, indexes)", { prefix: "Transfer" });
137
+ const structureCreationTasks = sourceDatabases.databases.map(db => this.limit(async () => {
136
138
  try {
137
139
  // Check if database exists in target
138
140
  const existingDb = targetDatabases.databases.find(tdb => tdb.$id === db.$id);
@@ -141,23 +143,132 @@ export class ComprehensiveTransfer {
141
143
  await this.targetDatabases.create(db.$id, db.name, db.enabled);
142
144
  MessageFormatter.success(`Created database: ${db.name}`, { prefix: "Transfer" });
143
145
  }
144
- // Transfer database content
145
- await transferDatabaseLocalToRemote(this.sourceDatabases, this.options.targetEndpoint, this.options.targetProject, this.options.targetKey, db.$id, db.$id);
146
+ // Create collections, attributes, and indexes WITHOUT transferring documents
147
+ await this.createDatabaseStructure(db.$id);
148
+ MessageFormatter.success(`Database structure created: ${db.name}`, { prefix: "Transfer" });
149
+ }
150
+ catch (error) {
151
+ MessageFormatter.error(`Database structure creation failed for ${db.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
152
+ this.results.databases.failed++;
153
+ }
154
+ }));
155
+ await Promise.all(structureCreationTasks);
156
+ // Phase 2: Transfer all documents after all structures are created
157
+ MessageFormatter.info("Phase 2: Transferring documents to all collections", { prefix: "Transfer" });
158
+ const documentTransferTasks = sourceDatabases.databases.map(db => this.limit(async () => {
159
+ try {
160
+ // Transfer documents for this database
161
+ await this.transferDatabaseDocuments(db.$id);
146
162
  this.results.databases.transferred++;
147
- MessageFormatter.success(`Database ${db.name} transferred successfully`, { prefix: "Transfer" });
163
+ MessageFormatter.success(`Database documents transferred: ${db.name}`, { prefix: "Transfer" });
148
164
  }
149
165
  catch (error) {
150
- MessageFormatter.error(`Database ${db.name} transfer failed`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
166
+ MessageFormatter.error(`Document transfer failed for ${db.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
151
167
  this.results.databases.failed++;
152
168
  }
153
169
  }));
154
- await Promise.all(transferTasks);
170
+ await Promise.all(documentTransferTasks);
155
171
  MessageFormatter.success("Database transfer phase completed", { prefix: "Transfer" });
156
172
  }
157
173
  catch (error) {
158
174
  MessageFormatter.error("Database transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
159
175
  }
160
176
  }
177
+ /**
178
+ * Phase 1: Create database structure (collections, attributes, indexes) without transferring documents
179
+ */
180
+ async createDatabaseStructure(dbId) {
181
+ MessageFormatter.info(`Creating database structure for ${dbId}`, { prefix: "Transfer" });
182
+ try {
183
+ // Get all collections from source database
184
+ const sourceCollections = await this.fetchAllCollections(dbId, this.sourceDatabases);
185
+ MessageFormatter.info(`Found ${sourceCollections.length} collections in source database ${dbId}`, { prefix: "Transfer" });
186
+ // Process each collection
187
+ for (const collection of sourceCollections) {
188
+ MessageFormatter.info(`Processing collection: ${collection.name} (${collection.$id})`, { prefix: "Transfer" });
189
+ try {
190
+ // Create or update collection in target
191
+ let targetCollection;
192
+ const existingCollection = await tryAwaitWithRetry(async () => this.targetDatabases.listCollections(dbId, [Query.equal("$id", collection.$id)]));
193
+ if (existingCollection.collections.length > 0) {
194
+ targetCollection = existingCollection.collections[0];
195
+ MessageFormatter.info(`Collection ${collection.name} exists in target database`, { prefix: "Transfer" });
196
+ // Update collection if needed
197
+ if (targetCollection.name !== collection.name ||
198
+ JSON.stringify(targetCollection.$permissions) !== JSON.stringify(collection.$permissions) ||
199
+ targetCollection.documentSecurity !== collection.documentSecurity ||
200
+ targetCollection.enabled !== collection.enabled) {
201
+ targetCollection = await tryAwaitWithRetry(async () => this.targetDatabases.updateCollection(dbId, collection.$id, collection.name, collection.$permissions, collection.documentSecurity, collection.enabled));
202
+ MessageFormatter.success(`Collection ${collection.name} updated`, { prefix: "Transfer" });
203
+ }
204
+ }
205
+ else {
206
+ MessageFormatter.info(`Creating collection ${collection.name} in target database...`, { prefix: "Transfer" });
207
+ targetCollection = await tryAwaitWithRetry(async () => this.targetDatabases.createCollection(dbId, collection.$id, collection.name, collection.$permissions, collection.documentSecurity, collection.enabled));
208
+ MessageFormatter.success(`Collection ${collection.name} created`, { prefix: "Transfer" });
209
+ }
210
+ // Handle attributes with enhanced status checking
211
+ MessageFormatter.info(`Creating attributes for collection ${collection.name} with enhanced monitoring...`, { prefix: "Transfer" });
212
+ const attributesToCreate = collection.attributes.map(attr => parseAttribute(attr));
213
+ const attributesSuccess = await this.createCollectionAttributesWithStatusCheck(this.targetDatabases, dbId, targetCollection, attributesToCreate);
214
+ if (!attributesSuccess) {
215
+ MessageFormatter.error(`Failed to create some attributes for collection ${collection.name}`, undefined, { prefix: "Transfer" });
216
+ MessageFormatter.error(`Skipping index creation and document transfer for collection ${collection.name} due to attribute failures`, undefined, { prefix: "Transfer" });
217
+ // Skip indexes and document transfer if attributes failed
218
+ continue;
219
+ }
220
+ else {
221
+ MessageFormatter.success(`All attributes created successfully for collection ${collection.name}`, { prefix: "Transfer" });
222
+ }
223
+ // Handle indexes with enhanced status checking
224
+ MessageFormatter.info(`Creating indexes for collection ${collection.name} with enhanced monitoring...`, { prefix: "Transfer" });
225
+ const indexesSuccess = await this.createCollectionIndexesWithStatusCheck(dbId, this.targetDatabases, targetCollection.$id, targetCollection, collection.indexes);
226
+ if (!indexesSuccess) {
227
+ MessageFormatter.error(`Failed to create some indexes for collection ${collection.name}`, undefined, { prefix: "Transfer" });
228
+ MessageFormatter.warning(`Proceeding with document transfer despite index failures for collection ${collection.name}`, { prefix: "Transfer" });
229
+ }
230
+ else {
231
+ MessageFormatter.success(`All indexes created successfully for collection ${collection.name}`, { prefix: "Transfer" });
232
+ }
233
+ MessageFormatter.success(`Structure complete for collection ${collection.name}`, { prefix: "Transfer" });
234
+ }
235
+ catch (error) {
236
+ MessageFormatter.error(`Error processing collection ${collection.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
237
+ }
238
+ }
239
+ }
240
+ catch (error) {
241
+ MessageFormatter.error(`Failed to create database structure for ${dbId}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
242
+ throw error;
243
+ }
244
+ }
245
+ /**
246
+ * Phase 2: Transfer documents to all collections in the database
247
+ */
248
+ async transferDatabaseDocuments(dbId) {
249
+ MessageFormatter.info(`Transferring documents for database ${dbId}`, { prefix: "Transfer" });
250
+ try {
251
+ // Get all collections from source database
252
+ const sourceCollections = await this.fetchAllCollections(dbId, this.sourceDatabases);
253
+ MessageFormatter.info(`Transferring documents for ${sourceCollections.length} collections in database ${dbId}`, { prefix: "Transfer" });
254
+ // Process each collection
255
+ for (const collection of sourceCollections) {
256
+ MessageFormatter.info(`Transferring documents for collection: ${collection.name} (${collection.$id})`, { prefix: "Transfer" });
257
+ try {
258
+ // Transfer documents
259
+ await this.transferDocumentsBetweenDatabases(this.sourceDatabases, this.targetDatabases, dbId, dbId, collection.$id, collection.$id);
260
+ MessageFormatter.success(`Documents transferred for collection ${collection.name}`, { prefix: "Transfer" });
261
+ }
262
+ catch (error) {
263
+ MessageFormatter.error(`Error transferring documents for collection ${collection.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
264
+ }
265
+ }
266
+ }
267
+ catch (error) {
268
+ MessageFormatter.error(`Failed to transfer documents for database ${dbId}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
269
+ throw error;
270
+ }
271
+ }
161
272
  async transferAllBuckets() {
162
273
  MessageFormatter.info("Starting bucket transfer phase", { prefix: "Transfer" });
163
274
  try {
@@ -346,6 +457,115 @@ export class ComprehensiveTransfer {
346
457
  return null;
347
458
  }
348
459
  }
460
+ /**
461
+ * Helper method to fetch all collections from a database
462
+ */
463
+ async fetchAllCollections(dbId, databases) {
464
+ const collections = [];
465
+ let lastId;
466
+ while (true) {
467
+ const queries = [Query.limit(100)];
468
+ if (lastId) {
469
+ queries.push(Query.cursorAfter(lastId));
470
+ }
471
+ const result = await tryAwaitWithRetry(async () => databases.listCollections(dbId, queries));
472
+ if (result.collections.length === 0) {
473
+ break;
474
+ }
475
+ collections.push(...result.collections);
476
+ if (result.collections.length < 100) {
477
+ break;
478
+ }
479
+ lastId = result.collections[result.collections.length - 1].$id;
480
+ }
481
+ return collections;
482
+ }
483
+ /**
484
+ * Helper method to parse attribute objects (simplified version of parseAttribute)
485
+ */
486
+ parseAttribute(attr) {
487
+ // This is a simplified version - in production you'd use the actual parseAttribute from appwrite-utils
488
+ return {
489
+ key: attr.key,
490
+ type: attr.type,
491
+ size: attr.size,
492
+ required: attr.required,
493
+ array: attr.array,
494
+ default: attr.default,
495
+ format: attr.format,
496
+ elements: attr.elements,
497
+ min: attr.min,
498
+ max: attr.max,
499
+ relatedCollection: attr.relatedCollection,
500
+ relationType: attr.relationType,
501
+ twoWay: attr.twoWay,
502
+ twoWayKey: attr.twoWayKey,
503
+ onDelete: attr.onDelete,
504
+ side: attr.side
505
+ };
506
+ }
507
+ /**
508
+ * Helper method to create collection attributes with status checking
509
+ */
510
+ async createCollectionAttributesWithStatusCheck(databases, dbId, collection, attributes) {
511
+ // Import the enhanced attribute creation function
512
+ const { createUpdateCollectionAttributesWithStatusCheck } = await import("../collections/attributes.js");
513
+ return await createUpdateCollectionAttributesWithStatusCheck(databases, dbId, collection, attributes);
514
+ }
515
+ /**
516
+ * Helper method to create collection indexes with status checking
517
+ */
518
+ async createCollectionIndexesWithStatusCheck(dbId, databases, collectionId, collection, indexes) {
519
+ // Import the enhanced index creation function
520
+ const { createOrUpdateIndexesWithStatusCheck } = await import("../collections/indexes.js");
521
+ return await createOrUpdateIndexesWithStatusCheck(dbId, databases, collectionId, collection, indexes);
522
+ }
523
+ /**
524
+ * Helper method to transfer documents between databases
525
+ */
526
+ async transferDocumentsBetweenDatabases(sourceDb, targetDb, sourceDbId, targetDbId, sourceCollectionId, targetCollectionId) {
527
+ MessageFormatter.info(`Transferring documents from ${sourceCollectionId} to ${targetCollectionId}`, { prefix: "Transfer" });
528
+ let lastId;
529
+ let totalTransferred = 0;
530
+ while (true) {
531
+ const queries = [Query.limit(50)]; // Smaller batch size for better performance
532
+ if (lastId) {
533
+ queries.push(Query.cursorAfter(lastId));
534
+ }
535
+ const documents = await tryAwaitWithRetry(async () => sourceDb.listDocuments(sourceDbId, sourceCollectionId, queries));
536
+ if (documents.documents.length === 0) {
537
+ break;
538
+ }
539
+ // Transfer documents with rate limiting
540
+ const transferTasks = documents.documents.map(doc => this.limit(async () => {
541
+ try {
542
+ // Check if document already exists
543
+ try {
544
+ await targetDb.getDocument(targetDbId, targetCollectionId, doc.$id);
545
+ MessageFormatter.info(`Document ${doc.$id} already exists, skipping`, { prefix: "Transfer" });
546
+ return;
547
+ }
548
+ catch (error) {
549
+ // Document doesn't exist, proceed with creation
550
+ }
551
+ // Create document in target
552
+ const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
553
+ await tryAwaitWithRetry(async () => targetDb.createDocument(targetDbId, targetCollectionId, doc.$id, docData, doc.$permissions));
554
+ totalTransferred++;
555
+ MessageFormatter.success(`Transferred document ${doc.$id}`, { prefix: "Transfer" });
556
+ }
557
+ catch (error) {
558
+ MessageFormatter.error(`Failed to transfer document ${doc.$id}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
559
+ }
560
+ }));
561
+ await Promise.all(transferTasks);
562
+ if (documents.documents.length < 50) {
563
+ break;
564
+ }
565
+ lastId = documents.documents[documents.documents.length - 1].$id;
566
+ }
567
+ MessageFormatter.info(`Transferred ${totalTransferred} documents from ${sourceCollectionId} to ${targetCollectionId}`, { prefix: "Transfer" });
568
+ }
349
569
  printSummary() {
350
570
  const duration = Math.round((Date.now() - this.startTime) / 1000);
351
571
  MessageFormatter.info("=== COMPREHENSIVE TRANSFER SUMMARY ===", { prefix: "Transfer" });
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.1.0",
4
+ "version": "1.1.2",
5
5
  "main": "src/main.ts",
6
6
  "type": "module",
7
7
  "repository": {
@@ -263,26 +263,65 @@ export const createOrUpdateAttributeWithStatusCheck = async (
263
263
  return true;
264
264
  }
265
265
 
266
- // If not successful and we have retries left, delete collection and try again
266
+ // If not successful and we have retries left, delete specific attribute and try again
267
267
  if (retryCount < maxRetries) {
268
- console.log(chalk.yellow(`Attribute '${attribute.key}' failed/stuck, retrying...`));
268
+ console.log(chalk.yellow(`Attribute '${attribute.key}' failed/stuck, deleting and retrying...`));
269
269
 
270
- // Get fresh collection data
271
- const freshCollection = await db.getCollection(dbId, collection.$id);
272
-
273
- // Delete and recreate collection
274
- const newCollection = await deleteAndRecreateCollection(db, dbId, freshCollection, retryCount + 1);
275
-
276
- if (newCollection) {
277
- // Retry with the new collection
270
+ // Try to delete the specific stuck attribute instead of the entire collection
271
+ try {
272
+ await db.deleteAttribute(dbId, collection.$id, attribute.key);
273
+ console.log(chalk.yellow(`Deleted stuck attribute '${attribute.key}', will retry creation`));
274
+
275
+ // Wait a bit before retry
276
+ await delay(3000);
277
+
278
+ // Get fresh collection data
279
+ const freshCollection = await db.getCollection(dbId, collection.$id);
280
+
281
+ // Retry with the same collection (attribute should be gone now)
278
282
  return await createOrUpdateAttributeWithStatusCheck(
279
283
  db,
280
284
  dbId,
281
- newCollection,
285
+ freshCollection,
282
286
  attribute,
283
287
  retryCount + 1,
284
288
  maxRetries
285
289
  );
290
+ } catch (deleteError) {
291
+ console.log(chalk.red(`Failed to delete stuck attribute '${attribute.key}': ${deleteError}`));
292
+
293
+ // If attribute deletion fails, only then try collection recreation as last resort
294
+ if (retryCount >= maxRetries - 1) {
295
+ console.log(chalk.yellow(`Last resort: Recreating collection for attribute '${attribute.key}'`));
296
+
297
+ // Get fresh collection data
298
+ const freshCollection = await db.getCollection(dbId, collection.$id);
299
+
300
+ // Delete and recreate collection
301
+ const newCollection = await deleteAndRecreateCollection(db, dbId, freshCollection, retryCount + 1);
302
+
303
+ if (newCollection) {
304
+ // Retry with the new collection
305
+ return await createOrUpdateAttributeWithStatusCheck(
306
+ db,
307
+ dbId,
308
+ newCollection,
309
+ attribute,
310
+ retryCount + 1,
311
+ maxRetries
312
+ );
313
+ }
314
+ } else {
315
+ // Continue to next retry without collection recreation
316
+ return await createOrUpdateAttributeWithStatusCheck(
317
+ db,
318
+ dbId,
319
+ collection,
320
+ attribute,
321
+ retryCount + 1,
322
+ maxRetries
323
+ );
324
+ }
286
325
  }
287
326
  }
288
327
 
@@ -810,42 +849,73 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
810
849
  }
811
850
  }
812
851
 
813
- // Create attributes ONE BY ONE with proper status checking
852
+ // Create attributes ONE BY ONE with proper status checking and persistent retry logic
814
853
  console.log(chalk.blue(`Creating ${attributes.length} attributes sequentially with status monitoring...`));
815
854
 
816
855
  let currentCollection = collection;
817
- const failedAttributes: string[] = [];
856
+ let attributesToProcess = [...attributes];
857
+ let overallRetryCount = 0;
858
+ const maxOverallRetries = 3;
818
859
 
819
- for (const attribute of attributes) {
820
- console.log(chalk.blue(`\n--- Processing attribute: ${attribute.key} ---`));
860
+ while (attributesToProcess.length > 0 && overallRetryCount < maxOverallRetries) {
861
+ const remainingAttributes = [...attributesToProcess];
862
+ attributesToProcess = []; // Reset for next iteration
821
863
 
822
- const success = await createOrUpdateAttributeWithStatusCheck(
823
- db,
824
- dbId,
825
- currentCollection,
826
- attribute
827
- );
864
+ console.log(chalk.blue(`\n=== Attempt ${overallRetryCount + 1}/${maxOverallRetries} - Processing ${remainingAttributes.length} attributes ===`));
828
865
 
829
- if (success) {
830
- console.log(chalk.green(`✅ Successfully created attribute: ${attribute.key}`));
866
+ for (const attribute of remainingAttributes) {
867
+ console.log(chalk.blue(`\n--- Processing attribute: ${attribute.key} ---`));
868
+
869
+ const success = await createOrUpdateAttributeWithStatusCheck(
870
+ db,
871
+ dbId,
872
+ currentCollection,
873
+ attribute
874
+ );
875
+
876
+ if (success) {
877
+ console.log(chalk.green(`✅ Successfully created attribute: ${attribute.key}`));
878
+
879
+ // Get updated collection data for next iteration
880
+ try {
881
+ currentCollection = await db.getCollection(dbId, collection.$id);
882
+ } catch (error) {
883
+ console.log(chalk.yellow(`Warning: Could not refresh collection data: ${error}`));
884
+ }
885
+
886
+ // Add delay between successful attributes
887
+ await delay(1000);
888
+ } else {
889
+ console.log(chalk.red(`❌ Failed to create attribute: ${attribute.key}, will retry in next round`));
890
+ attributesToProcess.push(attribute); // Add back to retry list
891
+ }
892
+ }
893
+
894
+ if (attributesToProcess.length === 0) {
895
+ console.log(chalk.green(`\n✅ Successfully created all ${attributes.length} attributes for collection: ${collection.name}`));
896
+ return true;
897
+ }
898
+
899
+ overallRetryCount++;
900
+
901
+ if (overallRetryCount < maxOverallRetries) {
902
+ console.log(chalk.yellow(`\n⏳ Waiting 5 seconds before retrying ${attributesToProcess.length} failed attributes...`));
903
+ await delay(5000);
831
904
 
832
- // Get updated collection data for next iteration
905
+ // Refresh collection data before retry
833
906
  try {
834
907
  currentCollection = await db.getCollection(dbId, collection.$id);
908
+ console.log(chalk.blue(`Refreshed collection data for retry`));
835
909
  } catch (error) {
836
- console.log(chalk.yellow(`Warning: Could not refresh collection data: ${error}`));
910
+ console.log(chalk.yellow(`Warning: Could not refresh collection data for retry: ${error}`));
837
911
  }
838
-
839
- // Add delay between successful attributes
840
- await delay(1000);
841
- } else {
842
- console.log(chalk.red(`❌ Failed to create attribute: ${attribute.key}`));
843
- failedAttributes.push(attribute.key);
844
912
  }
845
913
  }
846
914
 
847
- if (failedAttributes.length > 0) {
848
- console.log(chalk.red(`\n❌ Failed to create ${failedAttributes.length} attributes: ${failedAttributes.join(', ')}`));
915
+ // If we get here, some attributes still failed after all retries
916
+ if (attributesToProcess.length > 0) {
917
+ console.log(chalk.red(`\n❌ Failed to create ${attributesToProcess.length} attributes after ${maxOverallRetries} attempts: ${attributesToProcess.map(a => a.key).join(', ')}`));
918
+ console.log(chalk.red(`This may indicate a fundamental issue with the attribute definitions or Appwrite instance`));
849
919
  return false;
850
920
  }
851
921
 
@@ -233,32 +233,55 @@ export const createOrUpdateIndexesWithStatusCheck = async (
233
233
  ): Promise<boolean> => {
234
234
  console.log(chalk.blue(`Creating/updating ${indexes.length} indexes with status monitoring...`));
235
235
 
236
- const failedIndexes: string[] = [];
236
+ let indexesToProcess = [...indexes];
237
+ let overallRetryCount = 0;
238
+ const maxOverallRetries = 3;
237
239
 
238
- for (const index of indexes) {
239
- console.log(chalk.blue(`\n--- Processing index: ${index.key} ---`));
240
+ while (indexesToProcess.length > 0 && overallRetryCount < maxOverallRetries) {
241
+ const remainingIndexes = [...indexesToProcess];
242
+ indexesToProcess = []; // Reset for next iteration
240
243
 
241
- const success = await createOrUpdateIndexWithStatusCheck(
242
- dbId,
243
- db,
244
- collectionId,
245
- collection,
246
- index
247
- );
244
+ console.log(chalk.blue(`\n=== Attempt ${overallRetryCount + 1}/${maxOverallRetries} - Processing ${remainingIndexes.length} indexes ===`));
248
245
 
249
- if (success) {
250
- console.log(chalk.green(`✅ Successfully created index: ${index.key}`));
246
+ for (const index of remainingIndexes) {
247
+ console.log(chalk.blue(`\n--- Processing index: ${index.key} ---`));
251
248
 
252
- // Add delay between successful indexes
253
- await delay(1000);
254
- } else {
255
- console.log(chalk.red(`❌ Failed to create index: ${index.key}`));
256
- failedIndexes.push(index.key);
249
+ const success = await createOrUpdateIndexWithStatusCheck(
250
+ dbId,
251
+ db,
252
+ collectionId,
253
+ collection,
254
+ index
255
+ );
256
+
257
+ if (success) {
258
+ console.log(chalk.green(`✅ Successfully created index: ${index.key}`));
259
+
260
+ // Add delay between successful indexes
261
+ await delay(1000);
262
+ } else {
263
+ console.log(chalk.red(`❌ Failed to create index: ${index.key}, will retry in next round`));
264
+ indexesToProcess.push(index); // Add back to retry list
265
+ }
266
+ }
267
+
268
+ if (indexesToProcess.length === 0) {
269
+ console.log(chalk.green(`\n✅ Successfully created all ${indexes.length} indexes`));
270
+ return true;
271
+ }
272
+
273
+ overallRetryCount++;
274
+
275
+ if (overallRetryCount < maxOverallRetries) {
276
+ console.log(chalk.yellow(`\n⏳ Waiting 5 seconds before retrying ${indexesToProcess.length} failed indexes...`));
277
+ await delay(5000);
257
278
  }
258
279
  }
259
280
 
260
- if (failedIndexes.length > 0) {
261
- console.log(chalk.red(`\n❌ Failed to create ${failedIndexes.length} indexes: ${failedIndexes.join(', ')}`));
281
+ // If we get here, some indexes still failed after all retries
282
+ if (indexesToProcess.length > 0) {
283
+ console.log(chalk.red(`\n❌ Failed to create ${indexesToProcess.length} indexes after ${maxOverallRetries} attempts: ${indexesToProcess.map(i => i.key).join(', ')}`));
284
+ console.log(chalk.red(`This may indicate a fundamental issue with the index definitions or Appwrite instance`));
262
285
  return false;
263
286
  }
264
287
 
@@ -2052,6 +2052,9 @@ export class InteractiveCLI {
2052
2052
  MessageFormatter.info("Starting comprehensive transfer configuration...", { prefix: "Transfer" });
2053
2053
 
2054
2054
  try {
2055
+ // Initialize controller to optionally load config if available (supports both YAML and TypeScript configs)
2056
+ await this.initControllerIfNeeded();
2057
+
2055
2058
  // Check if user has an appwrite config for easier setup
2056
2059
  const hasAppwriteConfig = this.controller?.config?.appwriteEndpoint &&
2057
2060
  this.controller?.config?.appwriteProject &&
@@ -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,191 @@ 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
+ MessageFormatter.error(`Skipping index creation and document transfer for collection ${collection.name} due to attribute failures`, undefined, { prefix: "Transfer" });
335
+ // Skip indexes and document transfer if attributes failed
336
+ continue;
337
+ } else {
338
+ MessageFormatter.success(`All attributes created successfully for collection ${collection.name}`, { prefix: "Transfer" });
339
+ }
340
+
341
+ // Handle indexes with enhanced status checking
342
+ MessageFormatter.info(`Creating indexes for collection ${collection.name} with enhanced monitoring...`, { prefix: "Transfer" });
343
+
344
+ const indexesSuccess = await this.createCollectionIndexesWithStatusCheck(
345
+ dbId,
346
+ this.targetDatabases,
347
+ targetCollection.$id,
348
+ targetCollection,
349
+ collection.indexes as any
350
+ );
351
+
352
+ if (!indexesSuccess) {
353
+ MessageFormatter.error(`Failed to create some indexes for collection ${collection.name}`, undefined, { prefix: "Transfer" });
354
+ MessageFormatter.warning(`Proceeding with document transfer despite index failures for collection ${collection.name}`, { prefix: "Transfer" });
355
+ } else {
356
+ MessageFormatter.success(`All indexes created successfully for collection ${collection.name}`, { prefix: "Transfer" });
357
+ }
358
+
359
+ MessageFormatter.success(`Structure complete for collection ${collection.name}`, { prefix: "Transfer" });
360
+ } catch (error) {
361
+ MessageFormatter.error(`Error processing collection ${collection.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
362
+ }
363
+ }
364
+ } catch (error) {
365
+ MessageFormatter.error(`Failed to create database structure for ${dbId}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
366
+ throw error;
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Phase 2: Transfer documents to all collections in the database
372
+ */
373
+ private async transferDatabaseDocuments(dbId: string): Promise<void> {
374
+ MessageFormatter.info(`Transferring documents for database ${dbId}`, { prefix: "Transfer" });
375
+
376
+ try {
377
+ // Get all collections from source database
378
+ const sourceCollections = await this.fetchAllCollections(dbId, this.sourceDatabases);
379
+ MessageFormatter.info(`Transferring documents for ${sourceCollections.length} collections in database ${dbId}`, { prefix: "Transfer" });
380
+
381
+ // Process each collection
382
+ for (const collection of sourceCollections) {
383
+ MessageFormatter.info(`Transferring documents for collection: ${collection.name} (${collection.$id})`, { prefix: "Transfer" });
384
+
385
+ try {
386
+ // Transfer documents
387
+ await this.transferDocumentsBetweenDatabases(
388
+ this.sourceDatabases,
389
+ this.targetDatabases,
390
+ dbId,
391
+ dbId,
392
+ collection.$id,
393
+ collection.$id
394
+ );
395
+
396
+ MessageFormatter.success(`Documents transferred for collection ${collection.name}`, { prefix: "Transfer" });
397
+ } catch (error) {
398
+ MessageFormatter.error(`Error transferring documents for collection ${collection.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
399
+ }
400
+ }
401
+ } catch (error) {
402
+ MessageFormatter.error(`Failed to transfer documents for database ${dbId}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
403
+ throw error;
404
+ }
405
+ }
406
+
245
407
  private async transferAllBuckets(): Promise<void> {
246
408
  MessageFormatter.info("Starting bucket transfer phase", { prefix: "Transfer" });
247
409
 
@@ -483,6 +645,180 @@ export class ComprehensiveTransfer {
483
645
  }
484
646
  }
485
647
 
648
+ /**
649
+ * Helper method to fetch all collections from a database
650
+ */
651
+ private async fetchAllCollections(dbId: string, databases: Databases): Promise<Models.Collection[]> {
652
+ const collections: Models.Collection[] = [];
653
+ let lastId: string | undefined;
654
+
655
+ while (true) {
656
+ const queries = [Query.limit(100)];
657
+ if (lastId) {
658
+ queries.push(Query.cursorAfter(lastId));
659
+ }
660
+
661
+ const result = await tryAwaitWithRetry(async () => databases.listCollections(dbId, queries));
662
+
663
+ if (result.collections.length === 0) {
664
+ break;
665
+ }
666
+
667
+ collections.push(...result.collections);
668
+
669
+ if (result.collections.length < 100) {
670
+ break;
671
+ }
672
+
673
+ lastId = result.collections[result.collections.length - 1].$id;
674
+ }
675
+
676
+ return collections;
677
+ }
678
+
679
+ /**
680
+ * Helper method to parse attribute objects (simplified version of parseAttribute)
681
+ */
682
+ private parseAttribute(attr: any): any {
683
+ // This is a simplified version - in production you'd use the actual parseAttribute from appwrite-utils
684
+ return {
685
+ key: attr.key,
686
+ type: attr.type,
687
+ size: attr.size,
688
+ required: attr.required,
689
+ array: attr.array,
690
+ default: attr.default,
691
+ format: attr.format,
692
+ elements: attr.elements,
693
+ min: attr.min,
694
+ max: attr.max,
695
+ relatedCollection: attr.relatedCollection,
696
+ relationType: attr.relationType,
697
+ twoWay: attr.twoWay,
698
+ twoWayKey: attr.twoWayKey,
699
+ onDelete: attr.onDelete,
700
+ side: attr.side
701
+ };
702
+ }
703
+
704
+ /**
705
+ * Helper method to create collection attributes with status checking
706
+ */
707
+ private async createCollectionAttributesWithStatusCheck(
708
+ databases: Databases,
709
+ dbId: string,
710
+ collection: Models.Collection,
711
+ attributes: any[]
712
+ ): Promise<boolean> {
713
+ // Import the enhanced attribute creation function
714
+ const { createUpdateCollectionAttributesWithStatusCheck } = await import("../collections/attributes.js");
715
+
716
+ return await createUpdateCollectionAttributesWithStatusCheck(
717
+ databases,
718
+ dbId,
719
+ collection,
720
+ attributes
721
+ );
722
+ }
723
+
724
+ /**
725
+ * Helper method to create collection indexes with status checking
726
+ */
727
+ private async createCollectionIndexesWithStatusCheck(
728
+ dbId: string,
729
+ databases: Databases,
730
+ collectionId: string,
731
+ collection: Models.Collection,
732
+ indexes: any[]
733
+ ): Promise<boolean> {
734
+ // Import the enhanced index creation function
735
+ const { createOrUpdateIndexesWithStatusCheck } = await import("../collections/indexes.js");
736
+
737
+ return await createOrUpdateIndexesWithStatusCheck(
738
+ dbId,
739
+ databases,
740
+ collectionId,
741
+ collection,
742
+ indexes
743
+ );
744
+ }
745
+
746
+ /**
747
+ * Helper method to transfer documents between databases
748
+ */
749
+ private async transferDocumentsBetweenDatabases(
750
+ sourceDb: Databases,
751
+ targetDb: Databases,
752
+ sourceDbId: string,
753
+ targetDbId: string,
754
+ sourceCollectionId: string,
755
+ targetCollectionId: string
756
+ ): Promise<void> {
757
+ MessageFormatter.info(`Transferring documents from ${sourceCollectionId} to ${targetCollectionId}`, { prefix: "Transfer" });
758
+
759
+ let lastId: string | undefined;
760
+ let totalTransferred = 0;
761
+
762
+ while (true) {
763
+ const queries = [Query.limit(50)]; // Smaller batch size for better performance
764
+ if (lastId) {
765
+ queries.push(Query.cursorAfter(lastId));
766
+ }
767
+
768
+ const documents = await tryAwaitWithRetry(async () =>
769
+ sourceDb.listDocuments(sourceDbId, sourceCollectionId, queries)
770
+ );
771
+
772
+ if (documents.documents.length === 0) {
773
+ break;
774
+ }
775
+
776
+ // Transfer documents with rate limiting
777
+ const transferTasks = documents.documents.map(doc =>
778
+ this.limit(async () => {
779
+ try {
780
+ // Check if document already exists
781
+ try {
782
+ await targetDb.getDocument(targetDbId, targetCollectionId, doc.$id);
783
+ MessageFormatter.info(`Document ${doc.$id} already exists, skipping`, { prefix: "Transfer" });
784
+ return;
785
+ } catch (error) {
786
+ // Document doesn't exist, proceed with creation
787
+ }
788
+
789
+ // Create document in target
790
+ const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
791
+
792
+ await tryAwaitWithRetry(async () =>
793
+ targetDb.createDocument(
794
+ targetDbId,
795
+ targetCollectionId,
796
+ doc.$id,
797
+ docData,
798
+ doc.$permissions
799
+ )
800
+ );
801
+
802
+ totalTransferred++;
803
+ MessageFormatter.success(`Transferred document ${doc.$id}`, { prefix: "Transfer" });
804
+ } catch (error) {
805
+ MessageFormatter.error(`Failed to transfer document ${doc.$id}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
806
+ }
807
+ })
808
+ );
809
+
810
+ await Promise.all(transferTasks);
811
+
812
+ if (documents.documents.length < 50) {
813
+ break;
814
+ }
815
+
816
+ lastId = documents.documents[documents.documents.length - 1].$id;
817
+ }
818
+
819
+ MessageFormatter.info(`Transferred ${totalTransferred} documents from ${sourceCollectionId} to ${targetCollectionId}`, { prefix: "Transfer" });
820
+ }
821
+
486
822
  private printSummary(): void {
487
823
  const duration = Math.round((Date.now() - this.startTime) / 1000);
488
824