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.
- package/dist/collections/attributes.js +74 -26
- package/dist/collections/indexes.js +31 -13
- package/dist/interactiveCLI.js +2 -0
- package/dist/migrations/comprehensiveTransfer.d.ts +28 -0
- package/dist/migrations/comprehensiveTransfer.js +227 -7
- package/package.json +1 -1
- package/src/collections/attributes.ts +103 -33
- package/src/collections/indexes.ts +42 -19
- package/src/interactiveCLI.ts +3 -0
- package/src/migrations/comprehensiveTransfer.ts +350 -14
@@ -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
|
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
|
-
//
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
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
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
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
|
-
|
465
|
-
|
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
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
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
|
-
|
143
|
-
|
144
|
-
|
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
|
-
|
148
|
-
|
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`));
|
package/dist/interactiveCLI.js
CHANGED
@@ -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
|
-
|
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
|
-
//
|
145
|
-
await
|
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}
|
163
|
+
MessageFormatter.success(`Database documents transferred: ${db.name}`, { prefix: "Transfer" });
|
148
164
|
}
|
149
165
|
catch (error) {
|
150
|
-
MessageFormatter.error(`
|
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(
|
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.
|
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
|
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
|
-
//
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
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
|
-
|
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
|
-
|
856
|
+
let attributesToProcess = [...attributes];
|
857
|
+
let overallRetryCount = 0;
|
858
|
+
const maxOverallRetries = 3;
|
818
859
|
|
819
|
-
|
820
|
-
|
860
|
+
while (attributesToProcess.length > 0 && overallRetryCount < maxOverallRetries) {
|
861
|
+
const remainingAttributes = [...attributesToProcess];
|
862
|
+
attributesToProcess = []; // Reset for next iteration
|
821
863
|
|
822
|
-
|
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
|
-
|
830
|
-
console.log(chalk.
|
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
|
-
//
|
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
|
-
|
848
|
-
|
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
|
-
|
236
|
+
let indexesToProcess = [...indexes];
|
237
|
+
let overallRetryCount = 0;
|
238
|
+
const maxOverallRetries = 3;
|
237
239
|
|
238
|
-
|
239
|
-
|
240
|
+
while (indexesToProcess.length > 0 && overallRetryCount < maxOverallRetries) {
|
241
|
+
const remainingIndexes = [...indexesToProcess];
|
242
|
+
indexesToProcess = []; // Reset for next iteration
|
240
243
|
|
241
|
-
|
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
|
-
|
250
|
-
console.log(chalk.
|
246
|
+
for (const index of remainingIndexes) {
|
247
|
+
console.log(chalk.blue(`\n--- Processing index: ${index.key} ---`));
|
251
248
|
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
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
|
-
|
261
|
-
|
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
|
|
package/src/interactiveCLI.ts
CHANGED
@@ -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
|
-
|
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
|
-
//
|
220
|
-
await
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
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}
|
245
|
+
MessageFormatter.success(`Database documents transferred: ${db.name}`, { prefix: "Transfer" });
|
231
246
|
} catch (error) {
|
232
|
-
MessageFormatter.error(`
|
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(
|
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
|
|