appwrite-utils-cli 1.2.12 → 1.2.16
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 +1 -0
- package/dist/collections/indexes.js +6 -7
- package/dist/functions/methods.d.ts +1 -0
- package/dist/functions/methods.js +5 -0
- package/dist/migrations/appwriteToX.js +2 -10
- package/dist/migrations/comprehensiveTransfer.js +187 -100
- package/package.json +1 -1
- package/src/collections/indexes.ts +6 -7
- package/src/functions/methods.ts +10 -0
- package/src/migrations/appwriteToX.ts +3 -17
- package/src/migrations/comprehensiveTransfer.ts +877 -364
@@ -1,12 +1,12 @@
|
|
1
|
-
import { converterFunctions, tryAwaitWithRetry, parseAttribute, objectNeedsUpdate } from "appwrite-utils";
|
2
|
-
import { Client, Databases, Storage, Users, Functions, Teams, Query, } from "node-appwrite";
|
1
|
+
import { converterFunctions, tryAwaitWithRetry, parseAttribute, objectNeedsUpdate, } from "appwrite-utils";
|
2
|
+
import { Client, Databases, Storage, Users, Functions, Teams, Query, AppwriteException, } from "node-appwrite";
|
3
3
|
import { InputFile } from "node-appwrite/file";
|
4
4
|
import { MessageFormatter } from "../shared/messageFormatter.js";
|
5
5
|
import { ProgressManager } from "../shared/progressManager.js";
|
6
6
|
import { getClient } from "../utils/getClientFromConfig.js";
|
7
|
-
import { transferDatabaseLocalToLocal, transferDatabaseLocalToRemote, transferStorageLocalToLocal, transferStorageLocalToRemote, transferUsersLocalToRemote } from "./transfer.js";
|
7
|
+
import { transferDatabaseLocalToLocal, transferDatabaseLocalToRemote, transferStorageLocalToLocal, transferStorageLocalToRemote, transferUsersLocalToRemote, } from "./transfer.js";
|
8
8
|
import { deployLocalFunction } from "../functions/deployments.js";
|
9
|
-
import { listFunctions, downloadLatestFunctionDeployment } from "../functions/methods.js";
|
9
|
+
import { listFunctions, downloadLatestFunctionDeployment, } from "../functions/methods.js";
|
10
10
|
import pLimit from "p-limit";
|
11
11
|
import chalk from "chalk";
|
12
12
|
import { join } from "node:path";
|
@@ -66,9 +66,13 @@ export class ComprehensiveTransfer {
|
|
66
66
|
}
|
67
67
|
async execute() {
|
68
68
|
try {
|
69
|
-
MessageFormatter.info("Starting comprehensive transfer", {
|
69
|
+
MessageFormatter.info("Starting comprehensive transfer", {
|
70
|
+
prefix: "Transfer",
|
71
|
+
});
|
70
72
|
if (this.options.dryRun) {
|
71
|
-
MessageFormatter.info("DRY RUN MODE - No actual changes will be made", {
|
73
|
+
MessageFormatter.info("DRY RUN MODE - No actual changes will be made", {
|
74
|
+
prefix: "Transfer",
|
75
|
+
});
|
72
76
|
}
|
73
77
|
// Show rate limiting configuration
|
74
78
|
const baseLimit = this.options.concurrencyLimit || 10;
|
@@ -111,7 +115,9 @@ export class ComprehensiveTransfer {
|
|
111
115
|
}
|
112
116
|
}
|
113
117
|
async transferAllUsers() {
|
114
|
-
MessageFormatter.info("Starting user transfer phase", {
|
118
|
+
MessageFormatter.info("Starting user transfer phase", {
|
119
|
+
prefix: "Transfer",
|
120
|
+
});
|
115
121
|
if (this.options.dryRun) {
|
116
122
|
const usersList = await this.sourceUsers.list([Query.limit(1)]);
|
117
123
|
MessageFormatter.info(`DRY RUN: Would transfer ${usersList.total} users`, { prefix: "Transfer" });
|
@@ -125,7 +131,9 @@ export class ComprehensiveTransfer {
|
|
125
131
|
// Get actual count for results
|
126
132
|
const usersList = await this.sourceUsers.list([Query.limit(1)]);
|
127
133
|
this.results.users.transferred = usersList.total;
|
128
|
-
MessageFormatter.success(`User transfer completed`, {
|
134
|
+
MessageFormatter.success(`User transfer completed`, {
|
135
|
+
prefix: "Transfer",
|
136
|
+
});
|
129
137
|
}
|
130
138
|
catch (error) {
|
131
139
|
MessageFormatter.error("User transfer failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
@@ -133,7 +141,9 @@ export class ComprehensiveTransfer {
|
|
133
141
|
}
|
134
142
|
}
|
135
143
|
async transferAllTeams() {
|
136
|
-
MessageFormatter.info("Starting team transfer phase", {
|
144
|
+
MessageFormatter.info("Starting team transfer phase", {
|
145
|
+
prefix: "Transfer",
|
146
|
+
});
|
137
147
|
try {
|
138
148
|
// Fetch all teams from source with pagination
|
139
149
|
const allSourceTeams = await this.fetchAllTeams(this.sourceTeams);
|
@@ -141,37 +151,41 @@ export class ComprehensiveTransfer {
|
|
141
151
|
if (this.options.dryRun) {
|
142
152
|
let totalMemberships = 0;
|
143
153
|
for (const team of allSourceTeams) {
|
144
|
-
const memberships = await this.sourceTeams.listMemberships(team.$id, [
|
154
|
+
const memberships = await this.sourceTeams.listMemberships(team.$id, [
|
155
|
+
Query.limit(1),
|
156
|
+
]);
|
145
157
|
totalMemberships += memberships.total;
|
146
158
|
}
|
147
159
|
MessageFormatter.info(`DRY RUN: Would transfer ${allSourceTeams.length} teams with ${totalMemberships} memberships`, { prefix: "Transfer" });
|
148
160
|
return;
|
149
161
|
}
|
150
|
-
const transferTasks = allSourceTeams.map(team => this.limit(async () => {
|
162
|
+
const transferTasks = allSourceTeams.map((team) => this.limit(async () => {
|
151
163
|
try {
|
152
164
|
// Check if team exists in target
|
153
|
-
const existingTeam = allTargetTeams.find(tt => tt.$id === team.$id);
|
165
|
+
const existingTeam = allTargetTeams.find((tt) => tt.$id === team.$id);
|
154
166
|
if (!existingTeam) {
|
155
167
|
// Fetch all memberships to extract unique roles before creating team
|
156
168
|
MessageFormatter.info(`Fetching memberships for team ${team.name} to extract roles`, { prefix: "Transfer" });
|
157
169
|
const memberships = await this.fetchAllMemberships(team.$id);
|
158
170
|
// Extract unique roles from all memberships
|
159
171
|
const allRoles = new Set();
|
160
|
-
memberships.forEach(membership => {
|
161
|
-
membership.roles.forEach(role => allRoles.add(role));
|
172
|
+
memberships.forEach((membership) => {
|
173
|
+
membership.roles.forEach((role) => allRoles.add(role));
|
162
174
|
});
|
163
175
|
const uniqueRoles = Array.from(allRoles);
|
164
|
-
MessageFormatter.info(`Found ${uniqueRoles.length} unique roles for team ${team.name}: ${uniqueRoles.join(
|
176
|
+
MessageFormatter.info(`Found ${uniqueRoles.length} unique roles for team ${team.name}: ${uniqueRoles.join(", ")}`, { prefix: "Transfer" });
|
165
177
|
// Create team in target with the collected roles
|
166
178
|
await this.targetTeams.create(team.$id, team.name, uniqueRoles);
|
167
|
-
MessageFormatter.success(`Created team: ${team.name} with roles: ${uniqueRoles.join(
|
179
|
+
MessageFormatter.success(`Created team: ${team.name} with roles: ${uniqueRoles.join(", ")}`, { prefix: "Transfer" });
|
168
180
|
}
|
169
181
|
else {
|
170
182
|
MessageFormatter.info(`Team ${team.name} already exists, updating if needed`, { prefix: "Transfer" });
|
171
183
|
// Update team if needed
|
172
184
|
if (existingTeam.name !== team.name) {
|
173
185
|
await this.targetTeams.updateName(team.$id, team.name);
|
174
|
-
MessageFormatter.success(`Updated team name: ${team.name}`, {
|
186
|
+
MessageFormatter.success(`Updated team name: ${team.name}`, {
|
187
|
+
prefix: "Transfer",
|
188
|
+
});
|
175
189
|
}
|
176
190
|
}
|
177
191
|
// Transfer team memberships
|
@@ -185,14 +199,18 @@ export class ComprehensiveTransfer {
|
|
185
199
|
}
|
186
200
|
}));
|
187
201
|
await Promise.all(transferTasks);
|
188
|
-
MessageFormatter.success("Team transfer phase completed", {
|
202
|
+
MessageFormatter.success("Team transfer phase completed", {
|
203
|
+
prefix: "Transfer",
|
204
|
+
});
|
189
205
|
}
|
190
206
|
catch (error) {
|
191
207
|
MessageFormatter.error("Team transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
192
208
|
}
|
193
209
|
}
|
194
210
|
async transferAllDatabases() {
|
195
|
-
MessageFormatter.info("Starting database transfer phase", {
|
211
|
+
MessageFormatter.info("Starting database transfer phase", {
|
212
|
+
prefix: "Transfer",
|
213
|
+
});
|
196
214
|
try {
|
197
215
|
const sourceDatabases = await this.sourceDatabases.list();
|
198
216
|
const targetDatabases = await this.targetDatabases.list();
|
@@ -202,18 +220,22 @@ export class ComprehensiveTransfer {
|
|
202
220
|
}
|
203
221
|
// Phase 1: Create all databases and collections (structure only)
|
204
222
|
MessageFormatter.info("Phase 1: Creating database structures (databases, collections, attributes, indexes)", { prefix: "Transfer" });
|
205
|
-
const structureCreationTasks = sourceDatabases.databases.map(db => this.limit(async () => {
|
223
|
+
const structureCreationTasks = sourceDatabases.databases.map((db) => this.limit(async () => {
|
206
224
|
try {
|
207
225
|
// Check if database exists in target
|
208
|
-
const existingDb = targetDatabases.databases.find(tdb => tdb.$id === db.$id);
|
226
|
+
const existingDb = targetDatabases.databases.find((tdb) => tdb.$id === db.$id);
|
209
227
|
if (!existingDb) {
|
210
228
|
// Create database in target
|
211
229
|
await this.targetDatabases.create(db.$id, db.name, db.enabled);
|
212
|
-
MessageFormatter.success(`Created database: ${db.name}`, {
|
230
|
+
MessageFormatter.success(`Created database: ${db.name}`, {
|
231
|
+
prefix: "Transfer",
|
232
|
+
});
|
213
233
|
}
|
214
234
|
// Create collections, attributes, and indexes WITHOUT transferring documents
|
215
235
|
await this.createDatabaseStructure(db.$id);
|
216
|
-
MessageFormatter.success(`Database structure created: ${db.name}`, {
|
236
|
+
MessageFormatter.success(`Database structure created: ${db.name}`, {
|
237
|
+
prefix: "Transfer",
|
238
|
+
});
|
217
239
|
}
|
218
240
|
catch (error) {
|
219
241
|
MessageFormatter.error(`Database structure creation failed for ${db.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
@@ -223,7 +245,7 @@ export class ComprehensiveTransfer {
|
|
223
245
|
await Promise.all(structureCreationTasks);
|
224
246
|
// Phase 2: Transfer all documents after all structures are created
|
225
247
|
MessageFormatter.info("Phase 2: Transferring documents to all collections", { prefix: "Transfer" });
|
226
|
-
const documentTransferTasks = sourceDatabases.databases.map(db => this.limit(async () => {
|
248
|
+
const documentTransferTasks = sourceDatabases.databases.map((db) => this.limit(async () => {
|
227
249
|
try {
|
228
250
|
// Transfer documents for this database
|
229
251
|
await this.transferDatabaseDocuments(db.$id);
|
@@ -236,7 +258,9 @@ export class ComprehensiveTransfer {
|
|
236
258
|
}
|
237
259
|
}));
|
238
260
|
await Promise.all(documentTransferTasks);
|
239
|
-
MessageFormatter.success("Database transfer phase completed", {
|
261
|
+
MessageFormatter.success("Database transfer phase completed", {
|
262
|
+
prefix: "Transfer",
|
263
|
+
});
|
240
264
|
}
|
241
265
|
catch (error) {
|
242
266
|
MessageFormatter.error("Database transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
@@ -246,7 +270,9 @@ export class ComprehensiveTransfer {
|
|
246
270
|
* Phase 1: Create database structure (collections, attributes, indexes) without transferring documents
|
247
271
|
*/
|
248
272
|
async createDatabaseStructure(dbId) {
|
249
|
-
MessageFormatter.info(`Creating database structure for ${dbId}`, {
|
273
|
+
MessageFormatter.info(`Creating database structure for ${dbId}`, {
|
274
|
+
prefix: "Transfer",
|
275
|
+
});
|
250
276
|
try {
|
251
277
|
// Get all collections from source database
|
252
278
|
const sourceCollections = await this.fetchAllCollections(dbId, this.sourceDatabases);
|
@@ -257,14 +283,18 @@ export class ComprehensiveTransfer {
|
|
257
283
|
try {
|
258
284
|
// Create or update collection in target
|
259
285
|
let targetCollection;
|
260
|
-
const existingCollection = await tryAwaitWithRetry(async () => this.targetDatabases.listCollections(dbId, [
|
286
|
+
const existingCollection = await tryAwaitWithRetry(async () => this.targetDatabases.listCollections(dbId, [
|
287
|
+
Query.equal("$id", collection.$id),
|
288
|
+
]));
|
261
289
|
if (existingCollection.collections.length > 0) {
|
262
290
|
targetCollection = existingCollection.collections[0];
|
263
291
|
MessageFormatter.info(`Collection ${collection.name} exists in target database`, { prefix: "Transfer" });
|
264
292
|
// Update collection if needed
|
265
293
|
if (targetCollection.name !== collection.name ||
|
266
|
-
JSON.stringify(targetCollection.$permissions) !==
|
267
|
-
|
294
|
+
JSON.stringify(targetCollection.$permissions) !==
|
295
|
+
JSON.stringify(collection.$permissions) ||
|
296
|
+
targetCollection.documentSecurity !==
|
297
|
+
collection.documentSecurity ||
|
268
298
|
targetCollection.enabled !== collection.enabled) {
|
269
299
|
targetCollection = await tryAwaitWithRetry(async () => this.targetDatabases.updateCollection(dbId, collection.$id, collection.name, collection.$permissions, collection.documentSecurity, collection.enabled));
|
270
300
|
MessageFormatter.success(`Collection ${collection.name} updated`, { prefix: "Transfer" });
|
@@ -273,11 +303,13 @@ export class ComprehensiveTransfer {
|
|
273
303
|
else {
|
274
304
|
MessageFormatter.info(`Creating collection ${collection.name} in target database...`, { prefix: "Transfer" });
|
275
305
|
targetCollection = await tryAwaitWithRetry(async () => this.targetDatabases.createCollection(dbId, collection.$id, collection.name, collection.$permissions, collection.documentSecurity, collection.enabled));
|
276
|
-
MessageFormatter.success(`Collection ${collection.name} created`, {
|
306
|
+
MessageFormatter.success(`Collection ${collection.name} created`, {
|
307
|
+
prefix: "Transfer",
|
308
|
+
});
|
277
309
|
}
|
278
310
|
// Handle attributes with enhanced status checking
|
279
311
|
MessageFormatter.info(`Creating attributes for collection ${collection.name} with enhanced monitoring...`, { prefix: "Transfer" });
|
280
|
-
const attributesToCreate = collection.attributes.map(attr => parseAttribute(attr));
|
312
|
+
const attributesToCreate = collection.attributes.map((attr) => parseAttribute(attr));
|
281
313
|
const attributesSuccess = await this.createCollectionAttributesWithStatusCheck(this.targetDatabases, dbId, targetCollection, attributesToCreate);
|
282
314
|
if (!attributesSuccess) {
|
283
315
|
MessageFormatter.error(`Failed to create some attributes for collection ${collection.name}`, undefined, { prefix: "Transfer" });
|
@@ -314,7 +346,9 @@ export class ComprehensiveTransfer {
|
|
314
346
|
* Phase 2: Transfer documents to all collections in the database
|
315
347
|
*/
|
316
348
|
async transferDatabaseDocuments(dbId) {
|
317
|
-
MessageFormatter.info(`Transferring documents for database ${dbId}`, {
|
349
|
+
MessageFormatter.info(`Transferring documents for database ${dbId}`, {
|
350
|
+
prefix: "Transfer",
|
351
|
+
});
|
318
352
|
try {
|
319
353
|
// Get all collections from source database
|
320
354
|
const sourceCollections = await this.fetchAllCollections(dbId, this.sourceDatabases);
|
@@ -338,7 +372,9 @@ export class ComprehensiveTransfer {
|
|
338
372
|
}
|
339
373
|
}
|
340
374
|
async transferAllBuckets() {
|
341
|
-
MessageFormatter.info("Starting bucket transfer phase", {
|
375
|
+
MessageFormatter.info("Starting bucket transfer phase", {
|
376
|
+
prefix: "Transfer",
|
377
|
+
});
|
342
378
|
try {
|
343
379
|
// Get all buckets from source with pagination
|
344
380
|
const allSourceBuckets = await this.fetchAllBuckets(this.sourceStorage);
|
@@ -346,20 +382,24 @@ export class ComprehensiveTransfer {
|
|
346
382
|
if (this.options.dryRun) {
|
347
383
|
let totalFiles = 0;
|
348
384
|
for (const bucket of allSourceBuckets) {
|
349
|
-
const files = await this.sourceStorage.listFiles(bucket.$id, [
|
385
|
+
const files = await this.sourceStorage.listFiles(bucket.$id, [
|
386
|
+
Query.limit(1),
|
387
|
+
]);
|
350
388
|
totalFiles += files.total;
|
351
389
|
}
|
352
390
|
MessageFormatter.info(`DRY RUN: Would transfer ${allSourceBuckets.length} buckets with ${totalFiles} files`, { prefix: "Transfer" });
|
353
391
|
return;
|
354
392
|
}
|
355
|
-
const transferTasks = allSourceBuckets.map(bucket => this.limit(async () => {
|
393
|
+
const transferTasks = allSourceBuckets.map((bucket) => this.limit(async () => {
|
356
394
|
try {
|
357
395
|
// Check if bucket exists in target
|
358
|
-
const existingBucket = allTargetBuckets.find(tb => tb.$id === bucket.$id);
|
396
|
+
const existingBucket = allTargetBuckets.find((tb) => tb.$id === bucket.$id);
|
359
397
|
if (!existingBucket) {
|
360
398
|
// Create bucket with fallback strategy for maximumFileSize
|
361
399
|
await this.createBucketWithFallback(bucket);
|
362
|
-
MessageFormatter.success(`Created bucket: ${bucket.name}`, {
|
400
|
+
MessageFormatter.success(`Created bucket: ${bucket.name}`, {
|
401
|
+
prefix: "Transfer",
|
402
|
+
});
|
363
403
|
}
|
364
404
|
else {
|
365
405
|
// Compare bucket permissions and update if needed
|
@@ -375,7 +415,9 @@ export class ComprehensiveTransfer {
|
|
375
415
|
MessageFormatter.success(`Updated bucket ${bucket.name} to match source`, { prefix: "Transfer" });
|
376
416
|
}
|
377
417
|
catch (updateError) {
|
378
|
-
MessageFormatter.error(`Failed to update bucket ${bucket.name}`, updateError instanceof Error
|
418
|
+
MessageFormatter.error(`Failed to update bucket ${bucket.name}`, updateError instanceof Error
|
419
|
+
? updateError
|
420
|
+
: new Error(String(updateError)), { prefix: "Transfer" });
|
379
421
|
}
|
380
422
|
}
|
381
423
|
else {
|
@@ -393,7 +435,9 @@ export class ComprehensiveTransfer {
|
|
393
435
|
}
|
394
436
|
}));
|
395
437
|
await Promise.all(transferTasks);
|
396
|
-
MessageFormatter.success("Bucket transfer phase completed", {
|
438
|
+
MessageFormatter.success("Bucket transfer phase completed", {
|
439
|
+
prefix: "Transfer",
|
440
|
+
});
|
397
441
|
}
|
398
442
|
catch (error) {
|
399
443
|
MessageFormatter.error("Bucket transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
@@ -434,7 +478,8 @@ export class ComprehensiveTransfer {
|
|
434
478
|
catch (error) {
|
435
479
|
const err = error instanceof Error ? error : new Error(String(error));
|
436
480
|
// Check if the error is related to maximumFileSize validation
|
437
|
-
if (err.message.includes(
|
481
|
+
if (err.message.includes("maximumFileSize") ||
|
482
|
+
err.message.includes("valid range")) {
|
438
483
|
MessageFormatter.warning(`Bucket ${bucket.name}: Failed with maximumFileSize ${sizeToTry}, falling back to smaller sizes...`, { prefix: "Transfer" });
|
439
484
|
// Continue to fallback logic below
|
440
485
|
}
|
@@ -450,11 +495,11 @@ export class ComprehensiveTransfer {
|
|
450
495
|
2_000_000_000, // 2GB
|
451
496
|
1_000_000_000, // 1GB
|
452
497
|
500_000_000, // 500MB
|
453
|
-
100_000_000 // 100MB
|
498
|
+
100_000_000, // 100MB
|
454
499
|
];
|
455
500
|
// Remove sizes that are larger than or equal to the already-tried size
|
456
501
|
const validSizes = fallbackSizes
|
457
|
-
.filter(size => size < sizeToTry)
|
502
|
+
.filter((size) => size < sizeToTry)
|
458
503
|
.sort((a, b) => b - a); // Sort descending
|
459
504
|
let lastError = null;
|
460
505
|
for (const fileSize of validSizes) {
|
@@ -474,7 +519,8 @@ export class ComprehensiveTransfer {
|
|
474
519
|
catch (error) {
|
475
520
|
lastError = error instanceof Error ? error : new Error(String(error));
|
476
521
|
// Check if the error is related to maximumFileSize validation
|
477
|
-
if (lastError.message.includes(
|
522
|
+
if (lastError.message.includes("maximumFileSize") ||
|
523
|
+
lastError.message.includes("valid range")) {
|
478
524
|
MessageFormatter.warning(`Bucket ${bucket.name}: Failed with maximumFileSize ${fileSize}, trying smaller size...`, { prefix: "Transfer" });
|
479
525
|
continue; // Try next smaller size
|
480
526
|
}
|
@@ -486,7 +532,7 @@ export class ComprehensiveTransfer {
|
|
486
532
|
}
|
487
533
|
// If we get here, all fallback sizes failed
|
488
534
|
MessageFormatter.error(`Bucket ${bucket.name}: All fallback file sizes failed. Last error: ${lastError?.message}`, lastError || undefined, { prefix: "Transfer" });
|
489
|
-
throw lastError || new Error(
|
535
|
+
throw lastError || new Error("All fallback file sizes failed");
|
490
536
|
}
|
491
537
|
async transferBucketFiles(sourceBucketId, targetBucketId) {
|
492
538
|
let lastFileId;
|
@@ -500,7 +546,7 @@ export class ComprehensiveTransfer {
|
|
500
546
|
if (files.files.length === 0)
|
501
547
|
break;
|
502
548
|
// Process files with rate limiting
|
503
|
-
const fileTasks = files.files.map(file => this.fileLimit(async () => {
|
549
|
+
const fileTasks = files.files.map((file) => this.fileLimit(async () => {
|
504
550
|
try {
|
505
551
|
// Check if file already exists and compare permissions
|
506
552
|
let existingFile = null;
|
@@ -517,7 +563,9 @@ export class ComprehensiveTransfer {
|
|
517
563
|
MessageFormatter.success(`Updated file ${file.name} permissions to match source`, { prefix: "Transfer" });
|
518
564
|
}
|
519
565
|
catch (updateError) {
|
520
|
-
MessageFormatter.error(`Failed to update permissions for file ${file.name}`, updateError instanceof Error
|
566
|
+
MessageFormatter.error(`Failed to update permissions for file ${file.name}`, updateError instanceof Error
|
567
|
+
? updateError
|
568
|
+
: new Error(String(updateError)), { prefix: "Transfer" });
|
521
569
|
}
|
522
570
|
}
|
523
571
|
else {
|
@@ -538,7 +586,9 @@ export class ComprehensiveTransfer {
|
|
538
586
|
const fileToCreate = InputFile.fromBuffer(new Uint8Array(fileData), file.name);
|
539
587
|
await this.targetStorage.createFile(targetBucketId, file.$id, fileToCreate, file.$permissions);
|
540
588
|
transferredFiles++;
|
541
|
-
MessageFormatter.success(`Transferred file: ${file.name}`, {
|
589
|
+
MessageFormatter.success(`Transferred file: ${file.name}`, {
|
590
|
+
prefix: "Transfer",
|
591
|
+
});
|
542
592
|
}
|
543
593
|
catch (error) {
|
544
594
|
MessageFormatter.error(`Failed to transfer file ${file.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
@@ -558,10 +608,13 @@ export class ComprehensiveTransfer {
|
|
558
608
|
const fileData = await this.sourceStorage.getFileDownload(bucketId, fileId);
|
559
609
|
// Basic validation - ensure file is not empty and not too large
|
560
610
|
if (fileData.byteLength === 0) {
|
561
|
-
MessageFormatter.warning(`File ${fileId} is empty`, {
|
611
|
+
MessageFormatter.warning(`File ${fileId} is empty`, {
|
612
|
+
prefix: "Transfer",
|
613
|
+
});
|
562
614
|
return null;
|
563
615
|
}
|
564
|
-
if (fileData.byteLength > 50 * 1024 * 1024) {
|
616
|
+
if (fileData.byteLength > 50 * 1024 * 1024) {
|
617
|
+
// 50MB limit
|
565
618
|
MessageFormatter.warning(`File ${fileId} is too large (${fileData.byteLength} bytes)`, { prefix: "Transfer" });
|
566
619
|
return null;
|
567
620
|
}
|
@@ -575,24 +628,30 @@ export class ComprehensiveTransfer {
|
|
575
628
|
return null;
|
576
629
|
}
|
577
630
|
// Wait before retry
|
578
|
-
await new Promise(resolve => setTimeout(resolve, 1000 * (4 - attempts)));
|
631
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * (4 - attempts)));
|
579
632
|
}
|
580
633
|
}
|
581
634
|
return null;
|
582
635
|
}
|
583
636
|
async transferAllFunctions() {
|
584
|
-
MessageFormatter.info("Starting function transfer phase", {
|
637
|
+
MessageFormatter.info("Starting function transfer phase", {
|
638
|
+
prefix: "Transfer",
|
639
|
+
});
|
585
640
|
try {
|
586
|
-
const sourceFunctions = await listFunctions(this.sourceClient, [
|
587
|
-
|
641
|
+
const sourceFunctions = await listFunctions(this.sourceClient, [
|
642
|
+
Query.limit(1000),
|
643
|
+
]);
|
644
|
+
const targetFunctions = await listFunctions(this.targetClient, [
|
645
|
+
Query.limit(1000),
|
646
|
+
]);
|
588
647
|
if (this.options.dryRun) {
|
589
648
|
MessageFormatter.info(`DRY RUN: Would transfer ${sourceFunctions.functions.length} functions`, { prefix: "Transfer" });
|
590
649
|
return;
|
591
650
|
}
|
592
|
-
const transferTasks = sourceFunctions.functions.map(func => this.limit(async () => {
|
651
|
+
const transferTasks = sourceFunctions.functions.map((func) => this.limit(async () => {
|
593
652
|
try {
|
594
653
|
// Check if function exists in target
|
595
|
-
const existingFunc = targetFunctions.functions.find(tf => tf.$id === func.$id);
|
654
|
+
const existingFunc = targetFunctions.functions.find((tf) => tf.$id === func.$id);
|
596
655
|
if (existingFunc) {
|
597
656
|
MessageFormatter.info(`Function ${func.name} already exists, skipping creation`, { prefix: "Transfer" });
|
598
657
|
this.results.functions.skipped++;
|
@@ -637,7 +696,9 @@ export class ComprehensiveTransfer {
|
|
637
696
|
}
|
638
697
|
}));
|
639
698
|
await Promise.all(transferTasks);
|
640
|
-
MessageFormatter.success("Function transfer phase completed", {
|
699
|
+
MessageFormatter.success("Function transfer phase completed", {
|
700
|
+
prefix: "Transfer",
|
701
|
+
});
|
641
702
|
}
|
642
703
|
catch (error) {
|
643
704
|
MessageFormatter.error("Function transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
@@ -720,7 +781,7 @@ export class ComprehensiveTransfer {
|
|
720
781
|
twoWay: attr.twoWay,
|
721
782
|
twoWayKey: attr.twoWayKey,
|
722
783
|
onDelete: attr.onDelete,
|
723
|
-
side: attr.side
|
784
|
+
side: attr.side,
|
724
785
|
};
|
725
786
|
}
|
726
787
|
/**
|
@@ -749,10 +810,12 @@ export class ComprehensiveTransfer {
|
|
749
810
|
let totalSkipped = 0;
|
750
811
|
let totalUpdated = 0;
|
751
812
|
// Check if bulk operations are supported
|
752
|
-
const supportsBulk = this.options.sourceEndpoint.includes(
|
753
|
-
this.options.targetEndpoint.includes(
|
813
|
+
const supportsBulk = this.options.sourceEndpoint.includes("cloud.appwrite.io") ||
|
814
|
+
this.options.targetEndpoint.includes("cloud.appwrite.io");
|
754
815
|
if (supportsBulk) {
|
755
|
-
MessageFormatter.info(`Using bulk operations for enhanced performance`, {
|
816
|
+
MessageFormatter.info(`Using bulk operations for enhanced performance`, {
|
817
|
+
prefix: "Transfer",
|
818
|
+
});
|
756
819
|
}
|
757
820
|
while (true) {
|
758
821
|
// Fetch source documents in larger batches (1000 instead of 50)
|
@@ -766,12 +829,12 @@ export class ComprehensiveTransfer {
|
|
766
829
|
}
|
767
830
|
MessageFormatter.info(`Processing batch of ${sourceDocuments.documents.length} source documents`, { prefix: "Transfer" });
|
768
831
|
// Extract document IDs from the current batch
|
769
|
-
const sourceDocIds = sourceDocuments.documents.map(doc => doc.$id);
|
832
|
+
const sourceDocIds = sourceDocuments.documents.map((doc) => doc.$id);
|
770
833
|
// Fetch existing documents from target in a single query
|
771
834
|
const existingTargetDocs = await this.fetchTargetDocumentsBatch(targetDb, targetDbId, targetCollectionId, sourceDocIds);
|
772
835
|
// Create a map for quick lookup of existing documents
|
773
836
|
const existingDocsMap = new Map();
|
774
|
-
existingTargetDocs.forEach(doc => {
|
837
|
+
existingTargetDocs.forEach((doc) => {
|
775
838
|
existingDocsMap.set(doc.$id, doc);
|
776
839
|
});
|
777
840
|
// Filter documents based on existence, content comparison, and permission comparison
|
@@ -785,9 +848,10 @@ export class ComprehensiveTransfer {
|
|
785
848
|
}
|
786
849
|
else {
|
787
850
|
// Document exists, compare both content and permissions
|
788
|
-
const sourcePermissions =
|
789
|
-
const targetPermissions =
|
790
|
-
const permissionsDiffer = sourcePermissions !== targetPermissions
|
851
|
+
const sourcePermissions = Array.from(new Set(sourceDoc.$permissions || [])).sort();
|
852
|
+
const targetPermissions = Array.from(new Set(existingTargetDoc.$permissions || [])).sort();
|
853
|
+
const permissionsDiffer = sourcePermissions.join(",") !== targetPermissions.join(",") ||
|
854
|
+
sourcePermissions.length !== targetPermissions.length;
|
791
855
|
// Use objectNeedsUpdate to compare document content (excluding system fields)
|
792
856
|
const contentDiffers = objectNeedsUpdate(existingTargetDoc, sourceDoc);
|
793
857
|
if (contentDiffers && permissionsDiffer) {
|
@@ -795,32 +859,28 @@ export class ComprehensiveTransfer {
|
|
795
859
|
documentsToUpdate.push({
|
796
860
|
doc: sourceDoc,
|
797
861
|
targetDoc: existingTargetDoc,
|
798
|
-
reason: "content and permissions differ"
|
862
|
+
reason: "content and permissions differ",
|
799
863
|
});
|
800
|
-
MessageFormatter.info(`Document ${sourceDoc.$id} exists but content and permissions differ - will update`, { prefix: "Transfer" });
|
801
864
|
}
|
802
865
|
else if (contentDiffers) {
|
803
866
|
// Only content differs
|
804
867
|
documentsToUpdate.push({
|
805
868
|
doc: sourceDoc,
|
806
869
|
targetDoc: existingTargetDoc,
|
807
|
-
reason: "content differs"
|
870
|
+
reason: "content differs",
|
808
871
|
});
|
809
|
-
MessageFormatter.info(`Document ${sourceDoc.$id} exists but content differs - will update`, { prefix: "Transfer" });
|
810
872
|
}
|
811
873
|
else if (permissionsDiffer) {
|
812
874
|
// Only permissions differ
|
813
875
|
documentsToUpdate.push({
|
814
876
|
doc: sourceDoc,
|
815
877
|
targetDoc: existingTargetDoc,
|
816
|
-
reason: "permissions differ"
|
878
|
+
reason: "permissions differ",
|
817
879
|
});
|
818
|
-
MessageFormatter.info(`Document ${sourceDoc.$id} exists but permissions differ - will update`, { prefix: "Transfer" });
|
819
880
|
}
|
820
881
|
else {
|
821
882
|
// Document exists with identical content AND permissions, skip
|
822
883
|
totalSkipped++;
|
823
|
-
MessageFormatter.info(`Document ${sourceDoc.$id} exists with matching content and permissions - skipping`, { prefix: "Transfer" });
|
824
884
|
}
|
825
885
|
}
|
826
886
|
}
|
@@ -831,7 +891,6 @@ export class ComprehensiveTransfer {
|
|
831
891
|
// Use bulk operations for large batches
|
832
892
|
await this.transferDocumentsBulk(targetDb, targetDbId, targetCollectionId, documentsToTransfer);
|
833
893
|
totalTransferred += documentsToTransfer.length;
|
834
|
-
MessageFormatter.success(`Bulk transferred ${documentsToTransfer.length} new documents`, { prefix: "Transfer" });
|
835
894
|
}
|
836
895
|
else {
|
837
896
|
// Use individual transfers for smaller batches or non-bulk endpoints
|
@@ -847,7 +906,8 @@ export class ComprehensiveTransfer {
|
|
847
906
|
if (sourceDocuments.documents.length < 1000) {
|
848
907
|
break;
|
849
908
|
}
|
850
|
-
lastId =
|
909
|
+
lastId =
|
910
|
+
sourceDocuments.documents[sourceDocuments.documents.length - 1].$id;
|
851
911
|
}
|
852
912
|
MessageFormatter.info(`Transfer complete: ${totalTransferred} new, ${totalUpdated} updated, ${totalSkipped} skipped from ${sourceCollectionId} to ${targetCollectionId}`, { prefix: "Transfer" });
|
853
913
|
}
|
@@ -861,8 +921,8 @@ export class ComprehensiveTransfer {
|
|
861
921
|
for (const chunk of idChunks) {
|
862
922
|
try {
|
863
923
|
const result = await tryAwaitWithRetry(async () => targetDb.listDocuments(targetDbId, targetCollectionId, [
|
864
|
-
Query.equal(
|
865
|
-
Query.limit(100)
|
924
|
+
Query.equal("$id", chunk),
|
925
|
+
Query.limit(100),
|
866
926
|
]));
|
867
927
|
documents.push(...result.documents);
|
868
928
|
}
|
@@ -887,12 +947,12 @@ export class ComprehensiveTransfer {
|
|
887
947
|
*/
|
888
948
|
async transferDocumentsBulk(targetDb, targetDbId, targetCollectionId, documents) {
|
889
949
|
// Prepare documents for bulk upsert
|
890
|
-
const preparedDocs = documents.map(doc => {
|
950
|
+
const preparedDocs = documents.map((doc) => {
|
891
951
|
const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
|
892
952
|
return {
|
893
953
|
$id,
|
894
954
|
$permissions,
|
895
|
-
...docData
|
955
|
+
...docData,
|
896
956
|
};
|
897
957
|
});
|
898
958
|
// Process in smaller chunks for bulk operations (1000 for Pro, 100 for Free tier)
|
@@ -902,13 +962,8 @@ export class ComprehensiveTransfer {
|
|
902
962
|
const documentBatches = this.chunkArray(preparedDocs, maxBatchSize);
|
903
963
|
try {
|
904
964
|
for (const batch of documentBatches) {
|
905
|
-
MessageFormatter.info(`Bulk upserting ${batch.length} documents...`, { prefix: "Transfer" });
|
906
965
|
await this.bulkUpsertDocuments(this.targetClient, targetDbId, targetCollectionId, batch);
|
907
966
|
MessageFormatter.success(`✅ Bulk upserted ${batch.length} documents`, { prefix: "Transfer" });
|
908
|
-
// Add delay between batches to respect rate limits
|
909
|
-
if (documentBatches.indexOf(batch) < documentBatches.length - 1) {
|
910
|
-
await new Promise(resolve => setTimeout(resolve, 200));
|
911
|
-
}
|
912
967
|
}
|
913
968
|
processed = true;
|
914
969
|
break; // Success, exit batch size loop
|
@@ -931,18 +986,20 @@ export class ComprehensiveTransfer {
|
|
931
986
|
const apiPath = `/databases/${dbId}/collections/${collectionId}/documents`;
|
932
987
|
const url = new URL(client.config.endpoint + apiPath);
|
933
988
|
const headers = {
|
934
|
-
|
935
|
-
|
936
|
-
|
989
|
+
"Content-Type": "application/json",
|
990
|
+
"X-Appwrite-Project": client.config.project,
|
991
|
+
"X-Appwrite-Key": client.config.key,
|
937
992
|
};
|
938
993
|
const response = await fetch(url.toString(), {
|
939
|
-
method:
|
994
|
+
method: "PUT",
|
940
995
|
headers,
|
941
|
-
body: JSON.stringify({ documents })
|
996
|
+
body: JSON.stringify({ documents }),
|
942
997
|
});
|
943
998
|
if (!response.ok) {
|
944
|
-
const errorData = await response
|
945
|
-
|
999
|
+
const errorData = await response
|
1000
|
+
.json()
|
1001
|
+
.catch(() => ({ message: "Unknown error" }));
|
1002
|
+
throw new Error(`Bulk upsert failed: ${response.status} - ${errorData.message || "Unknown error"}`);
|
946
1003
|
}
|
947
1004
|
return await response.json();
|
948
1005
|
}
|
@@ -951,14 +1008,28 @@ export class ComprehensiveTransfer {
|
|
951
1008
|
*/
|
952
1009
|
async transferDocumentsIndividual(targetDb, targetDbId, targetCollectionId, documents) {
|
953
1010
|
let successCount = 0;
|
954
|
-
const transferTasks = documents.map(doc => this.limit(async () => {
|
1011
|
+
const transferTasks = documents.map((doc) => this.limit(async () => {
|
955
1012
|
try {
|
956
1013
|
const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
|
957
1014
|
await tryAwaitWithRetry(async () => targetDb.createDocument(targetDbId, targetCollectionId, doc.$id, docData, doc.$permissions));
|
958
1015
|
successCount++;
|
959
|
-
MessageFormatter.success(`Transferred document ${doc.$id}`, { prefix: "Transfer" });
|
960
1016
|
}
|
961
1017
|
catch (error) {
|
1018
|
+
if (error instanceof AppwriteException &&
|
1019
|
+
error.message.includes("already exists")) {
|
1020
|
+
try {
|
1021
|
+
// Update it! It's here because it needs an update or a create
|
1022
|
+
const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
|
1023
|
+
await tryAwaitWithRetry(async () => targetDb.updateDocument(targetDbId, targetCollectionId, doc.$id, docData, doc.$permissions));
|
1024
|
+
successCount++;
|
1025
|
+
}
|
1026
|
+
catch (updateError) {
|
1027
|
+
// just send the error to the formatter
|
1028
|
+
MessageFormatter.error(`Failed to transfer document ${doc.$id}`, updateError instanceof Error
|
1029
|
+
? updateError
|
1030
|
+
: new Error(String(updateError)), { prefix: "Transfer" });
|
1031
|
+
}
|
1032
|
+
}
|
962
1033
|
MessageFormatter.error(`Failed to transfer document ${doc.$id}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
963
1034
|
}
|
964
1035
|
}));
|
@@ -975,7 +1046,6 @@ export class ComprehensiveTransfer {
|
|
975
1046
|
const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
|
976
1047
|
await tryAwaitWithRetry(async () => targetDb.updateDocument(targetDbId, targetCollectionId, doc.$id, docData, doc.$permissions));
|
977
1048
|
successCount++;
|
978
|
-
MessageFormatter.success(`Updated document ${doc.$id} (${reason}) - permissions: [${targetDoc.$permissions?.join(', ')}] → [${doc.$permissions?.join(', ')}]`, { prefix: "Transfer" });
|
979
1049
|
}
|
980
1050
|
catch (error) {
|
981
1051
|
MessageFormatter.error(`Failed to update document ${doc.$id} (${reason})`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
@@ -1044,18 +1114,23 @@ export class ComprehensiveTransfer {
|
|
1044
1114
|
* Helper method to transfer team memberships
|
1045
1115
|
*/
|
1046
1116
|
async transferTeamMemberships(teamId) {
|
1047
|
-
MessageFormatter.info(`Transferring memberships for team ${teamId}`, {
|
1117
|
+
MessageFormatter.info(`Transferring memberships for team ${teamId}`, {
|
1118
|
+
prefix: "Transfer",
|
1119
|
+
});
|
1048
1120
|
try {
|
1049
1121
|
// Fetch all memberships for this team
|
1050
1122
|
const memberships = await this.fetchAllMemberships(teamId);
|
1051
1123
|
if (memberships.length === 0) {
|
1052
|
-
MessageFormatter.info(`No memberships found for team ${teamId}`, {
|
1124
|
+
MessageFormatter.info(`No memberships found for team ${teamId}`, {
|
1125
|
+
prefix: "Transfer",
|
1126
|
+
});
|
1053
1127
|
return;
|
1054
1128
|
}
|
1055
1129
|
MessageFormatter.info(`Found ${memberships.length} memberships for team ${teamId}`, { prefix: "Transfer" });
|
1056
1130
|
let totalTransferred = 0;
|
1057
1131
|
// Transfer memberships with rate limiting
|
1058
|
-
const transferTasks = memberships.map(membership => this.userLimit(async () => {
|
1132
|
+
const transferTasks = memberships.map((membership) => this.userLimit(async () => {
|
1133
|
+
// Use userLimit for team operations (more sensitive)
|
1059
1134
|
try {
|
1060
1135
|
// Check if membership already exists and compare roles
|
1061
1136
|
let existingMembership = null;
|
@@ -1072,7 +1147,9 @@ export class ComprehensiveTransfer {
|
|
1072
1147
|
MessageFormatter.success(`Updated membership ${membership.$id} roles to match source`, { prefix: "Transfer" });
|
1073
1148
|
}
|
1074
1149
|
catch (updateError) {
|
1075
|
-
MessageFormatter.error(`Failed to update roles for membership ${membership.$id}`, updateError instanceof Error
|
1150
|
+
MessageFormatter.error(`Failed to update roles for membership ${membership.$id}`, updateError instanceof Error
|
1151
|
+
? updateError
|
1152
|
+
: new Error(String(updateError)), { prefix: "Transfer" });
|
1076
1153
|
}
|
1077
1154
|
}
|
1078
1155
|
else {
|
@@ -1114,15 +1191,25 @@ export class ComprehensiveTransfer {
|
|
1114
1191
|
}
|
1115
1192
|
printSummary() {
|
1116
1193
|
const duration = Math.round((Date.now() - this.startTime) / 1000);
|
1117
|
-
MessageFormatter.info("=== COMPREHENSIVE TRANSFER SUMMARY ===", {
|
1194
|
+
MessageFormatter.info("=== COMPREHENSIVE TRANSFER SUMMARY ===", {
|
1195
|
+
prefix: "Transfer",
|
1196
|
+
});
|
1118
1197
|
MessageFormatter.info(`Total Time: ${duration}s`, { prefix: "Transfer" });
|
1119
1198
|
MessageFormatter.info(`Users: ${this.results.users.transferred} transferred, ${this.results.users.skipped} skipped, ${this.results.users.failed} failed`, { prefix: "Transfer" });
|
1120
1199
|
MessageFormatter.info(`Teams: ${this.results.teams.transferred} transferred, ${this.results.teams.skipped} skipped, ${this.results.teams.failed} failed`, { prefix: "Transfer" });
|
1121
1200
|
MessageFormatter.info(`Databases: ${this.results.databases.transferred} transferred, ${this.results.databases.skipped} skipped, ${this.results.databases.failed} failed`, { prefix: "Transfer" });
|
1122
1201
|
MessageFormatter.info(`Buckets: ${this.results.buckets.transferred} transferred, ${this.results.buckets.skipped} skipped, ${this.results.buckets.failed} failed`, { prefix: "Transfer" });
|
1123
1202
|
MessageFormatter.info(`Functions: ${this.results.functions.transferred} transferred, ${this.results.functions.skipped} skipped, ${this.results.functions.failed} failed`, { prefix: "Transfer" });
|
1124
|
-
const totalTransferred = this.results.users.transferred +
|
1125
|
-
|
1203
|
+
const totalTransferred = this.results.users.transferred +
|
1204
|
+
this.results.teams.transferred +
|
1205
|
+
this.results.databases.transferred +
|
1206
|
+
this.results.buckets.transferred +
|
1207
|
+
this.results.functions.transferred;
|
1208
|
+
const totalFailed = this.results.users.failed +
|
1209
|
+
this.results.teams.failed +
|
1210
|
+
this.results.databases.failed +
|
1211
|
+
this.results.buckets.failed +
|
1212
|
+
this.results.functions.failed;
|
1126
1213
|
if (totalFailed === 0) {
|
1127
1214
|
MessageFormatter.success(`All ${totalTransferred} items transferred successfully!`, { prefix: "Transfer" });
|
1128
1215
|
}
|