appwrite-utils-cli 1.2.15 → 1.2.17
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/README.md +2 -0
- package/dist/collections/indexes.js +6 -7
- package/dist/migrations/comprehensiveTransfer.js +196 -101
- package/dist/migrations/transfer.js +6 -0
- package/package.json +1 -1
- package/src/collections/indexes.ts +6 -7
- package/src/migrations/comprehensiveTransfer.ts +891 -364
- package/src/migrations/transfer.ts +12 -0
@@ -1,4 +1,9 @@
|
|
1
|
-
import {
|
1
|
+
import {
|
2
|
+
converterFunctions,
|
3
|
+
tryAwaitWithRetry,
|
4
|
+
parseAttribute,
|
5
|
+
objectNeedsUpdate,
|
6
|
+
} from "appwrite-utils";
|
2
7
|
import {
|
3
8
|
Client,
|
4
9
|
Databases,
|
@@ -8,22 +13,24 @@ import {
|
|
8
13
|
Teams,
|
9
14
|
type Models,
|
10
15
|
Query,
|
16
|
+
AppwriteException,
|
11
17
|
} from "node-appwrite";
|
12
18
|
import { InputFile } from "node-appwrite/file";
|
13
19
|
import { MessageFormatter } from "../shared/messageFormatter.js";
|
14
20
|
import { ProgressManager } from "../shared/progressManager.js";
|
15
21
|
import { getClient } from "../utils/getClientFromConfig.js";
|
16
|
-
import {
|
17
|
-
transferDatabaseLocalToLocal,
|
22
|
+
import {
|
23
|
+
transferDatabaseLocalToLocal,
|
18
24
|
transferDatabaseLocalToRemote,
|
19
25
|
transferStorageLocalToLocal,
|
20
26
|
transferStorageLocalToRemote,
|
21
|
-
transferUsersLocalToRemote
|
27
|
+
transferUsersLocalToRemote,
|
22
28
|
} from "./transfer.js";
|
23
|
-
import {
|
24
|
-
|
25
|
-
|
26
|
-
|
29
|
+
import { deployLocalFunction } from "../functions/deployments.js";
|
30
|
+
import {
|
31
|
+
listFunctions,
|
32
|
+
downloadLatestFunctionDeployment,
|
33
|
+
} from "../functions/methods.js";
|
27
34
|
import pLimit from "p-limit";
|
28
35
|
import chalk from "chalk";
|
29
36
|
import { join } from "node:path";
|
@@ -100,7 +107,7 @@ export class ComprehensiveTransfer {
|
|
100
107
|
|
101
108
|
const baseLimit = options.concurrencyLimit || 10;
|
102
109
|
this.limit = pLimit(baseLimit);
|
103
|
-
|
110
|
+
|
104
111
|
// Different rate limits for different operations to prevent API throttling
|
105
112
|
// Users: Half speed (more sensitive operations)
|
106
113
|
// Files: Quarter speed (most bandwidth intensive)
|
@@ -120,18 +127,25 @@ export class ComprehensiveTransfer {
|
|
120
127
|
|
121
128
|
async execute(): Promise<TransferResults> {
|
122
129
|
try {
|
123
|
-
MessageFormatter.info("Starting comprehensive transfer", {
|
124
|
-
|
130
|
+
MessageFormatter.info("Starting comprehensive transfer", {
|
131
|
+
prefix: "Transfer",
|
132
|
+
});
|
133
|
+
|
125
134
|
if (this.options.dryRun) {
|
126
|
-
MessageFormatter.info("DRY RUN MODE - No actual changes will be made", {
|
135
|
+
MessageFormatter.info("DRY RUN MODE - No actual changes will be made", {
|
136
|
+
prefix: "Transfer",
|
137
|
+
});
|
127
138
|
}
|
128
139
|
|
129
140
|
// Show rate limiting configuration
|
130
141
|
const baseLimit = this.options.concurrencyLimit || 10;
|
131
142
|
const userLimit = Math.max(1, Math.floor(baseLimit / 2));
|
132
143
|
const fileLimit = Math.max(1, Math.floor(baseLimit / 4));
|
133
|
-
|
134
|
-
MessageFormatter.info(
|
144
|
+
|
145
|
+
MessageFormatter.info(
|
146
|
+
`Rate limits: General=${baseLimit}, Users=${userLimit}, Files=${fileLimit}`,
|
147
|
+
{ prefix: "Transfer" }
|
148
|
+
);
|
135
149
|
|
136
150
|
// Ensure temp directory exists
|
137
151
|
if (!fs.existsSync(this.tempDir)) {
|
@@ -164,7 +178,11 @@ export class ComprehensiveTransfer {
|
|
164
178
|
|
165
179
|
return this.results;
|
166
180
|
} catch (error) {
|
167
|
-
MessageFormatter.error(
|
181
|
+
MessageFormatter.error(
|
182
|
+
"Comprehensive transfer failed",
|
183
|
+
error instanceof Error ? error : new Error(String(error)),
|
184
|
+
{ prefix: "Transfer" }
|
185
|
+
);
|
168
186
|
throw error;
|
169
187
|
} finally {
|
170
188
|
// Clean up temp directory
|
@@ -175,11 +193,16 @@ export class ComprehensiveTransfer {
|
|
175
193
|
}
|
176
194
|
|
177
195
|
private async transferAllUsers(): Promise<void> {
|
178
|
-
MessageFormatter.info("Starting user transfer phase", {
|
196
|
+
MessageFormatter.info("Starting user transfer phase", {
|
197
|
+
prefix: "Transfer",
|
198
|
+
});
|
179
199
|
|
180
200
|
if (this.options.dryRun) {
|
181
201
|
const usersList = await this.sourceUsers.list([Query.limit(1)]);
|
182
|
-
MessageFormatter.info(
|
202
|
+
MessageFormatter.info(
|
203
|
+
`DRY RUN: Would transfer ${usersList.total} users`,
|
204
|
+
{ prefix: "Transfer" }
|
205
|
+
);
|
183
206
|
return;
|
184
207
|
}
|
185
208
|
|
@@ -193,20 +216,28 @@ export class ComprehensiveTransfer {
|
|
193
216
|
this.options.targetProject,
|
194
217
|
this.options.targetKey
|
195
218
|
);
|
196
|
-
|
219
|
+
|
197
220
|
// Get actual count for results
|
198
221
|
const usersList = await this.sourceUsers.list([Query.limit(1)]);
|
199
222
|
this.results.users.transferred = usersList.total;
|
200
|
-
|
201
|
-
MessageFormatter.success(`User transfer completed`, {
|
223
|
+
|
224
|
+
MessageFormatter.success(`User transfer completed`, {
|
225
|
+
prefix: "Transfer",
|
226
|
+
});
|
202
227
|
} catch (error) {
|
203
|
-
MessageFormatter.error(
|
228
|
+
MessageFormatter.error(
|
229
|
+
"User transfer failed",
|
230
|
+
error instanceof Error ? error : new Error(String(error)),
|
231
|
+
{ prefix: "Transfer" }
|
232
|
+
);
|
204
233
|
this.results.users.failed = 1;
|
205
234
|
}
|
206
235
|
}
|
207
236
|
|
208
237
|
private async transferAllTeams(): Promise<void> {
|
209
|
-
MessageFormatter.info("Starting team transfer phase", {
|
238
|
+
MessageFormatter.info("Starting team transfer phase", {
|
239
|
+
prefix: "Transfer",
|
240
|
+
});
|
210
241
|
|
211
242
|
try {
|
212
243
|
// Fetch all teams from source with pagination
|
@@ -216,47 +247,68 @@ export class ComprehensiveTransfer {
|
|
216
247
|
if (this.options.dryRun) {
|
217
248
|
let totalMemberships = 0;
|
218
249
|
for (const team of allSourceTeams) {
|
219
|
-
const memberships = await this.sourceTeams.listMemberships(team.$id, [
|
250
|
+
const memberships = await this.sourceTeams.listMemberships(team.$id, [
|
251
|
+
Query.limit(1),
|
252
|
+
]);
|
220
253
|
totalMemberships += memberships.total;
|
221
254
|
}
|
222
|
-
MessageFormatter.info(
|
255
|
+
MessageFormatter.info(
|
256
|
+
`DRY RUN: Would transfer ${allSourceTeams.length} teams with ${totalMemberships} memberships`,
|
257
|
+
{ prefix: "Transfer" }
|
258
|
+
);
|
223
259
|
return;
|
224
260
|
}
|
225
261
|
|
226
|
-
const transferTasks = allSourceTeams.map(team =>
|
262
|
+
const transferTasks = allSourceTeams.map((team) =>
|
227
263
|
this.limit(async () => {
|
228
264
|
try {
|
229
265
|
// Check if team exists in target
|
230
|
-
const existingTeam = allTargetTeams.find(
|
231
|
-
|
266
|
+
const existingTeam = allTargetTeams.find(
|
267
|
+
(tt) => tt.$id === team.$id
|
268
|
+
);
|
269
|
+
|
232
270
|
if (!existingTeam) {
|
233
271
|
// Fetch all memberships to extract unique roles before creating team
|
234
|
-
MessageFormatter.info(
|
272
|
+
MessageFormatter.info(
|
273
|
+
`Fetching memberships for team ${team.name} to extract roles`,
|
274
|
+
{ prefix: "Transfer" }
|
275
|
+
);
|
235
276
|
const memberships = await this.fetchAllMemberships(team.$id);
|
236
|
-
|
277
|
+
|
237
278
|
// Extract unique roles from all memberships
|
238
279
|
const allRoles = new Set<string>();
|
239
|
-
memberships.forEach(membership => {
|
240
|
-
membership.roles.forEach(role => allRoles.add(role));
|
280
|
+
memberships.forEach((membership) => {
|
281
|
+
membership.roles.forEach((role) => allRoles.add(role));
|
241
282
|
});
|
242
283
|
const uniqueRoles = Array.from(allRoles);
|
243
|
-
|
244
|
-
MessageFormatter.info(
|
245
|
-
|
284
|
+
|
285
|
+
MessageFormatter.info(
|
286
|
+
`Found ${uniqueRoles.length} unique roles for team ${
|
287
|
+
team.name
|
288
|
+
}: ${uniqueRoles.join(", ")}`,
|
289
|
+
{ prefix: "Transfer" }
|
290
|
+
);
|
291
|
+
|
246
292
|
// Create team in target with the collected roles
|
247
|
-
await this.targetTeams.create(
|
248
|
-
|
249
|
-
team.name
|
250
|
-
|
293
|
+
await this.targetTeams.create(team.$id, team.name, uniqueRoles);
|
294
|
+
MessageFormatter.success(
|
295
|
+
`Created team: ${team.name} with roles: ${uniqueRoles.join(
|
296
|
+
", "
|
297
|
+
)}`,
|
298
|
+
{ prefix: "Transfer" }
|
251
299
|
);
|
252
|
-
MessageFormatter.success(`Created team: ${team.name} with roles: ${uniqueRoles.join(', ')}`, { prefix: "Transfer" });
|
253
300
|
} else {
|
254
|
-
MessageFormatter.info(
|
255
|
-
|
301
|
+
MessageFormatter.info(
|
302
|
+
`Team ${team.name} already exists, updating if needed`,
|
303
|
+
{ prefix: "Transfer" }
|
304
|
+
);
|
305
|
+
|
256
306
|
// Update team if needed
|
257
307
|
if (existingTeam.name !== team.name) {
|
258
308
|
await this.targetTeams.updateName(team.$id, team.name);
|
259
|
-
MessageFormatter.success(`Updated team name: ${team.name}`, {
|
309
|
+
MessageFormatter.success(`Updated team name: ${team.name}`, {
|
310
|
+
prefix: "Transfer",
|
311
|
+
});
|
260
312
|
}
|
261
313
|
}
|
262
314
|
|
@@ -264,54 +316,85 @@ export class ComprehensiveTransfer {
|
|
264
316
|
await this.transferTeamMemberships(team.$id);
|
265
317
|
|
266
318
|
this.results.teams.transferred++;
|
267
|
-
MessageFormatter.success(
|
319
|
+
MessageFormatter.success(
|
320
|
+
`Team ${team.name} transferred successfully`,
|
321
|
+
{ prefix: "Transfer" }
|
322
|
+
);
|
268
323
|
} catch (error) {
|
269
|
-
MessageFormatter.error(
|
324
|
+
MessageFormatter.error(
|
325
|
+
`Team ${team.name} transfer failed`,
|
326
|
+
error instanceof Error ? error : new Error(String(error)),
|
327
|
+
{ prefix: "Transfer" }
|
328
|
+
);
|
270
329
|
this.results.teams.failed++;
|
271
330
|
}
|
272
331
|
})
|
273
332
|
);
|
274
333
|
|
275
334
|
await Promise.all(transferTasks);
|
276
|
-
MessageFormatter.success("Team transfer phase completed", {
|
335
|
+
MessageFormatter.success("Team transfer phase completed", {
|
336
|
+
prefix: "Transfer",
|
337
|
+
});
|
277
338
|
} catch (error) {
|
278
|
-
MessageFormatter.error(
|
339
|
+
MessageFormatter.error(
|
340
|
+
"Team transfer phase failed",
|
341
|
+
error instanceof Error ? error : new Error(String(error)),
|
342
|
+
{ prefix: "Transfer" }
|
343
|
+
);
|
279
344
|
}
|
280
345
|
}
|
281
346
|
|
282
347
|
private async transferAllDatabases(): Promise<void> {
|
283
|
-
MessageFormatter.info("Starting database transfer phase", {
|
348
|
+
MessageFormatter.info("Starting database transfer phase", {
|
349
|
+
prefix: "Transfer",
|
350
|
+
});
|
284
351
|
|
285
352
|
try {
|
286
353
|
const sourceDatabases = await this.sourceDatabases.list();
|
287
354
|
const targetDatabases = await this.targetDatabases.list();
|
288
355
|
|
289
356
|
if (this.options.dryRun) {
|
290
|
-
MessageFormatter.info(
|
357
|
+
MessageFormatter.info(
|
358
|
+
`DRY RUN: Would transfer ${sourceDatabases.databases.length} databases`,
|
359
|
+
{ prefix: "Transfer" }
|
360
|
+
);
|
291
361
|
return;
|
292
362
|
}
|
293
363
|
|
294
364
|
// Phase 1: Create all databases and collections (structure only)
|
295
|
-
MessageFormatter.info(
|
296
|
-
|
297
|
-
|
365
|
+
MessageFormatter.info(
|
366
|
+
"Phase 1: Creating database structures (databases, collections, attributes, indexes)",
|
367
|
+
{ prefix: "Transfer" }
|
368
|
+
);
|
369
|
+
|
370
|
+
const structureCreationTasks = sourceDatabases.databases.map((db) =>
|
298
371
|
this.limit(async () => {
|
299
372
|
try {
|
300
373
|
// Check if database exists in target
|
301
|
-
const existingDb = targetDatabases.databases.find(
|
302
|
-
|
374
|
+
const existingDb = targetDatabases.databases.find(
|
375
|
+
(tdb) => tdb.$id === db.$id
|
376
|
+
);
|
377
|
+
|
303
378
|
if (!existingDb) {
|
304
379
|
// Create database in target
|
305
380
|
await this.targetDatabases.create(db.$id, db.name, db.enabled);
|
306
|
-
MessageFormatter.success(`Created database: ${db.name}`, {
|
381
|
+
MessageFormatter.success(`Created database: ${db.name}`, {
|
382
|
+
prefix: "Transfer",
|
383
|
+
});
|
307
384
|
}
|
308
385
|
|
309
386
|
// Create collections, attributes, and indexes WITHOUT transferring documents
|
310
387
|
await this.createDatabaseStructure(db.$id);
|
311
388
|
|
312
|
-
MessageFormatter.success(`Database structure created: ${db.name}`, {
|
389
|
+
MessageFormatter.success(`Database structure created: ${db.name}`, {
|
390
|
+
prefix: "Transfer",
|
391
|
+
});
|
313
392
|
} catch (error) {
|
314
|
-
MessageFormatter.error(
|
393
|
+
MessageFormatter.error(
|
394
|
+
`Database structure creation failed for ${db.name}`,
|
395
|
+
error instanceof Error ? error : new Error(String(error)),
|
396
|
+
{ prefix: "Transfer" }
|
397
|
+
);
|
315
398
|
this.results.databases.failed++;
|
316
399
|
}
|
317
400
|
})
|
@@ -320,27 +403,43 @@ export class ComprehensiveTransfer {
|
|
320
403
|
await Promise.all(structureCreationTasks);
|
321
404
|
|
322
405
|
// Phase 2: Transfer all documents after all structures are created
|
323
|
-
MessageFormatter.info(
|
324
|
-
|
325
|
-
|
406
|
+
MessageFormatter.info(
|
407
|
+
"Phase 2: Transferring documents to all collections",
|
408
|
+
{ prefix: "Transfer" }
|
409
|
+
);
|
410
|
+
|
411
|
+
const documentTransferTasks = sourceDatabases.databases.map((db) =>
|
326
412
|
this.limit(async () => {
|
327
413
|
try {
|
328
414
|
// Transfer documents for this database
|
329
415
|
await this.transferDatabaseDocuments(db.$id);
|
330
416
|
|
331
417
|
this.results.databases.transferred++;
|
332
|
-
MessageFormatter.success(
|
418
|
+
MessageFormatter.success(
|
419
|
+
`Database documents transferred: ${db.name}`,
|
420
|
+
{ prefix: "Transfer" }
|
421
|
+
);
|
333
422
|
} catch (error) {
|
334
|
-
MessageFormatter.error(
|
423
|
+
MessageFormatter.error(
|
424
|
+
`Document transfer failed for ${db.name}`,
|
425
|
+
error instanceof Error ? error : new Error(String(error)),
|
426
|
+
{ prefix: "Transfer" }
|
427
|
+
);
|
335
428
|
this.results.databases.failed++;
|
336
429
|
}
|
337
430
|
})
|
338
431
|
);
|
339
432
|
|
340
433
|
await Promise.all(documentTransferTasks);
|
341
|
-
MessageFormatter.success("Database transfer phase completed", {
|
434
|
+
MessageFormatter.success("Database transfer phase completed", {
|
435
|
+
prefix: "Transfer",
|
436
|
+
});
|
342
437
|
} catch (error) {
|
343
|
-
MessageFormatter.error(
|
438
|
+
MessageFormatter.error(
|
439
|
+
"Database transfer phase failed",
|
440
|
+
error instanceof Error ? error : new Error(String(error)),
|
441
|
+
{ prefix: "Transfer" }
|
442
|
+
);
|
344
443
|
}
|
345
444
|
}
|
346
445
|
|
@@ -348,33 +447,51 @@ export class ComprehensiveTransfer {
|
|
348
447
|
* Phase 1: Create database structure (collections, attributes, indexes) without transferring documents
|
349
448
|
*/
|
350
449
|
private async createDatabaseStructure(dbId: string): Promise<void> {
|
351
|
-
MessageFormatter.info(`Creating database structure for ${dbId}`, {
|
450
|
+
MessageFormatter.info(`Creating database structure for ${dbId}`, {
|
451
|
+
prefix: "Transfer",
|
452
|
+
});
|
352
453
|
|
353
454
|
try {
|
354
455
|
// Get all collections from source database
|
355
|
-
const sourceCollections = await this.fetchAllCollections(
|
356
|
-
|
456
|
+
const sourceCollections = await this.fetchAllCollections(
|
457
|
+
dbId,
|
458
|
+
this.sourceDatabases
|
459
|
+
);
|
460
|
+
MessageFormatter.info(
|
461
|
+
`Found ${sourceCollections.length} collections in source database ${dbId}`,
|
462
|
+
{ prefix: "Transfer" }
|
463
|
+
);
|
357
464
|
|
358
465
|
// Process each collection
|
359
466
|
for (const collection of sourceCollections) {
|
360
|
-
MessageFormatter.info(
|
467
|
+
MessageFormatter.info(
|
468
|
+
`Processing collection: ${collection.name} (${collection.$id})`,
|
469
|
+
{ prefix: "Transfer" }
|
470
|
+
);
|
361
471
|
|
362
472
|
try {
|
363
473
|
// Create or update collection in target
|
364
474
|
let targetCollection: Models.Collection;
|
365
475
|
const existingCollection = await tryAwaitWithRetry(async () =>
|
366
|
-
this.targetDatabases.listCollections(dbId, [
|
476
|
+
this.targetDatabases.listCollections(dbId, [
|
477
|
+
Query.equal("$id", collection.$id),
|
478
|
+
])
|
367
479
|
);
|
368
480
|
|
369
481
|
if (existingCollection.collections.length > 0) {
|
370
482
|
targetCollection = existingCollection.collections[0];
|
371
|
-
MessageFormatter.info(
|
483
|
+
MessageFormatter.info(
|
484
|
+
`Collection ${collection.name} exists in target database`,
|
485
|
+
{ prefix: "Transfer" }
|
486
|
+
);
|
372
487
|
|
373
488
|
// Update collection if needed
|
374
489
|
if (
|
375
490
|
targetCollection.name !== collection.name ||
|
376
|
-
JSON.stringify(targetCollection.$permissions) !==
|
377
|
-
|
491
|
+
JSON.stringify(targetCollection.$permissions) !==
|
492
|
+
JSON.stringify(collection.$permissions) ||
|
493
|
+
targetCollection.documentSecurity !==
|
494
|
+
collection.documentSecurity ||
|
378
495
|
targetCollection.enabled !== collection.enabled
|
379
496
|
) {
|
380
497
|
targetCollection = await tryAwaitWithRetry(async () =>
|
@@ -387,10 +504,16 @@ export class ComprehensiveTransfer {
|
|
387
504
|
collection.enabled
|
388
505
|
)
|
389
506
|
);
|
390
|
-
MessageFormatter.success(
|
507
|
+
MessageFormatter.success(
|
508
|
+
`Collection ${collection.name} updated`,
|
509
|
+
{ prefix: "Transfer" }
|
510
|
+
);
|
391
511
|
}
|
392
512
|
} else {
|
393
|
-
MessageFormatter.info(
|
513
|
+
MessageFormatter.info(
|
514
|
+
`Creating collection ${collection.name} in target database...`,
|
515
|
+
{ prefix: "Transfer" }
|
516
|
+
);
|
394
517
|
targetCollection = await tryAwaitWithRetry(async () =>
|
395
518
|
this.targetDatabases.createCollection(
|
396
519
|
dbId,
|
@@ -401,55 +524,113 @@ export class ComprehensiveTransfer {
|
|
401
524
|
collection.enabled
|
402
525
|
)
|
403
526
|
);
|
404
|
-
MessageFormatter.success(`Collection ${collection.name} created`, {
|
527
|
+
MessageFormatter.success(`Collection ${collection.name} created`, {
|
528
|
+
prefix: "Transfer",
|
529
|
+
});
|
405
530
|
}
|
406
531
|
|
407
532
|
// Handle attributes with enhanced status checking
|
408
|
-
MessageFormatter.info(
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
targetCollection,
|
416
|
-
attributesToCreate
|
533
|
+
MessageFormatter.info(
|
534
|
+
`Creating attributes for collection ${collection.name} with enhanced monitoring...`,
|
535
|
+
{ prefix: "Transfer" }
|
536
|
+
);
|
537
|
+
|
538
|
+
const attributesToCreate = collection.attributes.map((attr) =>
|
539
|
+
parseAttribute(attr as any)
|
417
540
|
);
|
418
|
-
|
541
|
+
|
542
|
+
const attributesSuccess =
|
543
|
+
await this.createCollectionAttributesWithStatusCheck(
|
544
|
+
this.targetDatabases,
|
545
|
+
dbId,
|
546
|
+
targetCollection,
|
547
|
+
attributesToCreate
|
548
|
+
);
|
549
|
+
|
419
550
|
if (!attributesSuccess) {
|
420
|
-
MessageFormatter.error(
|
421
|
-
|
551
|
+
MessageFormatter.error(
|
552
|
+
`Failed to create some attributes for collection ${collection.name}`,
|
553
|
+
undefined,
|
554
|
+
{ prefix: "Transfer" }
|
555
|
+
);
|
556
|
+
MessageFormatter.error(
|
557
|
+
`Skipping index creation and document transfer for collection ${collection.name} due to attribute failures`,
|
558
|
+
undefined,
|
559
|
+
{ prefix: "Transfer" }
|
560
|
+
);
|
422
561
|
// Skip indexes and document transfer if attributes failed
|
423
562
|
continue;
|
424
563
|
} else {
|
425
|
-
MessageFormatter.success(
|
564
|
+
MessageFormatter.success(
|
565
|
+
`All attributes created successfully for collection ${collection.name}`,
|
566
|
+
{ prefix: "Transfer" }
|
567
|
+
);
|
426
568
|
}
|
427
569
|
|
428
570
|
// Handle indexes with enhanced status checking
|
429
|
-
MessageFormatter.info(
|
430
|
-
|
431
|
-
|
432
|
-
dbId,
|
433
|
-
this.targetDatabases,
|
434
|
-
targetCollection.$id,
|
435
|
-
targetCollection,
|
436
|
-
collection.indexes as any
|
571
|
+
MessageFormatter.info(
|
572
|
+
`Creating indexes for collection ${collection.name} with enhanced monitoring...`,
|
573
|
+
{ prefix: "Transfer" }
|
437
574
|
);
|
438
|
-
|
575
|
+
|
576
|
+
let indexesSuccess = true;
|
577
|
+
// Check if indexes need to be created ahead of time
|
578
|
+
if (
|
579
|
+
collection.indexes.some(
|
580
|
+
(index) =>
|
581
|
+
!targetCollection.indexes.some(
|
582
|
+
(ti) =>
|
583
|
+
ti.key === index.key ||
|
584
|
+
ti.attributes.sort().join(",") ===
|
585
|
+
index.attributes.sort().join(",")
|
586
|
+
)
|
587
|
+
) ||
|
588
|
+
collection.indexes.length !== targetCollection.indexes.length
|
589
|
+
) {
|
590
|
+
indexesSuccess = await this.createCollectionIndexesWithStatusCheck(
|
591
|
+
dbId,
|
592
|
+
this.targetDatabases,
|
593
|
+
targetCollection.$id,
|
594
|
+
targetCollection,
|
595
|
+
collection.indexes as any
|
596
|
+
);
|
597
|
+
}
|
598
|
+
|
439
599
|
if (!indexesSuccess) {
|
440
|
-
MessageFormatter.error(
|
441
|
-
|
600
|
+
MessageFormatter.error(
|
601
|
+
`Failed to create some indexes for collection ${collection.name}`,
|
602
|
+
undefined,
|
603
|
+
{ prefix: "Transfer" }
|
604
|
+
);
|
605
|
+
MessageFormatter.warning(
|
606
|
+
`Proceeding with document transfer despite index failures for collection ${collection.name}`,
|
607
|
+
{ prefix: "Transfer" }
|
608
|
+
);
|
442
609
|
} else {
|
443
|
-
MessageFormatter.success(
|
610
|
+
MessageFormatter.success(
|
611
|
+
`All indexes created successfully for collection ${collection.name}`,
|
612
|
+
{ prefix: "Transfer" }
|
613
|
+
);
|
444
614
|
}
|
445
615
|
|
446
|
-
MessageFormatter.success(
|
616
|
+
MessageFormatter.success(
|
617
|
+
`Structure complete for collection ${collection.name}`,
|
618
|
+
{ prefix: "Transfer" }
|
619
|
+
);
|
447
620
|
} catch (error) {
|
448
|
-
MessageFormatter.error(
|
621
|
+
MessageFormatter.error(
|
622
|
+
`Error processing collection ${collection.name}`,
|
623
|
+
error instanceof Error ? error : new Error(String(error)),
|
624
|
+
{ prefix: "Transfer" }
|
625
|
+
);
|
449
626
|
}
|
450
627
|
}
|
451
628
|
} catch (error) {
|
452
|
-
MessageFormatter.error(
|
629
|
+
MessageFormatter.error(
|
630
|
+
`Failed to create database structure for ${dbId}`,
|
631
|
+
error instanceof Error ? error : new Error(String(error)),
|
632
|
+
{ prefix: "Transfer" }
|
633
|
+
);
|
453
634
|
throw error;
|
454
635
|
}
|
455
636
|
}
|
@@ -458,16 +639,27 @@ export class ComprehensiveTransfer {
|
|
458
639
|
* Phase 2: Transfer documents to all collections in the database
|
459
640
|
*/
|
460
641
|
private async transferDatabaseDocuments(dbId: string): Promise<void> {
|
461
|
-
MessageFormatter.info(`Transferring documents for database ${dbId}`, {
|
642
|
+
MessageFormatter.info(`Transferring documents for database ${dbId}`, {
|
643
|
+
prefix: "Transfer",
|
644
|
+
});
|
462
645
|
|
463
646
|
try {
|
464
647
|
// Get all collections from source database
|
465
|
-
const sourceCollections = await this.fetchAllCollections(
|
466
|
-
|
648
|
+
const sourceCollections = await this.fetchAllCollections(
|
649
|
+
dbId,
|
650
|
+
this.sourceDatabases
|
651
|
+
);
|
652
|
+
MessageFormatter.info(
|
653
|
+
`Transferring documents for ${sourceCollections.length} collections in database ${dbId}`,
|
654
|
+
{ prefix: "Transfer" }
|
655
|
+
);
|
467
656
|
|
468
657
|
// Process each collection
|
469
658
|
for (const collection of sourceCollections) {
|
470
|
-
MessageFormatter.info(
|
659
|
+
MessageFormatter.info(
|
660
|
+
`Transferring documents for collection: ${collection.name} (${collection.$id})`,
|
661
|
+
{ prefix: "Transfer" }
|
662
|
+
);
|
471
663
|
|
472
664
|
try {
|
473
665
|
// Transfer documents
|
@@ -479,20 +671,33 @@ export class ComprehensiveTransfer {
|
|
479
671
|
collection.$id,
|
480
672
|
collection.$id
|
481
673
|
);
|
482
|
-
|
483
|
-
MessageFormatter.success(
|
674
|
+
|
675
|
+
MessageFormatter.success(
|
676
|
+
`Documents transferred for collection ${collection.name}`,
|
677
|
+
{ prefix: "Transfer" }
|
678
|
+
);
|
484
679
|
} catch (error) {
|
485
|
-
MessageFormatter.error(
|
680
|
+
MessageFormatter.error(
|
681
|
+
`Error transferring documents for collection ${collection.name}`,
|
682
|
+
error instanceof Error ? error : new Error(String(error)),
|
683
|
+
{ prefix: "Transfer" }
|
684
|
+
);
|
486
685
|
}
|
487
686
|
}
|
488
687
|
} catch (error) {
|
489
|
-
MessageFormatter.error(
|
688
|
+
MessageFormatter.error(
|
689
|
+
`Failed to transfer documents for database ${dbId}`,
|
690
|
+
error instanceof Error ? error : new Error(String(error)),
|
691
|
+
{ prefix: "Transfer" }
|
692
|
+
);
|
490
693
|
throw error;
|
491
694
|
}
|
492
695
|
}
|
493
696
|
|
494
697
|
private async transferAllBuckets(): Promise<void> {
|
495
|
-
MessageFormatter.info("Starting bucket transfer phase", {
|
698
|
+
MessageFormatter.info("Starting bucket transfer phase", {
|
699
|
+
prefix: "Transfer",
|
700
|
+
});
|
496
701
|
|
497
702
|
try {
|
498
703
|
// Get all buckets from source with pagination
|
@@ -502,38 +707,52 @@ export class ComprehensiveTransfer {
|
|
502
707
|
if (this.options.dryRun) {
|
503
708
|
let totalFiles = 0;
|
504
709
|
for (const bucket of allSourceBuckets) {
|
505
|
-
const files = await this.sourceStorage.listFiles(bucket.$id, [
|
710
|
+
const files = await this.sourceStorage.listFiles(bucket.$id, [
|
711
|
+
Query.limit(1),
|
712
|
+
]);
|
506
713
|
totalFiles += files.total;
|
507
714
|
}
|
508
|
-
MessageFormatter.info(
|
715
|
+
MessageFormatter.info(
|
716
|
+
`DRY RUN: Would transfer ${allSourceBuckets.length} buckets with ${totalFiles} files`,
|
717
|
+
{ prefix: "Transfer" }
|
718
|
+
);
|
509
719
|
return;
|
510
720
|
}
|
511
721
|
|
512
|
-
const transferTasks = allSourceBuckets.map(bucket =>
|
722
|
+
const transferTasks = allSourceBuckets.map((bucket) =>
|
513
723
|
this.limit(async () => {
|
514
724
|
try {
|
515
725
|
// Check if bucket exists in target
|
516
|
-
const existingBucket = allTargetBuckets.find(
|
517
|
-
|
726
|
+
const existingBucket = allTargetBuckets.find(
|
727
|
+
(tb) => tb.$id === bucket.$id
|
728
|
+
);
|
729
|
+
|
518
730
|
if (!existingBucket) {
|
519
731
|
// Create bucket with fallback strategy for maximumFileSize
|
520
732
|
await this.createBucketWithFallback(bucket);
|
521
|
-
MessageFormatter.success(`Created bucket: ${bucket.name}`, {
|
733
|
+
MessageFormatter.success(`Created bucket: ${bucket.name}`, {
|
734
|
+
prefix: "Transfer",
|
735
|
+
});
|
522
736
|
} else {
|
523
737
|
// Compare bucket permissions and update if needed
|
524
|
-
const sourcePermissions = JSON.stringify(
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
738
|
+
const sourcePermissions = JSON.stringify(
|
739
|
+
bucket.$permissions?.sort() || []
|
740
|
+
);
|
741
|
+
const targetPermissions = JSON.stringify(
|
742
|
+
existingBucket.$permissions?.sort() || []
|
743
|
+
);
|
744
|
+
|
745
|
+
if (
|
746
|
+
sourcePermissions !== targetPermissions ||
|
747
|
+
existingBucket.name !== bucket.name ||
|
748
|
+
existingBucket.fileSecurity !== bucket.fileSecurity ||
|
749
|
+
existingBucket.enabled !== bucket.enabled
|
750
|
+
) {
|
532
751
|
MessageFormatter.warning(
|
533
|
-
`Bucket ${bucket.name} exists but has different settings. Updating to match source.`,
|
752
|
+
`Bucket ${bucket.name} exists but has different settings. Updating to match source.`,
|
534
753
|
{ prefix: "Transfer" }
|
535
754
|
);
|
536
|
-
|
755
|
+
|
537
756
|
try {
|
538
757
|
await this.targetStorage.updateBucket(
|
539
758
|
bucket.$id,
|
@@ -547,16 +766,24 @@ export class ComprehensiveTransfer {
|
|
547
766
|
bucket.encryption,
|
548
767
|
bucket.antivirus
|
549
768
|
);
|
550
|
-
MessageFormatter.success(
|
769
|
+
MessageFormatter.success(
|
770
|
+
`Updated bucket ${bucket.name} to match source`,
|
771
|
+
{ prefix: "Transfer" }
|
772
|
+
);
|
551
773
|
} catch (updateError) {
|
552
774
|
MessageFormatter.error(
|
553
|
-
`Failed to update bucket ${bucket.name}`,
|
554
|
-
updateError instanceof Error
|
775
|
+
`Failed to update bucket ${bucket.name}`,
|
776
|
+
updateError instanceof Error
|
777
|
+
? updateError
|
778
|
+
: new Error(String(updateError)),
|
555
779
|
{ prefix: "Transfer" }
|
556
780
|
);
|
557
781
|
}
|
558
782
|
} else {
|
559
|
-
MessageFormatter.info(
|
783
|
+
MessageFormatter.info(
|
784
|
+
`Bucket ${bucket.name} already exists with matching settings`,
|
785
|
+
{ prefix: "Transfer" }
|
786
|
+
);
|
560
787
|
}
|
561
788
|
}
|
562
789
|
|
@@ -564,31 +791,46 @@ export class ComprehensiveTransfer {
|
|
564
791
|
await this.transferBucketFiles(bucket.$id, bucket.$id);
|
565
792
|
|
566
793
|
this.results.buckets.transferred++;
|
567
|
-
MessageFormatter.success(
|
794
|
+
MessageFormatter.success(
|
795
|
+
`Bucket ${bucket.name} transferred successfully`,
|
796
|
+
{ prefix: "Transfer" }
|
797
|
+
);
|
568
798
|
} catch (error) {
|
569
|
-
MessageFormatter.error(
|
799
|
+
MessageFormatter.error(
|
800
|
+
`Bucket ${bucket.name} transfer failed`,
|
801
|
+
error instanceof Error ? error : new Error(String(error)),
|
802
|
+
{ prefix: "Transfer" }
|
803
|
+
);
|
570
804
|
this.results.buckets.failed++;
|
571
805
|
}
|
572
806
|
})
|
573
807
|
);
|
574
808
|
|
575
809
|
await Promise.all(transferTasks);
|
576
|
-
MessageFormatter.success("Bucket transfer phase completed", {
|
810
|
+
MessageFormatter.success("Bucket transfer phase completed", {
|
811
|
+
prefix: "Transfer",
|
812
|
+
});
|
577
813
|
} catch (error) {
|
578
|
-
MessageFormatter.error(
|
814
|
+
MessageFormatter.error(
|
815
|
+
"Bucket transfer phase failed",
|
816
|
+
error instanceof Error ? error : new Error(String(error)),
|
817
|
+
{ prefix: "Transfer" }
|
818
|
+
);
|
579
819
|
}
|
580
820
|
}
|
581
821
|
|
582
822
|
private async createBucketWithFallback(bucket: Models.Bucket): Promise<void> {
|
583
823
|
// Determine the optimal size to try first
|
584
824
|
let sizeToTry: number;
|
585
|
-
|
825
|
+
|
586
826
|
if (this.cachedMaxFileSize) {
|
587
827
|
// Use cached size if it's smaller than or equal to the bucket's original size
|
588
828
|
if (bucket.maximumFileSize >= this.cachedMaxFileSize) {
|
589
829
|
sizeToTry = this.cachedMaxFileSize;
|
590
830
|
MessageFormatter.info(
|
591
|
-
`Bucket ${bucket.name}: Using cached maximumFileSize ${sizeToTry} (${(
|
831
|
+
`Bucket ${bucket.name}: Using cached maximumFileSize ${sizeToTry} (${(
|
832
|
+
sizeToTry / 1_000_000_000
|
833
|
+
).toFixed(1)}GB)`,
|
592
834
|
{ prefix: "Transfer" }
|
593
835
|
);
|
594
836
|
} else {
|
@@ -614,30 +856,41 @@ export class ComprehensiveTransfer {
|
|
614
856
|
bucket.encryption,
|
615
857
|
bucket.antivirus
|
616
858
|
);
|
617
|
-
|
859
|
+
|
618
860
|
// Success - cache this size if it's not already cached or is smaller than cached
|
619
861
|
if (!this.cachedMaxFileSize || sizeToTry < this.cachedMaxFileSize) {
|
620
862
|
this.cachedMaxFileSize = sizeToTry;
|
621
863
|
MessageFormatter.info(
|
622
|
-
`Bucket ${
|
864
|
+
`Bucket ${
|
865
|
+
bucket.name
|
866
|
+
}: Cached successful maximumFileSize ${sizeToTry} (${(
|
867
|
+
sizeToTry / 1_000_000_000
|
868
|
+
).toFixed(1)}GB)`,
|
623
869
|
{ prefix: "Transfer" }
|
624
870
|
);
|
625
871
|
}
|
626
|
-
|
872
|
+
|
627
873
|
// Log if we used a different size than original
|
628
874
|
if (sizeToTry !== bucket.maximumFileSize) {
|
629
875
|
MessageFormatter.warning(
|
630
|
-
`Bucket ${
|
876
|
+
`Bucket ${
|
877
|
+
bucket.name
|
878
|
+
}: maximumFileSize used ${sizeToTry} instead of original ${
|
879
|
+
bucket.maximumFileSize
|
880
|
+
} (${(sizeToTry / 1_000_000_000).toFixed(1)}GB)`,
|
631
881
|
{ prefix: "Transfer" }
|
632
882
|
);
|
633
883
|
}
|
634
|
-
|
884
|
+
|
635
885
|
return; // Success, exit the function
|
636
886
|
} catch (error) {
|
637
887
|
const err = error instanceof Error ? error : new Error(String(error));
|
638
|
-
|
888
|
+
|
639
889
|
// Check if the error is related to maximumFileSize validation
|
640
|
-
if (
|
890
|
+
if (
|
891
|
+
err.message.includes("maximumFileSize") ||
|
892
|
+
err.message.includes("valid range")
|
893
|
+
) {
|
641
894
|
MessageFormatter.warning(
|
642
895
|
`Bucket ${bucket.name}: Failed with maximumFileSize ${sizeToTry}, falling back to smaller sizes...`,
|
643
896
|
{ prefix: "Transfer" }
|
@@ -655,17 +908,17 @@ export class ComprehensiveTransfer {
|
|
655
908
|
2_500_000_000, // 2.5GB
|
656
909
|
2_000_000_000, // 2GB
|
657
910
|
1_000_000_000, // 1GB
|
658
|
-
500_000_000,
|
659
|
-
100_000_000
|
911
|
+
500_000_000, // 500MB
|
912
|
+
100_000_000, // 100MB
|
660
913
|
];
|
661
914
|
|
662
915
|
// Remove sizes that are larger than or equal to the already-tried size
|
663
916
|
const validSizes = fallbackSizes
|
664
|
-
.filter(size => size < sizeToTry)
|
917
|
+
.filter((size) => size < sizeToTry)
|
665
918
|
.sort((a, b) => b - a); // Sort descending
|
666
919
|
|
667
920
|
let lastError: Error | null = null;
|
668
|
-
|
921
|
+
|
669
922
|
for (const fileSize of validSizes) {
|
670
923
|
try {
|
671
924
|
await this.targetStorage.createBucket(
|
@@ -680,30 +933,39 @@ export class ComprehensiveTransfer {
|
|
680
933
|
bucket.encryption,
|
681
934
|
bucket.antivirus
|
682
935
|
);
|
683
|
-
|
936
|
+
|
684
937
|
// Success - cache this size if it's not already cached or is smaller than cached
|
685
938
|
if (!this.cachedMaxFileSize || fileSize < this.cachedMaxFileSize) {
|
686
939
|
this.cachedMaxFileSize = fileSize;
|
687
940
|
MessageFormatter.info(
|
688
|
-
`Bucket ${
|
941
|
+
`Bucket ${
|
942
|
+
bucket.name
|
943
|
+
}: Cached successful maximumFileSize ${fileSize} (${(
|
944
|
+
fileSize / 1_000_000_000
|
945
|
+
).toFixed(1)}GB)`,
|
689
946
|
{ prefix: "Transfer" }
|
690
947
|
);
|
691
948
|
}
|
692
|
-
|
949
|
+
|
693
950
|
// Log if we had to reduce the file size
|
694
951
|
if (fileSize !== bucket.maximumFileSize) {
|
695
952
|
MessageFormatter.warning(
|
696
|
-
`Bucket ${bucket.name}: maximumFileSize reduced from ${
|
953
|
+
`Bucket ${bucket.name}: maximumFileSize reduced from ${
|
954
|
+
bucket.maximumFileSize
|
955
|
+
} to ${fileSize} (${(fileSize / 1_000_000_000).toFixed(1)}GB)`,
|
697
956
|
{ prefix: "Transfer" }
|
698
957
|
);
|
699
958
|
}
|
700
|
-
|
959
|
+
|
701
960
|
return; // Success, exit the function
|
702
961
|
} catch (error) {
|
703
962
|
lastError = error instanceof Error ? error : new Error(String(error));
|
704
|
-
|
963
|
+
|
705
964
|
// Check if the error is related to maximumFileSize validation
|
706
|
-
if (
|
965
|
+
if (
|
966
|
+
lastError.message.includes("maximumFileSize") ||
|
967
|
+
lastError.message.includes("valid range")
|
968
|
+
) {
|
707
969
|
MessageFormatter.warning(
|
708
970
|
`Bucket ${bucket.name}: Failed with maximumFileSize ${fileSize}, trying smaller size...`,
|
709
971
|
{ prefix: "Transfer" }
|
@@ -715,17 +977,20 @@ export class ComprehensiveTransfer {
|
|
715
977
|
}
|
716
978
|
}
|
717
979
|
}
|
718
|
-
|
980
|
+
|
719
981
|
// If we get here, all fallback sizes failed
|
720
982
|
MessageFormatter.error(
|
721
983
|
`Bucket ${bucket.name}: All fallback file sizes failed. Last error: ${lastError?.message}`,
|
722
984
|
lastError || undefined,
|
723
985
|
{ prefix: "Transfer" }
|
724
986
|
);
|
725
|
-
throw lastError || new Error(
|
987
|
+
throw lastError || new Error("All fallback file sizes failed");
|
726
988
|
}
|
727
989
|
|
728
|
-
private async transferBucketFiles(
|
990
|
+
private async transferBucketFiles(
|
991
|
+
sourceBucketId: string,
|
992
|
+
targetBucketId: string
|
993
|
+
): Promise<void> {
|
729
994
|
let lastFileId: string | undefined;
|
730
995
|
let transferredFiles = 0;
|
731
996
|
|
@@ -739,24 +1004,31 @@ export class ComprehensiveTransfer {
|
|
739
1004
|
if (files.files.length === 0) break;
|
740
1005
|
|
741
1006
|
// Process files with rate limiting
|
742
|
-
const fileTasks = files.files.map(file =>
|
1007
|
+
const fileTasks = files.files.map((file) =>
|
743
1008
|
this.fileLimit(async () => {
|
744
1009
|
try {
|
745
1010
|
// Check if file already exists and compare permissions
|
746
1011
|
let existingFile: Models.File | null = null;
|
747
1012
|
try {
|
748
|
-
existingFile = await this.targetStorage.getFile(
|
749
|
-
|
1013
|
+
existingFile = await this.targetStorage.getFile(
|
1014
|
+
targetBucketId,
|
1015
|
+
file.$id
|
1016
|
+
);
|
1017
|
+
|
750
1018
|
// Compare permissions between source and target file
|
751
|
-
const sourcePermissions = JSON.stringify(
|
752
|
-
|
753
|
-
|
1019
|
+
const sourcePermissions = JSON.stringify(
|
1020
|
+
file.$permissions?.sort() || []
|
1021
|
+
);
|
1022
|
+
const targetPermissions = JSON.stringify(
|
1023
|
+
existingFile.$permissions?.sort() || []
|
1024
|
+
);
|
1025
|
+
|
754
1026
|
if (sourcePermissions !== targetPermissions) {
|
755
1027
|
MessageFormatter.warning(
|
756
|
-
`File ${file.name} (${file.$id}) exists but has different permissions. Source: ${sourcePermissions}, Target: ${targetPermissions}`,
|
1028
|
+
`File ${file.name} (${file.$id}) exists but has different permissions. Source: ${sourcePermissions}, Target: ${targetPermissions}`,
|
757
1029
|
{ prefix: "Transfer" }
|
758
1030
|
);
|
759
|
-
|
1031
|
+
|
760
1032
|
// Update file permissions to match source
|
761
1033
|
try {
|
762
1034
|
await this.targetStorage.updateFile(
|
@@ -765,16 +1037,24 @@ export class ComprehensiveTransfer {
|
|
765
1037
|
file.name,
|
766
1038
|
file.$permissions
|
767
1039
|
);
|
768
|
-
MessageFormatter.success(
|
1040
|
+
MessageFormatter.success(
|
1041
|
+
`Updated file ${file.name} permissions to match source`,
|
1042
|
+
{ prefix: "Transfer" }
|
1043
|
+
);
|
769
1044
|
} catch (updateError) {
|
770
1045
|
MessageFormatter.error(
|
771
|
-
`Failed to update permissions for file ${file.name}`,
|
772
|
-
updateError instanceof Error
|
1046
|
+
`Failed to update permissions for file ${file.name}`,
|
1047
|
+
updateError instanceof Error
|
1048
|
+
? updateError
|
1049
|
+
: new Error(String(updateError)),
|
773
1050
|
{ prefix: "Transfer" }
|
774
1051
|
);
|
775
1052
|
}
|
776
1053
|
} else {
|
777
|
-
MessageFormatter.info(
|
1054
|
+
MessageFormatter.info(
|
1055
|
+
`File ${file.name} already exists with matching permissions, skipping`,
|
1056
|
+
{ prefix: "Transfer" }
|
1057
|
+
);
|
778
1058
|
}
|
779
1059
|
return;
|
780
1060
|
} catch (error) {
|
@@ -782,9 +1062,15 @@ export class ComprehensiveTransfer {
|
|
782
1062
|
}
|
783
1063
|
|
784
1064
|
// Download file with validation
|
785
|
-
const fileData = await this.validateAndDownloadFile(
|
1065
|
+
const fileData = await this.validateAndDownloadFile(
|
1066
|
+
sourceBucketId,
|
1067
|
+
file.$id
|
1068
|
+
);
|
786
1069
|
if (!fileData) {
|
787
|
-
MessageFormatter.warning(
|
1070
|
+
MessageFormatter.warning(
|
1071
|
+
`File ${file.name} failed validation, skipping`,
|
1072
|
+
{ prefix: "Transfer" }
|
1073
|
+
);
|
788
1074
|
return;
|
789
1075
|
}
|
790
1076
|
|
@@ -802,9 +1088,15 @@ export class ComprehensiveTransfer {
|
|
802
1088
|
);
|
803
1089
|
|
804
1090
|
transferredFiles++;
|
805
|
-
MessageFormatter.success(`Transferred file: ${file.name}`, {
|
1091
|
+
MessageFormatter.success(`Transferred file: ${file.name}`, {
|
1092
|
+
prefix: "Transfer",
|
1093
|
+
});
|
806
1094
|
} catch (error) {
|
807
|
-
MessageFormatter.error(
|
1095
|
+
MessageFormatter.error(
|
1096
|
+
`Failed to transfer file ${file.name}`,
|
1097
|
+
error instanceof Error ? error : new Error(String(error)),
|
1098
|
+
{ prefix: "Transfer" }
|
1099
|
+
);
|
808
1100
|
}
|
809
1101
|
})
|
810
1102
|
);
|
@@ -815,61 +1107,99 @@ export class ComprehensiveTransfer {
|
|
815
1107
|
lastFileId = files.files[files.files.length - 1].$id;
|
816
1108
|
}
|
817
1109
|
|
818
|
-
MessageFormatter.info(
|
1110
|
+
MessageFormatter.info(
|
1111
|
+
`Transferred ${transferredFiles} files from bucket ${sourceBucketId}`,
|
1112
|
+
{ prefix: "Transfer" }
|
1113
|
+
);
|
819
1114
|
}
|
820
1115
|
|
821
|
-
private async validateAndDownloadFile(
|
1116
|
+
private async validateAndDownloadFile(
|
1117
|
+
bucketId: string,
|
1118
|
+
fileId: string
|
1119
|
+
): Promise<ArrayBuffer | null> {
|
822
1120
|
let attempts = 3;
|
823
1121
|
while (attempts > 0) {
|
824
1122
|
try {
|
825
|
-
const fileData = await this.sourceStorage.getFileDownload(
|
826
|
-
|
1123
|
+
const fileData = await this.sourceStorage.getFileDownload(
|
1124
|
+
bucketId,
|
1125
|
+
fileId
|
1126
|
+
);
|
1127
|
+
|
827
1128
|
// Basic validation - ensure file is not empty and not too large
|
828
1129
|
if (fileData.byteLength === 0) {
|
829
|
-
MessageFormatter.warning(`File ${fileId} is empty`, {
|
1130
|
+
MessageFormatter.warning(`File ${fileId} is empty`, {
|
1131
|
+
prefix: "Transfer",
|
1132
|
+
});
|
830
1133
|
return null;
|
831
1134
|
}
|
832
1135
|
|
833
|
-
if (fileData.byteLength > 50 * 1024 * 1024) {
|
834
|
-
|
1136
|
+
if (fileData.byteLength > 50 * 1024 * 1024) {
|
1137
|
+
// 50MB limit
|
1138
|
+
MessageFormatter.warning(
|
1139
|
+
`File ${fileId} is too large (${fileData.byteLength} bytes)`,
|
1140
|
+
{ prefix: "Transfer" }
|
1141
|
+
);
|
835
1142
|
return null;
|
836
1143
|
}
|
837
1144
|
|
838
1145
|
return fileData;
|
839
1146
|
} catch (error) {
|
840
1147
|
attempts--;
|
841
|
-
MessageFormatter.warning(
|
1148
|
+
MessageFormatter.warning(
|
1149
|
+
`Error downloading file ${fileId}, attempts left: ${attempts}`,
|
1150
|
+
{ prefix: "Transfer" }
|
1151
|
+
);
|
842
1152
|
if (attempts === 0) {
|
843
|
-
MessageFormatter.error(
|
1153
|
+
MessageFormatter.error(
|
1154
|
+
`Failed to download file ${fileId} after all attempts`,
|
1155
|
+
error instanceof Error ? error : new Error(String(error)),
|
1156
|
+
{ prefix: "Transfer" }
|
1157
|
+
);
|
844
1158
|
return null;
|
845
1159
|
}
|
846
1160
|
// Wait before retry
|
847
|
-
await new Promise(resolve =>
|
1161
|
+
await new Promise((resolve) =>
|
1162
|
+
setTimeout(resolve, 1000 * (4 - attempts))
|
1163
|
+
);
|
848
1164
|
}
|
849
1165
|
}
|
850
1166
|
return null;
|
851
1167
|
}
|
852
1168
|
|
853
1169
|
private async transferAllFunctions(): Promise<void> {
|
854
|
-
MessageFormatter.info("Starting function transfer phase", {
|
1170
|
+
MessageFormatter.info("Starting function transfer phase", {
|
1171
|
+
prefix: "Transfer",
|
1172
|
+
});
|
855
1173
|
|
856
1174
|
try {
|
857
|
-
const sourceFunctions = await listFunctions(this.sourceClient, [
|
858
|
-
|
1175
|
+
const sourceFunctions = await listFunctions(this.sourceClient, [
|
1176
|
+
Query.limit(1000),
|
1177
|
+
]);
|
1178
|
+
const targetFunctions = await listFunctions(this.targetClient, [
|
1179
|
+
Query.limit(1000),
|
1180
|
+
]);
|
859
1181
|
|
860
1182
|
if (this.options.dryRun) {
|
861
|
-
MessageFormatter.info(
|
1183
|
+
MessageFormatter.info(
|
1184
|
+
`DRY RUN: Would transfer ${sourceFunctions.functions.length} functions`,
|
1185
|
+
{ prefix: "Transfer" }
|
1186
|
+
);
|
862
1187
|
return;
|
863
1188
|
}
|
864
1189
|
|
865
|
-
const transferTasks = sourceFunctions.functions.map(func =>
|
1190
|
+
const transferTasks = sourceFunctions.functions.map((func) =>
|
866
1191
|
this.limit(async () => {
|
867
1192
|
try {
|
868
1193
|
// Check if function exists in target
|
869
|
-
const existingFunc = targetFunctions.functions.find(
|
870
|
-
|
1194
|
+
const existingFunc = targetFunctions.functions.find(
|
1195
|
+
(tf) => tf.$id === func.$id
|
1196
|
+
);
|
1197
|
+
|
871
1198
|
if (existingFunc) {
|
872
|
-
MessageFormatter.info(
|
1199
|
+
MessageFormatter.info(
|
1200
|
+
`Function ${func.name} already exists, skipping creation`,
|
1201
|
+
{ prefix: "Transfer" }
|
1202
|
+
);
|
873
1203
|
this.results.functions.skipped++;
|
874
1204
|
return;
|
875
1205
|
}
|
@@ -877,7 +1207,11 @@ export class ComprehensiveTransfer {
|
|
877
1207
|
// Download function from source
|
878
1208
|
const functionPath = await this.downloadFunction(func);
|
879
1209
|
if (!functionPath) {
|
880
|
-
MessageFormatter.error(
|
1210
|
+
MessageFormatter.error(
|
1211
|
+
`Failed to download function ${func.name}`,
|
1212
|
+
undefined,
|
1213
|
+
{ prefix: "Transfer" }
|
1214
|
+
);
|
881
1215
|
this.results.functions.failed++;
|
882
1216
|
return;
|
883
1217
|
}
|
@@ -904,26 +1238,45 @@ export class ComprehensiveTransfer {
|
|
904
1238
|
specification: func.specification as any,
|
905
1239
|
dirPath: functionPath,
|
906
1240
|
};
|
907
|
-
|
908
|
-
await deployLocalFunction(
|
1241
|
+
|
1242
|
+
await deployLocalFunction(
|
1243
|
+
this.targetClient,
|
1244
|
+
func.name,
|
1245
|
+
functionConfig
|
1246
|
+
);
|
909
1247
|
|
910
1248
|
this.results.functions.transferred++;
|
911
|
-
MessageFormatter.success(
|
1249
|
+
MessageFormatter.success(
|
1250
|
+
`Function ${func.name} transferred successfully`,
|
1251
|
+
{ prefix: "Transfer" }
|
1252
|
+
);
|
912
1253
|
} catch (error) {
|
913
|
-
MessageFormatter.error(
|
1254
|
+
MessageFormatter.error(
|
1255
|
+
`Function ${func.name} transfer failed`,
|
1256
|
+
error instanceof Error ? error : new Error(String(error)),
|
1257
|
+
{ prefix: "Transfer" }
|
1258
|
+
);
|
914
1259
|
this.results.functions.failed++;
|
915
1260
|
}
|
916
1261
|
})
|
917
1262
|
);
|
918
1263
|
|
919
1264
|
await Promise.all(transferTasks);
|
920
|
-
MessageFormatter.success("Function transfer phase completed", {
|
1265
|
+
MessageFormatter.success("Function transfer phase completed", {
|
1266
|
+
prefix: "Transfer",
|
1267
|
+
});
|
921
1268
|
} catch (error) {
|
922
|
-
MessageFormatter.error(
|
1269
|
+
MessageFormatter.error(
|
1270
|
+
"Function transfer phase failed",
|
1271
|
+
error instanceof Error ? error : new Error(String(error)),
|
1272
|
+
{ prefix: "Transfer" }
|
1273
|
+
);
|
923
1274
|
}
|
924
1275
|
}
|
925
1276
|
|
926
|
-
private async downloadFunction(
|
1277
|
+
private async downloadFunction(
|
1278
|
+
func: Models.Function
|
1279
|
+
): Promise<string | null> {
|
927
1280
|
try {
|
928
1281
|
const { path } = await downloadLatestFunctionDeployment(
|
929
1282
|
this.sourceClient,
|
@@ -932,7 +1285,11 @@ export class ComprehensiveTransfer {
|
|
932
1285
|
);
|
933
1286
|
return path;
|
934
1287
|
} catch (error) {
|
935
|
-
MessageFormatter.error(
|
1288
|
+
MessageFormatter.error(
|
1289
|
+
`Failed to download function ${func.name}`,
|
1290
|
+
error instanceof Error ? error : new Error(String(error)),
|
1291
|
+
{ prefix: "Transfer" }
|
1292
|
+
);
|
936
1293
|
return null;
|
937
1294
|
}
|
938
1295
|
}
|
@@ -940,7 +1297,10 @@ export class ComprehensiveTransfer {
|
|
940
1297
|
/**
|
941
1298
|
* Helper method to fetch all collections from a database
|
942
1299
|
*/
|
943
|
-
private async fetchAllCollections(
|
1300
|
+
private async fetchAllCollections(
|
1301
|
+
dbId: string,
|
1302
|
+
databases: Databases
|
1303
|
+
): Promise<Models.Collection[]> {
|
944
1304
|
const collections: Models.Collection[] = [];
|
945
1305
|
let lastId: string | undefined;
|
946
1306
|
|
@@ -950,18 +1310,20 @@ export class ComprehensiveTransfer {
|
|
950
1310
|
queries.push(Query.cursorAfter(lastId));
|
951
1311
|
}
|
952
1312
|
|
953
|
-
const result = await tryAwaitWithRetry(async () =>
|
954
|
-
|
1313
|
+
const result = await tryAwaitWithRetry(async () =>
|
1314
|
+
databases.listCollections(dbId, queries)
|
1315
|
+
);
|
1316
|
+
|
955
1317
|
if (result.collections.length === 0) {
|
956
1318
|
break;
|
957
1319
|
}
|
958
1320
|
|
959
1321
|
collections.push(...result.collections);
|
960
|
-
|
1322
|
+
|
961
1323
|
if (result.collections.length < 100) {
|
962
1324
|
break;
|
963
1325
|
}
|
964
|
-
|
1326
|
+
|
965
1327
|
lastId = result.collections[result.collections.length - 1].$id;
|
966
1328
|
}
|
967
1329
|
|
@@ -981,18 +1343,20 @@ export class ComprehensiveTransfer {
|
|
981
1343
|
queries.push(Query.cursorAfter(lastId));
|
982
1344
|
}
|
983
1345
|
|
984
|
-
const result = await tryAwaitWithRetry(async () =>
|
985
|
-
|
1346
|
+
const result = await tryAwaitWithRetry(async () =>
|
1347
|
+
storage.listBuckets(queries)
|
1348
|
+
);
|
1349
|
+
|
986
1350
|
if (result.buckets.length === 0) {
|
987
1351
|
break;
|
988
1352
|
}
|
989
1353
|
|
990
1354
|
buckets.push(...result.buckets);
|
991
|
-
|
1355
|
+
|
992
1356
|
if (result.buckets.length < 100) {
|
993
1357
|
break;
|
994
1358
|
}
|
995
|
-
|
1359
|
+
|
996
1360
|
lastId = result.buckets[result.buckets.length - 1].$id;
|
997
1361
|
}
|
998
1362
|
|
@@ -1020,7 +1384,7 @@ export class ComprehensiveTransfer {
|
|
1020
1384
|
twoWay: attr.twoWay,
|
1021
1385
|
twoWayKey: attr.twoWayKey,
|
1022
1386
|
onDelete: attr.onDelete,
|
1023
|
-
side: attr.side
|
1387
|
+
side: attr.side,
|
1024
1388
|
};
|
1025
1389
|
}
|
1026
1390
|
|
@@ -1034,8 +1398,10 @@ export class ComprehensiveTransfer {
|
|
1034
1398
|
attributes: any[]
|
1035
1399
|
): Promise<boolean> {
|
1036
1400
|
// Import the enhanced attribute creation function
|
1037
|
-
const { createUpdateCollectionAttributesWithStatusCheck } = await import(
|
1038
|
-
|
1401
|
+
const { createUpdateCollectionAttributesWithStatusCheck } = await import(
|
1402
|
+
"../collections/attributes.js"
|
1403
|
+
);
|
1404
|
+
|
1039
1405
|
return await createUpdateCollectionAttributesWithStatusCheck(
|
1040
1406
|
databases,
|
1041
1407
|
dbId,
|
@@ -1055,8 +1421,10 @@ export class ComprehensiveTransfer {
|
|
1055
1421
|
indexes: any[]
|
1056
1422
|
): Promise<boolean> {
|
1057
1423
|
// Import the enhanced index creation function
|
1058
|
-
const { createOrUpdateIndexesWithStatusCheck } = await import(
|
1059
|
-
|
1424
|
+
const { createOrUpdateIndexesWithStatusCheck } = await import(
|
1425
|
+
"../collections/indexes.js"
|
1426
|
+
);
|
1427
|
+
|
1060
1428
|
return await createOrUpdateIndexesWithStatusCheck(
|
1061
1429
|
dbId,
|
1062
1430
|
databases,
|
@@ -1077,7 +1445,10 @@ export class ComprehensiveTransfer {
|
|
1077
1445
|
sourceCollectionId: string,
|
1078
1446
|
targetCollectionId: string
|
1079
1447
|
): Promise<void> {
|
1080
|
-
MessageFormatter.info(
|
1448
|
+
MessageFormatter.info(
|
1449
|
+
`Transferring documents from ${sourceCollectionId} to ${targetCollectionId} with bulk operations, content comparison, and permission filtering`,
|
1450
|
+
{ prefix: "Transfer" }
|
1451
|
+
);
|
1081
1452
|
|
1082
1453
|
let lastId: string | undefined;
|
1083
1454
|
let totalTransferred = 0;
|
@@ -1085,11 +1456,14 @@ export class ComprehensiveTransfer {
|
|
1085
1456
|
let totalUpdated = 0;
|
1086
1457
|
|
1087
1458
|
// Check if bulk operations are supported
|
1088
|
-
const
|
1089
|
-
|
1090
|
-
|
1459
|
+
const bulkEnabled = false;
|
1460
|
+
// Temporarily disable to see if it fixes my permissions issues
|
1461
|
+
const supportsBulk = bulkEnabled ? this.options.targetEndpoint.includes("cloud.appwrite.io") : false;
|
1462
|
+
|
1091
1463
|
if (supportsBulk) {
|
1092
|
-
MessageFormatter.info(`Using bulk operations for enhanced performance`, {
|
1464
|
+
MessageFormatter.info(`Using bulk operations for enhanced performance`, {
|
1465
|
+
prefix: "Transfer",
|
1466
|
+
});
|
1093
1467
|
}
|
1094
1468
|
|
1095
1469
|
while (true) {
|
@@ -1099,7 +1473,7 @@ export class ComprehensiveTransfer {
|
|
1099
1473
|
queries.push(Query.cursorAfter(lastId));
|
1100
1474
|
}
|
1101
1475
|
|
1102
|
-
const sourceDocuments = await tryAwaitWithRetry(async () =>
|
1476
|
+
const sourceDocuments = await tryAwaitWithRetry(async () =>
|
1103
1477
|
sourceDb.listDocuments(sourceDbId, sourceCollectionId, queries)
|
1104
1478
|
);
|
1105
1479
|
|
@@ -1107,87 +1481,90 @@ export class ComprehensiveTransfer {
|
|
1107
1481
|
break;
|
1108
1482
|
}
|
1109
1483
|
|
1110
|
-
MessageFormatter.info(
|
1484
|
+
MessageFormatter.info(
|
1485
|
+
`Processing batch of ${sourceDocuments.documents.length} source documents`,
|
1486
|
+
{ prefix: "Transfer" }
|
1487
|
+
);
|
1111
1488
|
|
1112
1489
|
// Extract document IDs from the current batch
|
1113
|
-
const sourceDocIds = sourceDocuments.documents.map(doc => doc.$id);
|
1114
|
-
|
1490
|
+
const sourceDocIds = sourceDocuments.documents.map((doc) => doc.$id);
|
1491
|
+
|
1115
1492
|
// Fetch existing documents from target in a single query
|
1116
1493
|
const existingTargetDocs = await this.fetchTargetDocumentsBatch(
|
1117
|
-
targetDb,
|
1118
|
-
targetDbId,
|
1119
|
-
targetCollectionId,
|
1494
|
+
targetDb,
|
1495
|
+
targetDbId,
|
1496
|
+
targetCollectionId,
|
1120
1497
|
sourceDocIds
|
1121
1498
|
);
|
1122
1499
|
|
1123
1500
|
// Create a map for quick lookup of existing documents
|
1124
1501
|
const existingDocsMap = new Map<string, Models.Document>();
|
1125
|
-
existingTargetDocs.forEach(doc => {
|
1502
|
+
existingTargetDocs.forEach((doc) => {
|
1126
1503
|
existingDocsMap.set(doc.$id, doc);
|
1127
1504
|
});
|
1128
1505
|
|
1129
1506
|
// Filter documents based on existence, content comparison, and permission comparison
|
1130
1507
|
const documentsToTransfer: Models.Document[] = [];
|
1131
|
-
const documentsToUpdate: {
|
1508
|
+
const documentsToUpdate: {
|
1509
|
+
doc: Models.Document;
|
1510
|
+
targetDoc: Models.Document;
|
1511
|
+
reason: string;
|
1512
|
+
}[] = [];
|
1132
1513
|
|
1133
1514
|
for (const sourceDoc of sourceDocuments.documents) {
|
1134
1515
|
const existingTargetDoc = existingDocsMap.get(sourceDoc.$id);
|
1135
|
-
|
1516
|
+
|
1136
1517
|
if (!existingTargetDoc) {
|
1137
1518
|
// Document doesn't exist in target, needs to be transferred
|
1138
1519
|
documentsToTransfer.push(sourceDoc);
|
1139
1520
|
} else {
|
1140
1521
|
// Document exists, compare both content and permissions
|
1141
|
-
const sourcePermissions =
|
1142
|
-
|
1143
|
-
|
1144
|
-
|
1522
|
+
const sourcePermissions = Array.from(
|
1523
|
+
new Set(sourceDoc.$permissions || [])
|
1524
|
+
).sort();
|
1525
|
+
const targetPermissions = Array.from(
|
1526
|
+
new Set(existingTargetDoc.$permissions || [])
|
1527
|
+
).sort();
|
1528
|
+
const permissionsDiffer =
|
1529
|
+
sourcePermissions.join(",") !== targetPermissions.join(",") ||
|
1530
|
+
sourcePermissions.length !== targetPermissions.length;
|
1531
|
+
|
1145
1532
|
// Use objectNeedsUpdate to compare document content (excluding system fields)
|
1146
|
-
const contentDiffers = objectNeedsUpdate(
|
1147
|
-
|
1533
|
+
const contentDiffers = objectNeedsUpdate(
|
1534
|
+
existingTargetDoc,
|
1535
|
+
sourceDoc
|
1536
|
+
);
|
1537
|
+
|
1148
1538
|
if (contentDiffers && permissionsDiffer) {
|
1149
1539
|
// Both content and permissions differ
|
1150
|
-
documentsToUpdate.push({
|
1151
|
-
doc: sourceDoc,
|
1152
|
-
targetDoc: existingTargetDoc,
|
1153
|
-
reason: "content and permissions differ"
|
1540
|
+
documentsToUpdate.push({
|
1541
|
+
doc: sourceDoc,
|
1542
|
+
targetDoc: existingTargetDoc,
|
1543
|
+
reason: "content and permissions differ",
|
1154
1544
|
});
|
1155
|
-
MessageFormatter.info(
|
1156
|
-
`Document ${sourceDoc.$id} exists but content and permissions differ - will update`,
|
1157
|
-
{ prefix: "Transfer" }
|
1158
|
-
);
|
1159
1545
|
} else if (contentDiffers) {
|
1160
1546
|
// Only content differs
|
1161
|
-
documentsToUpdate.push({
|
1162
|
-
doc: sourceDoc,
|
1163
|
-
targetDoc: existingTargetDoc,
|
1164
|
-
reason: "content differs"
|
1547
|
+
documentsToUpdate.push({
|
1548
|
+
doc: sourceDoc,
|
1549
|
+
targetDoc: existingTargetDoc,
|
1550
|
+
reason: "content differs",
|
1165
1551
|
});
|
1166
|
-
MessageFormatter.info(
|
1167
|
-
`Document ${sourceDoc.$id} exists but content differs - will update`,
|
1168
|
-
{ prefix: "Transfer" }
|
1169
|
-
);
|
1170
1552
|
} else if (permissionsDiffer) {
|
1171
1553
|
// Only permissions differ
|
1172
|
-
documentsToUpdate.push({
|
1173
|
-
doc: sourceDoc,
|
1174
|
-
targetDoc: existingTargetDoc,
|
1175
|
-
reason: "permissions differ"
|
1554
|
+
documentsToUpdate.push({
|
1555
|
+
doc: sourceDoc,
|
1556
|
+
targetDoc: existingTargetDoc,
|
1557
|
+
reason: "permissions differ",
|
1176
1558
|
});
|
1177
|
-
MessageFormatter.info(
|
1178
|
-
`Document ${sourceDoc.$id} exists but permissions differ - will update`,
|
1179
|
-
{ prefix: "Transfer" }
|
1180
|
-
);
|
1181
1559
|
} else {
|
1182
1560
|
// Document exists with identical content AND permissions, skip
|
1183
1561
|
totalSkipped++;
|
1184
|
-
MessageFormatter.info(`Document ${sourceDoc.$id} exists with matching content and permissions - skipping`, { prefix: "Transfer" });
|
1185
1562
|
}
|
1186
1563
|
}
|
1187
1564
|
}
|
1188
1565
|
|
1189
1566
|
MessageFormatter.info(
|
1190
|
-
`Batch analysis: ${documentsToTransfer.length} to create, ${documentsToUpdate.length} to update, ${totalSkipped} skipped so far`,
|
1567
|
+
`Batch analysis: ${documentsToTransfer.length} to create, ${documentsToUpdate.length} to update, ${totalSkipped} skipped so far`,
|
1191
1568
|
{ prefix: "Transfer" }
|
1192
1569
|
);
|
1193
1570
|
|
@@ -1202,7 +1579,6 @@ export class ComprehensiveTransfer {
|
|
1202
1579
|
documentsToTransfer
|
1203
1580
|
);
|
1204
1581
|
totalTransferred += documentsToTransfer.length;
|
1205
|
-
MessageFormatter.success(`Bulk transferred ${documentsToTransfer.length} new documents`, { prefix: "Transfer" });
|
1206
1582
|
} else {
|
1207
1583
|
// Use individual transfers for smaller batches or non-bulk endpoints
|
1208
1584
|
const transferCount = await this.transferDocumentsIndividual(
|
@@ -1230,11 +1606,12 @@ export class ComprehensiveTransfer {
|
|
1230
1606
|
break;
|
1231
1607
|
}
|
1232
1608
|
|
1233
|
-
lastId =
|
1609
|
+
lastId =
|
1610
|
+
sourceDocuments.documents[sourceDocuments.documents.length - 1].$id;
|
1234
1611
|
}
|
1235
1612
|
|
1236
1613
|
MessageFormatter.info(
|
1237
|
-
`Transfer complete: ${totalTransferred} new, ${totalUpdated} updated, ${totalSkipped} skipped from ${sourceCollectionId} to ${targetCollectionId}`,
|
1614
|
+
`Transfer complete: ${totalTransferred} new, ${totalUpdated} updated, ${totalSkipped} skipped from ${sourceCollectionId} to ${targetCollectionId}`,
|
1238
1615
|
{ prefix: "Transfer" }
|
1239
1616
|
);
|
1240
1617
|
}
|
@@ -1249,29 +1626,33 @@ export class ComprehensiveTransfer {
|
|
1249
1626
|
docIds: string[]
|
1250
1627
|
): Promise<Models.Document[]> {
|
1251
1628
|
const documents: Models.Document[] = [];
|
1252
|
-
|
1629
|
+
|
1253
1630
|
// Split IDs into chunks of 100 for Query.equal limitations
|
1254
1631
|
const idChunks = this.chunkArray(docIds, 100);
|
1255
|
-
|
1632
|
+
|
1256
1633
|
for (const chunk of idChunks) {
|
1257
1634
|
try {
|
1258
|
-
const result = await tryAwaitWithRetry(async () =>
|
1635
|
+
const result = await tryAwaitWithRetry(async () =>
|
1259
1636
|
targetDb.listDocuments(targetDbId, targetCollectionId, [
|
1260
|
-
Query.equal(
|
1261
|
-
Query.limit(100)
|
1637
|
+
Query.equal("$id", chunk),
|
1638
|
+
Query.limit(100),
|
1262
1639
|
])
|
1263
1640
|
);
|
1264
1641
|
documents.push(...result.documents);
|
1265
1642
|
} catch (error) {
|
1266
1643
|
// If query fails, fall back to individual gets (less efficient but more reliable)
|
1267
1644
|
MessageFormatter.warning(
|
1268
|
-
`Batch query failed for ${chunk.length} documents, falling back to individual checks`,
|
1645
|
+
`Batch query failed for ${chunk.length} documents, falling back to individual checks`,
|
1269
1646
|
{ prefix: "Transfer" }
|
1270
1647
|
);
|
1271
|
-
|
1648
|
+
|
1272
1649
|
for (const docId of chunk) {
|
1273
1650
|
try {
|
1274
|
-
const doc = await targetDb.getDocument(
|
1651
|
+
const doc = await targetDb.getDocument(
|
1652
|
+
targetDbId,
|
1653
|
+
targetCollectionId,
|
1654
|
+
docId
|
1655
|
+
);
|
1275
1656
|
documents.push(doc);
|
1276
1657
|
} catch (getError) {
|
1277
1658
|
// Document doesn't exist, which is fine
|
@@ -1279,7 +1660,7 @@ export class ComprehensiveTransfer {
|
|
1279
1660
|
}
|
1280
1661
|
}
|
1281
1662
|
}
|
1282
|
-
|
1663
|
+
|
1283
1664
|
return documents;
|
1284
1665
|
}
|
1285
1666
|
|
@@ -1293,12 +1674,20 @@ export class ComprehensiveTransfer {
|
|
1293
1674
|
documents: Models.Document[]
|
1294
1675
|
): Promise<void> {
|
1295
1676
|
// Prepare documents for bulk upsert
|
1296
|
-
const preparedDocs = documents.map(doc => {
|
1297
|
-
const {
|
1298
|
-
return {
|
1677
|
+
const preparedDocs = documents.map((doc) => {
|
1678
|
+
const {
|
1299
1679
|
$id,
|
1680
|
+
$createdAt,
|
1681
|
+
$updatedAt,
|
1300
1682
|
$permissions,
|
1683
|
+
$databaseId,
|
1684
|
+
$collectionId,
|
1301
1685
|
...docData
|
1686
|
+
} = doc;
|
1687
|
+
return {
|
1688
|
+
$id,
|
1689
|
+
$permissions,
|
1690
|
+
...docData,
|
1302
1691
|
};
|
1303
1692
|
});
|
1304
1693
|
|
@@ -1308,31 +1697,27 @@ export class ComprehensiveTransfer {
|
|
1308
1697
|
|
1309
1698
|
for (const maxBatchSize of batchSizes) {
|
1310
1699
|
const documentBatches = this.chunkArray(preparedDocs, maxBatchSize);
|
1311
|
-
|
1700
|
+
|
1312
1701
|
try {
|
1313
1702
|
for (const batch of documentBatches) {
|
1314
|
-
MessageFormatter.info(`Bulk upserting ${batch.length} documents...`, { prefix: "Transfer" });
|
1315
|
-
|
1316
1703
|
await this.bulkUpsertDocuments(
|
1317
1704
|
this.targetClient,
|
1318
1705
|
targetDbId,
|
1319
1706
|
targetCollectionId,
|
1320
1707
|
batch
|
1321
1708
|
);
|
1322
|
-
|
1323
|
-
MessageFormatter.success(
|
1324
|
-
|
1325
|
-
|
1326
|
-
|
1327
|
-
await new Promise(resolve => setTimeout(resolve, 200));
|
1328
|
-
}
|
1709
|
+
|
1710
|
+
MessageFormatter.success(
|
1711
|
+
`✅ Bulk upserted ${batch.length} documents`,
|
1712
|
+
{ prefix: "Transfer" }
|
1713
|
+
);
|
1329
1714
|
}
|
1330
|
-
|
1715
|
+
|
1331
1716
|
processed = true;
|
1332
1717
|
break; // Success, exit batch size loop
|
1333
1718
|
} catch (error) {
|
1334
1719
|
MessageFormatter.warning(
|
1335
|
-
`Bulk upsert with batch size ${maxBatchSize} failed, trying smaller size...`,
|
1720
|
+
`Bulk upsert with batch size ${maxBatchSize} failed, trying smaller size...`,
|
1336
1721
|
{ prefix: "Transfer" }
|
1337
1722
|
);
|
1338
1723
|
continue; // Try next smaller batch size
|
@@ -1341,12 +1726,17 @@ export class ComprehensiveTransfer {
|
|
1341
1726
|
|
1342
1727
|
if (!processed) {
|
1343
1728
|
MessageFormatter.warning(
|
1344
|
-
`All bulk operations failed, falling back to individual transfers`,
|
1729
|
+
`All bulk operations failed, falling back to individual transfers`,
|
1345
1730
|
{ prefix: "Transfer" }
|
1346
1731
|
);
|
1347
|
-
|
1732
|
+
|
1348
1733
|
// Fall back to individual transfers
|
1349
|
-
await this.transferDocumentsIndividual(
|
1734
|
+
await this.transferDocumentsIndividual(
|
1735
|
+
targetDb,
|
1736
|
+
targetDbId,
|
1737
|
+
targetCollectionId,
|
1738
|
+
documents
|
1739
|
+
);
|
1350
1740
|
}
|
1351
1741
|
}
|
1352
1742
|
|
@@ -1361,24 +1751,30 @@ export class ComprehensiveTransfer {
|
|
1361
1751
|
): Promise<any> {
|
1362
1752
|
const apiPath = `/databases/${dbId}/collections/${collectionId}/documents`;
|
1363
1753
|
const url = new URL(client.config.endpoint + apiPath);
|
1364
|
-
|
1754
|
+
|
1365
1755
|
const headers = {
|
1366
|
-
|
1367
|
-
|
1368
|
-
|
1756
|
+
"Content-Type": "application/json",
|
1757
|
+
"X-Appwrite-Project": client.config.project,
|
1758
|
+
"X-Appwrite-Key": client.config.key,
|
1369
1759
|
};
|
1370
|
-
|
1760
|
+
|
1371
1761
|
const response = await fetch(url.toString(), {
|
1372
|
-
method:
|
1762
|
+
method: "PUT",
|
1373
1763
|
headers,
|
1374
|
-
body: JSON.stringify({ documents })
|
1764
|
+
body: JSON.stringify({ documents }),
|
1375
1765
|
});
|
1376
|
-
|
1766
|
+
|
1377
1767
|
if (!response.ok) {
|
1378
|
-
const errorData: any = await response
|
1379
|
-
|
1768
|
+
const errorData: any = await response
|
1769
|
+
.json()
|
1770
|
+
.catch(() => ({ message: "Unknown error" }));
|
1771
|
+
throw new Error(
|
1772
|
+
`Bulk upsert failed: ${response.status} - ${
|
1773
|
+
errorData.message || "Unknown error"
|
1774
|
+
}`
|
1775
|
+
);
|
1380
1776
|
}
|
1381
|
-
|
1777
|
+
|
1382
1778
|
return await response.json();
|
1383
1779
|
}
|
1384
1780
|
|
@@ -1393,11 +1789,19 @@ export class ComprehensiveTransfer {
|
|
1393
1789
|
): Promise<number> {
|
1394
1790
|
let successCount = 0;
|
1395
1791
|
|
1396
|
-
const transferTasks = documents.map(doc =>
|
1792
|
+
const transferTasks = documents.map((doc) =>
|
1397
1793
|
this.limit(async () => {
|
1398
1794
|
try {
|
1399
|
-
const {
|
1400
|
-
|
1795
|
+
const {
|
1796
|
+
$id,
|
1797
|
+
$createdAt,
|
1798
|
+
$updatedAt,
|
1799
|
+
$permissions,
|
1800
|
+
$databaseId,
|
1801
|
+
$collectionId,
|
1802
|
+
...docData
|
1803
|
+
} = doc;
|
1804
|
+
|
1401
1805
|
await tryAwaitWithRetry(async () =>
|
1402
1806
|
targetDb.createDocument(
|
1403
1807
|
targetDbId,
|
@@ -1409,11 +1813,47 @@ export class ComprehensiveTransfer {
|
|
1409
1813
|
);
|
1410
1814
|
|
1411
1815
|
successCount++;
|
1412
|
-
MessageFormatter.success(`Transferred document ${doc.$id}`, { prefix: "Transfer" });
|
1413
1816
|
} catch (error) {
|
1817
|
+
if (
|
1818
|
+
error instanceof AppwriteException &&
|
1819
|
+
error.message.includes("already exists")
|
1820
|
+
) {
|
1821
|
+
try {
|
1822
|
+
// Update it! It's here because it needs an update or a create
|
1823
|
+
const {
|
1824
|
+
$id,
|
1825
|
+
$createdAt,
|
1826
|
+
$updatedAt,
|
1827
|
+
$permissions,
|
1828
|
+
$databaseId,
|
1829
|
+
$collectionId,
|
1830
|
+
...docData
|
1831
|
+
} = doc;
|
1832
|
+
await tryAwaitWithRetry(async () =>
|
1833
|
+
targetDb.updateDocument(
|
1834
|
+
targetDbId,
|
1835
|
+
targetCollectionId,
|
1836
|
+
doc.$id,
|
1837
|
+
docData,
|
1838
|
+
doc.$permissions
|
1839
|
+
)
|
1840
|
+
);
|
1841
|
+
successCount++;
|
1842
|
+
} catch (updateError) {
|
1843
|
+
// just send the error to the formatter
|
1844
|
+
MessageFormatter.error(
|
1845
|
+
`Failed to transfer document ${doc.$id}`,
|
1846
|
+
updateError instanceof Error
|
1847
|
+
? updateError
|
1848
|
+
: new Error(String(updateError)),
|
1849
|
+
{ prefix: "Transfer" }
|
1850
|
+
);
|
1851
|
+
}
|
1852
|
+
}
|
1853
|
+
|
1414
1854
|
MessageFormatter.error(
|
1415
|
-
`Failed to transfer document ${doc.$id}`,
|
1416
|
-
error instanceof Error ? error : new Error(String(error)),
|
1855
|
+
`Failed to transfer document ${doc.$id}`,
|
1856
|
+
error instanceof Error ? error : new Error(String(error)),
|
1417
1857
|
{ prefix: "Transfer" }
|
1418
1858
|
);
|
1419
1859
|
}
|
@@ -1431,15 +1871,27 @@ export class ComprehensiveTransfer {
|
|
1431
1871
|
targetDb: Databases,
|
1432
1872
|
targetDbId: string,
|
1433
1873
|
targetCollectionId: string,
|
1434
|
-
documentPairs: {
|
1874
|
+
documentPairs: {
|
1875
|
+
doc: Models.Document;
|
1876
|
+
targetDoc: Models.Document;
|
1877
|
+
reason: string;
|
1878
|
+
}[]
|
1435
1879
|
): Promise<number> {
|
1436
1880
|
let successCount = 0;
|
1437
1881
|
|
1438
|
-
const updateTasks = documentPairs.map(({ doc, targetDoc, reason }) =>
|
1882
|
+
const updateTasks = documentPairs.map(({ doc, targetDoc, reason }) =>
|
1439
1883
|
this.limit(async () => {
|
1440
1884
|
try {
|
1441
|
-
const {
|
1442
|
-
|
1885
|
+
const {
|
1886
|
+
$id,
|
1887
|
+
$createdAt,
|
1888
|
+
$updatedAt,
|
1889
|
+
$permissions,
|
1890
|
+
$databaseId,
|
1891
|
+
$collectionId,
|
1892
|
+
...docData
|
1893
|
+
} = doc;
|
1894
|
+
|
1443
1895
|
await tryAwaitWithRetry(async () =>
|
1444
1896
|
targetDb.updateDocument(
|
1445
1897
|
targetDbId,
|
@@ -1451,14 +1903,10 @@ export class ComprehensiveTransfer {
|
|
1451
1903
|
);
|
1452
1904
|
|
1453
1905
|
successCount++;
|
1454
|
-
MessageFormatter.success(
|
1455
|
-
`Updated document ${doc.$id} (${reason}) - permissions: [${targetDoc.$permissions?.join(', ')}] → [${doc.$permissions?.join(', ')}]`,
|
1456
|
-
{ prefix: "Transfer" }
|
1457
|
-
);
|
1458
1906
|
} catch (error) {
|
1459
1907
|
MessageFormatter.error(
|
1460
|
-
`Failed to update document ${doc.$id} (${reason})`,
|
1461
|
-
error instanceof Error ? error : new Error(String(error)),
|
1908
|
+
`Failed to update document ${doc.$id} (${reason})`,
|
1909
|
+
error instanceof Error ? error : new Error(String(error)),
|
1462
1910
|
{ prefix: "Transfer" }
|
1463
1911
|
);
|
1464
1912
|
}
|
@@ -1483,7 +1931,9 @@ export class ComprehensiveTransfer {
|
|
1483
1931
|
/**
|
1484
1932
|
* Helper method to fetch all teams with pagination
|
1485
1933
|
*/
|
1486
|
-
private async fetchAllTeams(
|
1934
|
+
private async fetchAllTeams(
|
1935
|
+
teams: Teams
|
1936
|
+
): Promise<Models.Team<Models.Preferences>[]> {
|
1487
1937
|
const teamsList: Models.Team<Models.Preferences>[] = [];
|
1488
1938
|
let lastId: string | undefined;
|
1489
1939
|
|
@@ -1494,17 +1944,17 @@ export class ComprehensiveTransfer {
|
|
1494
1944
|
}
|
1495
1945
|
|
1496
1946
|
const result = await tryAwaitWithRetry(async () => teams.list(queries));
|
1497
|
-
|
1947
|
+
|
1498
1948
|
if (result.teams.length === 0) {
|
1499
1949
|
break;
|
1500
1950
|
}
|
1501
1951
|
|
1502
1952
|
teamsList.push(...result.teams);
|
1503
|
-
|
1953
|
+
|
1504
1954
|
if (result.teams.length < 100) {
|
1505
1955
|
break;
|
1506
1956
|
}
|
1507
|
-
|
1957
|
+
|
1508
1958
|
lastId = result.teams[result.teams.length - 1].$id;
|
1509
1959
|
}
|
1510
1960
|
|
@@ -1514,7 +1964,9 @@ export class ComprehensiveTransfer {
|
|
1514
1964
|
/**
|
1515
1965
|
* Helper method to fetch all memberships for a team with pagination
|
1516
1966
|
*/
|
1517
|
-
private async fetchAllMemberships(
|
1967
|
+
private async fetchAllMemberships(
|
1968
|
+
teamId: string
|
1969
|
+
): Promise<Models.Membership[]> {
|
1518
1970
|
const membershipsList: Models.Membership[] = [];
|
1519
1971
|
let lastId: string | undefined;
|
1520
1972
|
|
@@ -1524,20 +1976,20 @@ export class ComprehensiveTransfer {
|
|
1524
1976
|
queries.push(Query.cursorAfter(lastId));
|
1525
1977
|
}
|
1526
1978
|
|
1527
|
-
const result = await tryAwaitWithRetry(async () =>
|
1979
|
+
const result = await tryAwaitWithRetry(async () =>
|
1528
1980
|
this.sourceTeams.listMemberships(teamId, queries)
|
1529
1981
|
);
|
1530
|
-
|
1982
|
+
|
1531
1983
|
if (result.memberships.length === 0) {
|
1532
1984
|
break;
|
1533
1985
|
}
|
1534
1986
|
|
1535
1987
|
membershipsList.push(...result.memberships);
|
1536
|
-
|
1988
|
+
|
1537
1989
|
if (result.memberships.length < 100) {
|
1538
1990
|
break;
|
1539
1991
|
}
|
1540
|
-
|
1992
|
+
|
1541
1993
|
lastId = result.memberships[result.memberships.length - 1].$id;
|
1542
1994
|
}
|
1543
1995
|
|
@@ -1548,40 +2000,55 @@ export class ComprehensiveTransfer {
|
|
1548
2000
|
* Helper method to transfer team memberships
|
1549
2001
|
*/
|
1550
2002
|
private async transferTeamMemberships(teamId: string): Promise<void> {
|
1551
|
-
MessageFormatter.info(`Transferring memberships for team ${teamId}`, {
|
2003
|
+
MessageFormatter.info(`Transferring memberships for team ${teamId}`, {
|
2004
|
+
prefix: "Transfer",
|
2005
|
+
});
|
1552
2006
|
|
1553
2007
|
try {
|
1554
2008
|
// Fetch all memberships for this team
|
1555
2009
|
const memberships = await this.fetchAllMemberships(teamId);
|
1556
|
-
|
2010
|
+
|
1557
2011
|
if (memberships.length === 0) {
|
1558
|
-
MessageFormatter.info(`No memberships found for team ${teamId}`, {
|
2012
|
+
MessageFormatter.info(`No memberships found for team ${teamId}`, {
|
2013
|
+
prefix: "Transfer",
|
2014
|
+
});
|
1559
2015
|
return;
|
1560
2016
|
}
|
1561
2017
|
|
1562
|
-
MessageFormatter.info(
|
2018
|
+
MessageFormatter.info(
|
2019
|
+
`Found ${memberships.length} memberships for team ${teamId}`,
|
2020
|
+
{ prefix: "Transfer" }
|
2021
|
+
);
|
1563
2022
|
|
1564
2023
|
let totalTransferred = 0;
|
1565
2024
|
|
1566
2025
|
// Transfer memberships with rate limiting
|
1567
|
-
const transferTasks = memberships.map(membership =>
|
1568
|
-
this.userLimit(async () => {
|
2026
|
+
const transferTasks = memberships.map((membership) =>
|
2027
|
+
this.userLimit(async () => {
|
2028
|
+
// Use userLimit for team operations (more sensitive)
|
1569
2029
|
try {
|
1570
2030
|
// Check if membership already exists and compare roles
|
1571
2031
|
let existingMembership: Models.Membership | null = null;
|
1572
2032
|
try {
|
1573
|
-
existingMembership = await this.targetTeams.getMembership(
|
1574
|
-
|
2033
|
+
existingMembership = await this.targetTeams.getMembership(
|
2034
|
+
teamId,
|
2035
|
+
membership.$id
|
2036
|
+
);
|
2037
|
+
|
1575
2038
|
// Compare roles between source and target membership
|
1576
|
-
const sourceRoles = JSON.stringify(
|
1577
|
-
|
1578
|
-
|
2039
|
+
const sourceRoles = JSON.stringify(
|
2040
|
+
membership.roles?.sort() || []
|
2041
|
+
);
|
2042
|
+
const targetRoles = JSON.stringify(
|
2043
|
+
existingMembership.roles?.sort() || []
|
2044
|
+
);
|
2045
|
+
|
1579
2046
|
if (sourceRoles !== targetRoles) {
|
1580
2047
|
MessageFormatter.warning(
|
1581
|
-
`Membership ${membership.$id} exists but has different roles. Source: ${sourceRoles}, Target: ${targetRoles}`,
|
2048
|
+
`Membership ${membership.$id} exists but has different roles. Source: ${sourceRoles}, Target: ${targetRoles}`,
|
1582
2049
|
{ prefix: "Transfer" }
|
1583
2050
|
);
|
1584
|
-
|
2051
|
+
|
1585
2052
|
// Update membership roles to match source
|
1586
2053
|
try {
|
1587
2054
|
await this.targetTeams.updateMembership(
|
@@ -1589,16 +2056,24 @@ export class ComprehensiveTransfer {
|
|
1589
2056
|
membership.$id,
|
1590
2057
|
membership.roles
|
1591
2058
|
);
|
1592
|
-
MessageFormatter.success(
|
2059
|
+
MessageFormatter.success(
|
2060
|
+
`Updated membership ${membership.$id} roles to match source`,
|
2061
|
+
{ prefix: "Transfer" }
|
2062
|
+
);
|
1593
2063
|
} catch (updateError) {
|
1594
2064
|
MessageFormatter.error(
|
1595
|
-
`Failed to update roles for membership ${membership.$id}`,
|
1596
|
-
updateError instanceof Error
|
2065
|
+
`Failed to update roles for membership ${membership.$id}`,
|
2066
|
+
updateError instanceof Error
|
2067
|
+
? updateError
|
2068
|
+
: new Error(String(updateError)),
|
1597
2069
|
{ prefix: "Transfer" }
|
1598
2070
|
);
|
1599
2071
|
}
|
1600
2072
|
} else {
|
1601
|
-
MessageFormatter.info(
|
2073
|
+
MessageFormatter.info(
|
2074
|
+
`Membership ${membership.$id} already exists with matching roles, skipping`,
|
2075
|
+
{ prefix: "Transfer" }
|
2076
|
+
);
|
1602
2077
|
}
|
1603
2078
|
return;
|
1604
2079
|
} catch (error) {
|
@@ -1610,7 +2085,10 @@ export class ComprehensiveTransfer {
|
|
1610
2085
|
try {
|
1611
2086
|
userData = await this.targetUsers.get(membership.userId);
|
1612
2087
|
} catch (error) {
|
1613
|
-
MessageFormatter.warning(
|
2088
|
+
MessageFormatter.warning(
|
2089
|
+
`User ${membership.userId} not found in target, membership ${membership.$id} may fail`,
|
2090
|
+
{ prefix: "Transfer" }
|
2091
|
+
);
|
1614
2092
|
}
|
1615
2093
|
|
1616
2094
|
// Create membership using the comprehensive user data
|
@@ -1627,38 +2105,87 @@ export class ComprehensiveTransfer {
|
|
1627
2105
|
);
|
1628
2106
|
|
1629
2107
|
totalTransferred++;
|
1630
|
-
MessageFormatter.success(
|
2108
|
+
MessageFormatter.success(
|
2109
|
+
`Transferred membership ${membership.$id} for user ${
|
2110
|
+
userData?.name || membership.userName
|
2111
|
+
}`,
|
2112
|
+
{ prefix: "Transfer" }
|
2113
|
+
);
|
1631
2114
|
} catch (error) {
|
1632
|
-
MessageFormatter.error(
|
2115
|
+
MessageFormatter.error(
|
2116
|
+
`Failed to transfer membership ${membership.$id}`,
|
2117
|
+
error instanceof Error ? error : new Error(String(error)),
|
2118
|
+
{ prefix: "Transfer" }
|
2119
|
+
);
|
1633
2120
|
}
|
1634
2121
|
})
|
1635
2122
|
);
|
1636
2123
|
|
1637
2124
|
await Promise.all(transferTasks);
|
1638
|
-
MessageFormatter.info(
|
2125
|
+
MessageFormatter.info(
|
2126
|
+
`Transferred ${totalTransferred} memberships for team ${teamId}`,
|
2127
|
+
{ prefix: "Transfer" }
|
2128
|
+
);
|
1639
2129
|
} catch (error) {
|
1640
|
-
MessageFormatter.error(
|
2130
|
+
MessageFormatter.error(
|
2131
|
+
`Failed to transfer memberships for team ${teamId}`,
|
2132
|
+
error instanceof Error ? error : new Error(String(error)),
|
2133
|
+
{ prefix: "Transfer" }
|
2134
|
+
);
|
1641
2135
|
}
|
1642
2136
|
}
|
1643
2137
|
|
1644
2138
|
private printSummary(): void {
|
1645
2139
|
const duration = Math.round((Date.now() - this.startTime) / 1000);
|
1646
|
-
|
1647
|
-
MessageFormatter.info("=== COMPREHENSIVE TRANSFER SUMMARY ===", {
|
2140
|
+
|
2141
|
+
MessageFormatter.info("=== COMPREHENSIVE TRANSFER SUMMARY ===", {
|
2142
|
+
prefix: "Transfer",
|
2143
|
+
});
|
1648
2144
|
MessageFormatter.info(`Total Time: ${duration}s`, { prefix: "Transfer" });
|
1649
|
-
MessageFormatter.info(
|
1650
|
-
|
1651
|
-
|
1652
|
-
|
1653
|
-
MessageFormatter.info(
|
1654
|
-
|
1655
|
-
|
1656
|
-
|
1657
|
-
|
2145
|
+
MessageFormatter.info(
|
2146
|
+
`Users: ${this.results.users.transferred} transferred, ${this.results.users.skipped} skipped, ${this.results.users.failed} failed`,
|
2147
|
+
{ prefix: "Transfer" }
|
2148
|
+
);
|
2149
|
+
MessageFormatter.info(
|
2150
|
+
`Teams: ${this.results.teams.transferred} transferred, ${this.results.teams.skipped} skipped, ${this.results.teams.failed} failed`,
|
2151
|
+
{ prefix: "Transfer" }
|
2152
|
+
);
|
2153
|
+
MessageFormatter.info(
|
2154
|
+
`Databases: ${this.results.databases.transferred} transferred, ${this.results.databases.skipped} skipped, ${this.results.databases.failed} failed`,
|
2155
|
+
{ prefix: "Transfer" }
|
2156
|
+
);
|
2157
|
+
MessageFormatter.info(
|
2158
|
+
`Buckets: ${this.results.buckets.transferred} transferred, ${this.results.buckets.skipped} skipped, ${this.results.buckets.failed} failed`,
|
2159
|
+
{ prefix: "Transfer" }
|
2160
|
+
);
|
2161
|
+
MessageFormatter.info(
|
2162
|
+
`Functions: ${this.results.functions.transferred} transferred, ${this.results.functions.skipped} skipped, ${this.results.functions.failed} failed`,
|
2163
|
+
{ prefix: "Transfer" }
|
2164
|
+
);
|
2165
|
+
|
2166
|
+
const totalTransferred =
|
2167
|
+
this.results.users.transferred +
|
2168
|
+
this.results.teams.transferred +
|
2169
|
+
this.results.databases.transferred +
|
2170
|
+
this.results.buckets.transferred +
|
2171
|
+
this.results.functions.transferred;
|
2172
|
+
const totalFailed =
|
2173
|
+
this.results.users.failed +
|
2174
|
+
this.results.teams.failed +
|
2175
|
+
this.results.databases.failed +
|
2176
|
+
this.results.buckets.failed +
|
2177
|
+
this.results.functions.failed;
|
2178
|
+
|
1658
2179
|
if (totalFailed === 0) {
|
1659
|
-
MessageFormatter.success(
|
2180
|
+
MessageFormatter.success(
|
2181
|
+
`All ${totalTransferred} items transferred successfully!`,
|
2182
|
+
{ prefix: "Transfer" }
|
2183
|
+
);
|
1660
2184
|
} else {
|
1661
|
-
MessageFormatter.warning(
|
2185
|
+
MessageFormatter.warning(
|
2186
|
+
`${totalTransferred} items transferred, ${totalFailed} failed`,
|
2187
|
+
{ prefix: "Transfer" }
|
2188
|
+
);
|
1662
2189
|
}
|
1663
2190
|
}
|
1664
|
-
}
|
2191
|
+
}
|