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,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" });
|
@@ -290,7 +322,14 @@ export class ComprehensiveTransfer {
|
|
290
322
|
}
|
291
323
|
// Handle indexes with enhanced status checking
|
292
324
|
MessageFormatter.info(`Creating indexes for collection ${collection.name} with enhanced monitoring...`, { prefix: "Transfer" });
|
293
|
-
|
325
|
+
let indexesSuccess = true;
|
326
|
+
// Check if indexes need to be created ahead of time
|
327
|
+
if (collection.indexes.some((index) => !targetCollection.indexes.some((ti) => ti.key === index.key ||
|
328
|
+
ti.attributes.sort().join(",") ===
|
329
|
+
index.attributes.sort().join(","))) ||
|
330
|
+
collection.indexes.length !== targetCollection.indexes.length) {
|
331
|
+
indexesSuccess = await this.createCollectionIndexesWithStatusCheck(dbId, this.targetDatabases, targetCollection.$id, targetCollection, collection.indexes);
|
332
|
+
}
|
294
333
|
if (!indexesSuccess) {
|
295
334
|
MessageFormatter.error(`Failed to create some indexes for collection ${collection.name}`, undefined, { prefix: "Transfer" });
|
296
335
|
MessageFormatter.warning(`Proceeding with document transfer despite index failures for collection ${collection.name}`, { prefix: "Transfer" });
|
@@ -314,7 +353,9 @@ export class ComprehensiveTransfer {
|
|
314
353
|
* Phase 2: Transfer documents to all collections in the database
|
315
354
|
*/
|
316
355
|
async transferDatabaseDocuments(dbId) {
|
317
|
-
MessageFormatter.info(`Transferring documents for database ${dbId}`, {
|
356
|
+
MessageFormatter.info(`Transferring documents for database ${dbId}`, {
|
357
|
+
prefix: "Transfer",
|
358
|
+
});
|
318
359
|
try {
|
319
360
|
// Get all collections from source database
|
320
361
|
const sourceCollections = await this.fetchAllCollections(dbId, this.sourceDatabases);
|
@@ -338,7 +379,9 @@ export class ComprehensiveTransfer {
|
|
338
379
|
}
|
339
380
|
}
|
340
381
|
async transferAllBuckets() {
|
341
|
-
MessageFormatter.info("Starting bucket transfer phase", {
|
382
|
+
MessageFormatter.info("Starting bucket transfer phase", {
|
383
|
+
prefix: "Transfer",
|
384
|
+
});
|
342
385
|
try {
|
343
386
|
// Get all buckets from source with pagination
|
344
387
|
const allSourceBuckets = await this.fetchAllBuckets(this.sourceStorage);
|
@@ -346,20 +389,24 @@ export class ComprehensiveTransfer {
|
|
346
389
|
if (this.options.dryRun) {
|
347
390
|
let totalFiles = 0;
|
348
391
|
for (const bucket of allSourceBuckets) {
|
349
|
-
const files = await this.sourceStorage.listFiles(bucket.$id, [
|
392
|
+
const files = await this.sourceStorage.listFiles(bucket.$id, [
|
393
|
+
Query.limit(1),
|
394
|
+
]);
|
350
395
|
totalFiles += files.total;
|
351
396
|
}
|
352
397
|
MessageFormatter.info(`DRY RUN: Would transfer ${allSourceBuckets.length} buckets with ${totalFiles} files`, { prefix: "Transfer" });
|
353
398
|
return;
|
354
399
|
}
|
355
|
-
const transferTasks = allSourceBuckets.map(bucket => this.limit(async () => {
|
400
|
+
const transferTasks = allSourceBuckets.map((bucket) => this.limit(async () => {
|
356
401
|
try {
|
357
402
|
// Check if bucket exists in target
|
358
|
-
const existingBucket = allTargetBuckets.find(tb => tb.$id === bucket.$id);
|
403
|
+
const existingBucket = allTargetBuckets.find((tb) => tb.$id === bucket.$id);
|
359
404
|
if (!existingBucket) {
|
360
405
|
// Create bucket with fallback strategy for maximumFileSize
|
361
406
|
await this.createBucketWithFallback(bucket);
|
362
|
-
MessageFormatter.success(`Created bucket: ${bucket.name}`, {
|
407
|
+
MessageFormatter.success(`Created bucket: ${bucket.name}`, {
|
408
|
+
prefix: "Transfer",
|
409
|
+
});
|
363
410
|
}
|
364
411
|
else {
|
365
412
|
// Compare bucket permissions and update if needed
|
@@ -375,7 +422,9 @@ export class ComprehensiveTransfer {
|
|
375
422
|
MessageFormatter.success(`Updated bucket ${bucket.name} to match source`, { prefix: "Transfer" });
|
376
423
|
}
|
377
424
|
catch (updateError) {
|
378
|
-
MessageFormatter.error(`Failed to update bucket ${bucket.name}`, updateError instanceof Error
|
425
|
+
MessageFormatter.error(`Failed to update bucket ${bucket.name}`, updateError instanceof Error
|
426
|
+
? updateError
|
427
|
+
: new Error(String(updateError)), { prefix: "Transfer" });
|
379
428
|
}
|
380
429
|
}
|
381
430
|
else {
|
@@ -393,7 +442,9 @@ export class ComprehensiveTransfer {
|
|
393
442
|
}
|
394
443
|
}));
|
395
444
|
await Promise.all(transferTasks);
|
396
|
-
MessageFormatter.success("Bucket transfer phase completed", {
|
445
|
+
MessageFormatter.success("Bucket transfer phase completed", {
|
446
|
+
prefix: "Transfer",
|
447
|
+
});
|
397
448
|
}
|
398
449
|
catch (error) {
|
399
450
|
MessageFormatter.error("Bucket transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
@@ -434,7 +485,8 @@ export class ComprehensiveTransfer {
|
|
434
485
|
catch (error) {
|
435
486
|
const err = error instanceof Error ? error : new Error(String(error));
|
436
487
|
// Check if the error is related to maximumFileSize validation
|
437
|
-
if (err.message.includes(
|
488
|
+
if (err.message.includes("maximumFileSize") ||
|
489
|
+
err.message.includes("valid range")) {
|
438
490
|
MessageFormatter.warning(`Bucket ${bucket.name}: Failed with maximumFileSize ${sizeToTry}, falling back to smaller sizes...`, { prefix: "Transfer" });
|
439
491
|
// Continue to fallback logic below
|
440
492
|
}
|
@@ -450,11 +502,11 @@ export class ComprehensiveTransfer {
|
|
450
502
|
2_000_000_000, // 2GB
|
451
503
|
1_000_000_000, // 1GB
|
452
504
|
500_000_000, // 500MB
|
453
|
-
100_000_000 // 100MB
|
505
|
+
100_000_000, // 100MB
|
454
506
|
];
|
455
507
|
// Remove sizes that are larger than or equal to the already-tried size
|
456
508
|
const validSizes = fallbackSizes
|
457
|
-
.filter(size => size < sizeToTry)
|
509
|
+
.filter((size) => size < sizeToTry)
|
458
510
|
.sort((a, b) => b - a); // Sort descending
|
459
511
|
let lastError = null;
|
460
512
|
for (const fileSize of validSizes) {
|
@@ -474,7 +526,8 @@ export class ComprehensiveTransfer {
|
|
474
526
|
catch (error) {
|
475
527
|
lastError = error instanceof Error ? error : new Error(String(error));
|
476
528
|
// Check if the error is related to maximumFileSize validation
|
477
|
-
if (lastError.message.includes(
|
529
|
+
if (lastError.message.includes("maximumFileSize") ||
|
530
|
+
lastError.message.includes("valid range")) {
|
478
531
|
MessageFormatter.warning(`Bucket ${bucket.name}: Failed with maximumFileSize ${fileSize}, trying smaller size...`, { prefix: "Transfer" });
|
479
532
|
continue; // Try next smaller size
|
480
533
|
}
|
@@ -486,7 +539,7 @@ export class ComprehensiveTransfer {
|
|
486
539
|
}
|
487
540
|
// If we get here, all fallback sizes failed
|
488
541
|
MessageFormatter.error(`Bucket ${bucket.name}: All fallback file sizes failed. Last error: ${lastError?.message}`, lastError || undefined, { prefix: "Transfer" });
|
489
|
-
throw lastError || new Error(
|
542
|
+
throw lastError || new Error("All fallback file sizes failed");
|
490
543
|
}
|
491
544
|
async transferBucketFiles(sourceBucketId, targetBucketId) {
|
492
545
|
let lastFileId;
|
@@ -500,7 +553,7 @@ export class ComprehensiveTransfer {
|
|
500
553
|
if (files.files.length === 0)
|
501
554
|
break;
|
502
555
|
// Process files with rate limiting
|
503
|
-
const fileTasks = files.files.map(file => this.fileLimit(async () => {
|
556
|
+
const fileTasks = files.files.map((file) => this.fileLimit(async () => {
|
504
557
|
try {
|
505
558
|
// Check if file already exists and compare permissions
|
506
559
|
let existingFile = null;
|
@@ -517,7 +570,9 @@ export class ComprehensiveTransfer {
|
|
517
570
|
MessageFormatter.success(`Updated file ${file.name} permissions to match source`, { prefix: "Transfer" });
|
518
571
|
}
|
519
572
|
catch (updateError) {
|
520
|
-
MessageFormatter.error(`Failed to update permissions for file ${file.name}`, updateError instanceof Error
|
573
|
+
MessageFormatter.error(`Failed to update permissions for file ${file.name}`, updateError instanceof Error
|
574
|
+
? updateError
|
575
|
+
: new Error(String(updateError)), { prefix: "Transfer" });
|
521
576
|
}
|
522
577
|
}
|
523
578
|
else {
|
@@ -538,7 +593,9 @@ export class ComprehensiveTransfer {
|
|
538
593
|
const fileToCreate = InputFile.fromBuffer(new Uint8Array(fileData), file.name);
|
539
594
|
await this.targetStorage.createFile(targetBucketId, file.$id, fileToCreate, file.$permissions);
|
540
595
|
transferredFiles++;
|
541
|
-
MessageFormatter.success(`Transferred file: ${file.name}`, {
|
596
|
+
MessageFormatter.success(`Transferred file: ${file.name}`, {
|
597
|
+
prefix: "Transfer",
|
598
|
+
});
|
542
599
|
}
|
543
600
|
catch (error) {
|
544
601
|
MessageFormatter.error(`Failed to transfer file ${file.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
@@ -558,10 +615,13 @@ export class ComprehensiveTransfer {
|
|
558
615
|
const fileData = await this.sourceStorage.getFileDownload(bucketId, fileId);
|
559
616
|
// Basic validation - ensure file is not empty and not too large
|
560
617
|
if (fileData.byteLength === 0) {
|
561
|
-
MessageFormatter.warning(`File ${fileId} is empty`, {
|
618
|
+
MessageFormatter.warning(`File ${fileId} is empty`, {
|
619
|
+
prefix: "Transfer",
|
620
|
+
});
|
562
621
|
return null;
|
563
622
|
}
|
564
|
-
if (fileData.byteLength > 50 * 1024 * 1024) {
|
623
|
+
if (fileData.byteLength > 50 * 1024 * 1024) {
|
624
|
+
// 50MB limit
|
565
625
|
MessageFormatter.warning(`File ${fileId} is too large (${fileData.byteLength} bytes)`, { prefix: "Transfer" });
|
566
626
|
return null;
|
567
627
|
}
|
@@ -575,24 +635,30 @@ export class ComprehensiveTransfer {
|
|
575
635
|
return null;
|
576
636
|
}
|
577
637
|
// Wait before retry
|
578
|
-
await new Promise(resolve => setTimeout(resolve, 1000 * (4 - attempts)));
|
638
|
+
await new Promise((resolve) => setTimeout(resolve, 1000 * (4 - attempts)));
|
579
639
|
}
|
580
640
|
}
|
581
641
|
return null;
|
582
642
|
}
|
583
643
|
async transferAllFunctions() {
|
584
|
-
MessageFormatter.info("Starting function transfer phase", {
|
644
|
+
MessageFormatter.info("Starting function transfer phase", {
|
645
|
+
prefix: "Transfer",
|
646
|
+
});
|
585
647
|
try {
|
586
|
-
const sourceFunctions = await listFunctions(this.sourceClient, [
|
587
|
-
|
648
|
+
const sourceFunctions = await listFunctions(this.sourceClient, [
|
649
|
+
Query.limit(1000),
|
650
|
+
]);
|
651
|
+
const targetFunctions = await listFunctions(this.targetClient, [
|
652
|
+
Query.limit(1000),
|
653
|
+
]);
|
588
654
|
if (this.options.dryRun) {
|
589
655
|
MessageFormatter.info(`DRY RUN: Would transfer ${sourceFunctions.functions.length} functions`, { prefix: "Transfer" });
|
590
656
|
return;
|
591
657
|
}
|
592
|
-
const transferTasks = sourceFunctions.functions.map(func => this.limit(async () => {
|
658
|
+
const transferTasks = sourceFunctions.functions.map((func) => this.limit(async () => {
|
593
659
|
try {
|
594
660
|
// Check if function exists in target
|
595
|
-
const existingFunc = targetFunctions.functions.find(tf => tf.$id === func.$id);
|
661
|
+
const existingFunc = targetFunctions.functions.find((tf) => tf.$id === func.$id);
|
596
662
|
if (existingFunc) {
|
597
663
|
MessageFormatter.info(`Function ${func.name} already exists, skipping creation`, { prefix: "Transfer" });
|
598
664
|
this.results.functions.skipped++;
|
@@ -637,7 +703,9 @@ export class ComprehensiveTransfer {
|
|
637
703
|
}
|
638
704
|
}));
|
639
705
|
await Promise.all(transferTasks);
|
640
|
-
MessageFormatter.success("Function transfer phase completed", {
|
706
|
+
MessageFormatter.success("Function transfer phase completed", {
|
707
|
+
prefix: "Transfer",
|
708
|
+
});
|
641
709
|
}
|
642
710
|
catch (error) {
|
643
711
|
MessageFormatter.error("Function transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
@@ -720,7 +788,7 @@ export class ComprehensiveTransfer {
|
|
720
788
|
twoWay: attr.twoWay,
|
721
789
|
twoWayKey: attr.twoWayKey,
|
722
790
|
onDelete: attr.onDelete,
|
723
|
-
side: attr.side
|
791
|
+
side: attr.side,
|
724
792
|
};
|
725
793
|
}
|
726
794
|
/**
|
@@ -749,10 +817,13 @@ export class ComprehensiveTransfer {
|
|
749
817
|
let totalSkipped = 0;
|
750
818
|
let totalUpdated = 0;
|
751
819
|
// Check if bulk operations are supported
|
752
|
-
const
|
753
|
-
|
820
|
+
const bulkEnabled = false;
|
821
|
+
// Temporarily disable to see if it fixes my permissions issues
|
822
|
+
const supportsBulk = bulkEnabled ? this.options.targetEndpoint.includes("cloud.appwrite.io") : false;
|
754
823
|
if (supportsBulk) {
|
755
|
-
MessageFormatter.info(`Using bulk operations for enhanced performance`, {
|
824
|
+
MessageFormatter.info(`Using bulk operations for enhanced performance`, {
|
825
|
+
prefix: "Transfer",
|
826
|
+
});
|
756
827
|
}
|
757
828
|
while (true) {
|
758
829
|
// Fetch source documents in larger batches (1000 instead of 50)
|
@@ -766,12 +837,12 @@ export class ComprehensiveTransfer {
|
|
766
837
|
}
|
767
838
|
MessageFormatter.info(`Processing batch of ${sourceDocuments.documents.length} source documents`, { prefix: "Transfer" });
|
768
839
|
// Extract document IDs from the current batch
|
769
|
-
const sourceDocIds = sourceDocuments.documents.map(doc => doc.$id);
|
840
|
+
const sourceDocIds = sourceDocuments.documents.map((doc) => doc.$id);
|
770
841
|
// Fetch existing documents from target in a single query
|
771
842
|
const existingTargetDocs = await this.fetchTargetDocumentsBatch(targetDb, targetDbId, targetCollectionId, sourceDocIds);
|
772
843
|
// Create a map for quick lookup of existing documents
|
773
844
|
const existingDocsMap = new Map();
|
774
|
-
existingTargetDocs.forEach(doc => {
|
845
|
+
existingTargetDocs.forEach((doc) => {
|
775
846
|
existingDocsMap.set(doc.$id, doc);
|
776
847
|
});
|
777
848
|
// Filter documents based on existence, content comparison, and permission comparison
|
@@ -785,9 +856,10 @@ export class ComprehensiveTransfer {
|
|
785
856
|
}
|
786
857
|
else {
|
787
858
|
// Document exists, compare both content and permissions
|
788
|
-
const sourcePermissions =
|
789
|
-
const targetPermissions =
|
790
|
-
const permissionsDiffer = sourcePermissions !== targetPermissions
|
859
|
+
const sourcePermissions = Array.from(new Set(sourceDoc.$permissions || [])).sort();
|
860
|
+
const targetPermissions = Array.from(new Set(existingTargetDoc.$permissions || [])).sort();
|
861
|
+
const permissionsDiffer = sourcePermissions.join(",") !== targetPermissions.join(",") ||
|
862
|
+
sourcePermissions.length !== targetPermissions.length;
|
791
863
|
// Use objectNeedsUpdate to compare document content (excluding system fields)
|
792
864
|
const contentDiffers = objectNeedsUpdate(existingTargetDoc, sourceDoc);
|
793
865
|
if (contentDiffers && permissionsDiffer) {
|
@@ -795,32 +867,28 @@ export class ComprehensiveTransfer {
|
|
795
867
|
documentsToUpdate.push({
|
796
868
|
doc: sourceDoc,
|
797
869
|
targetDoc: existingTargetDoc,
|
798
|
-
reason: "content and permissions differ"
|
870
|
+
reason: "content and permissions differ",
|
799
871
|
});
|
800
|
-
MessageFormatter.info(`Document ${sourceDoc.$id} exists but content and permissions differ - will update`, { prefix: "Transfer" });
|
801
872
|
}
|
802
873
|
else if (contentDiffers) {
|
803
874
|
// Only content differs
|
804
875
|
documentsToUpdate.push({
|
805
876
|
doc: sourceDoc,
|
806
877
|
targetDoc: existingTargetDoc,
|
807
|
-
reason: "content differs"
|
878
|
+
reason: "content differs",
|
808
879
|
});
|
809
|
-
MessageFormatter.info(`Document ${sourceDoc.$id} exists but content differs - will update`, { prefix: "Transfer" });
|
810
880
|
}
|
811
881
|
else if (permissionsDiffer) {
|
812
882
|
// Only permissions differ
|
813
883
|
documentsToUpdate.push({
|
814
884
|
doc: sourceDoc,
|
815
885
|
targetDoc: existingTargetDoc,
|
816
|
-
reason: "permissions differ"
|
886
|
+
reason: "permissions differ",
|
817
887
|
});
|
818
|
-
MessageFormatter.info(`Document ${sourceDoc.$id} exists but permissions differ - will update`, { prefix: "Transfer" });
|
819
888
|
}
|
820
889
|
else {
|
821
890
|
// Document exists with identical content AND permissions, skip
|
822
891
|
totalSkipped++;
|
823
|
-
MessageFormatter.info(`Document ${sourceDoc.$id} exists with matching content and permissions - skipping`, { prefix: "Transfer" });
|
824
892
|
}
|
825
893
|
}
|
826
894
|
}
|
@@ -831,7 +899,6 @@ export class ComprehensiveTransfer {
|
|
831
899
|
// Use bulk operations for large batches
|
832
900
|
await this.transferDocumentsBulk(targetDb, targetDbId, targetCollectionId, documentsToTransfer);
|
833
901
|
totalTransferred += documentsToTransfer.length;
|
834
|
-
MessageFormatter.success(`Bulk transferred ${documentsToTransfer.length} new documents`, { prefix: "Transfer" });
|
835
902
|
}
|
836
903
|
else {
|
837
904
|
// Use individual transfers for smaller batches or non-bulk endpoints
|
@@ -847,7 +914,8 @@ export class ComprehensiveTransfer {
|
|
847
914
|
if (sourceDocuments.documents.length < 1000) {
|
848
915
|
break;
|
849
916
|
}
|
850
|
-
lastId =
|
917
|
+
lastId =
|
918
|
+
sourceDocuments.documents[sourceDocuments.documents.length - 1].$id;
|
851
919
|
}
|
852
920
|
MessageFormatter.info(`Transfer complete: ${totalTransferred} new, ${totalUpdated} updated, ${totalSkipped} skipped from ${sourceCollectionId} to ${targetCollectionId}`, { prefix: "Transfer" });
|
853
921
|
}
|
@@ -861,8 +929,8 @@ export class ComprehensiveTransfer {
|
|
861
929
|
for (const chunk of idChunks) {
|
862
930
|
try {
|
863
931
|
const result = await tryAwaitWithRetry(async () => targetDb.listDocuments(targetDbId, targetCollectionId, [
|
864
|
-
Query.equal(
|
865
|
-
Query.limit(100)
|
932
|
+
Query.equal("$id", chunk),
|
933
|
+
Query.limit(100),
|
866
934
|
]));
|
867
935
|
documents.push(...result.documents);
|
868
936
|
}
|
@@ -887,12 +955,12 @@ export class ComprehensiveTransfer {
|
|
887
955
|
*/
|
888
956
|
async transferDocumentsBulk(targetDb, targetDbId, targetCollectionId, documents) {
|
889
957
|
// Prepare documents for bulk upsert
|
890
|
-
const preparedDocs = documents.map(doc => {
|
958
|
+
const preparedDocs = documents.map((doc) => {
|
891
959
|
const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
|
892
960
|
return {
|
893
961
|
$id,
|
894
962
|
$permissions,
|
895
|
-
...docData
|
963
|
+
...docData,
|
896
964
|
};
|
897
965
|
});
|
898
966
|
// Process in smaller chunks for bulk operations (1000 for Pro, 100 for Free tier)
|
@@ -902,13 +970,8 @@ export class ComprehensiveTransfer {
|
|
902
970
|
const documentBatches = this.chunkArray(preparedDocs, maxBatchSize);
|
903
971
|
try {
|
904
972
|
for (const batch of documentBatches) {
|
905
|
-
MessageFormatter.info(`Bulk upserting ${batch.length} documents...`, { prefix: "Transfer" });
|
906
973
|
await this.bulkUpsertDocuments(this.targetClient, targetDbId, targetCollectionId, batch);
|
907
974
|
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
975
|
}
|
913
976
|
processed = true;
|
914
977
|
break; // Success, exit batch size loop
|
@@ -931,18 +994,20 @@ export class ComprehensiveTransfer {
|
|
931
994
|
const apiPath = `/databases/${dbId}/collections/${collectionId}/documents`;
|
932
995
|
const url = new URL(client.config.endpoint + apiPath);
|
933
996
|
const headers = {
|
934
|
-
|
935
|
-
|
936
|
-
|
997
|
+
"Content-Type": "application/json",
|
998
|
+
"X-Appwrite-Project": client.config.project,
|
999
|
+
"X-Appwrite-Key": client.config.key,
|
937
1000
|
};
|
938
1001
|
const response = await fetch(url.toString(), {
|
939
|
-
method:
|
1002
|
+
method: "PUT",
|
940
1003
|
headers,
|
941
|
-
body: JSON.stringify({ documents })
|
1004
|
+
body: JSON.stringify({ documents }),
|
942
1005
|
});
|
943
1006
|
if (!response.ok) {
|
944
|
-
const errorData = await response
|
945
|
-
|
1007
|
+
const errorData = await response
|
1008
|
+
.json()
|
1009
|
+
.catch(() => ({ message: "Unknown error" }));
|
1010
|
+
throw new Error(`Bulk upsert failed: ${response.status} - ${errorData.message || "Unknown error"}`);
|
946
1011
|
}
|
947
1012
|
return await response.json();
|
948
1013
|
}
|
@@ -951,14 +1016,28 @@ export class ComprehensiveTransfer {
|
|
951
1016
|
*/
|
952
1017
|
async transferDocumentsIndividual(targetDb, targetDbId, targetCollectionId, documents) {
|
953
1018
|
let successCount = 0;
|
954
|
-
const transferTasks = documents.map(doc => this.limit(async () => {
|
1019
|
+
const transferTasks = documents.map((doc) => this.limit(async () => {
|
955
1020
|
try {
|
956
1021
|
const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
|
957
1022
|
await tryAwaitWithRetry(async () => targetDb.createDocument(targetDbId, targetCollectionId, doc.$id, docData, doc.$permissions));
|
958
1023
|
successCount++;
|
959
|
-
MessageFormatter.success(`Transferred document ${doc.$id}`, { prefix: "Transfer" });
|
960
1024
|
}
|
961
1025
|
catch (error) {
|
1026
|
+
if (error instanceof AppwriteException &&
|
1027
|
+
error.message.includes("already exists")) {
|
1028
|
+
try {
|
1029
|
+
// Update it! It's here because it needs an update or a create
|
1030
|
+
const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
|
1031
|
+
await tryAwaitWithRetry(async () => targetDb.updateDocument(targetDbId, targetCollectionId, doc.$id, docData, doc.$permissions));
|
1032
|
+
successCount++;
|
1033
|
+
}
|
1034
|
+
catch (updateError) {
|
1035
|
+
// just send the error to the formatter
|
1036
|
+
MessageFormatter.error(`Failed to transfer document ${doc.$id}`, updateError instanceof Error
|
1037
|
+
? updateError
|
1038
|
+
: new Error(String(updateError)), { prefix: "Transfer" });
|
1039
|
+
}
|
1040
|
+
}
|
962
1041
|
MessageFormatter.error(`Failed to transfer document ${doc.$id}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
963
1042
|
}
|
964
1043
|
}));
|
@@ -975,7 +1054,6 @@ export class ComprehensiveTransfer {
|
|
975
1054
|
const { $id, $createdAt, $updatedAt, $permissions, $databaseId, $collectionId, ...docData } = doc;
|
976
1055
|
await tryAwaitWithRetry(async () => targetDb.updateDocument(targetDbId, targetCollectionId, doc.$id, docData, doc.$permissions));
|
977
1056
|
successCount++;
|
978
|
-
MessageFormatter.success(`Updated document ${doc.$id} (${reason}) - permissions: [${targetDoc.$permissions?.join(', ')}] → [${doc.$permissions?.join(', ')}]`, { prefix: "Transfer" });
|
979
1057
|
}
|
980
1058
|
catch (error) {
|
981
1059
|
MessageFormatter.error(`Failed to update document ${doc.$id} (${reason})`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
@@ -1044,18 +1122,23 @@ export class ComprehensiveTransfer {
|
|
1044
1122
|
* Helper method to transfer team memberships
|
1045
1123
|
*/
|
1046
1124
|
async transferTeamMemberships(teamId) {
|
1047
|
-
MessageFormatter.info(`Transferring memberships for team ${teamId}`, {
|
1125
|
+
MessageFormatter.info(`Transferring memberships for team ${teamId}`, {
|
1126
|
+
prefix: "Transfer",
|
1127
|
+
});
|
1048
1128
|
try {
|
1049
1129
|
// Fetch all memberships for this team
|
1050
1130
|
const memberships = await this.fetchAllMemberships(teamId);
|
1051
1131
|
if (memberships.length === 0) {
|
1052
|
-
MessageFormatter.info(`No memberships found for team ${teamId}`, {
|
1132
|
+
MessageFormatter.info(`No memberships found for team ${teamId}`, {
|
1133
|
+
prefix: "Transfer",
|
1134
|
+
});
|
1053
1135
|
return;
|
1054
1136
|
}
|
1055
1137
|
MessageFormatter.info(`Found ${memberships.length} memberships for team ${teamId}`, { prefix: "Transfer" });
|
1056
1138
|
let totalTransferred = 0;
|
1057
1139
|
// Transfer memberships with rate limiting
|
1058
|
-
const transferTasks = memberships.map(membership => this.userLimit(async () => {
|
1140
|
+
const transferTasks = memberships.map((membership) => this.userLimit(async () => {
|
1141
|
+
// Use userLimit for team operations (more sensitive)
|
1059
1142
|
try {
|
1060
1143
|
// Check if membership already exists and compare roles
|
1061
1144
|
let existingMembership = null;
|
@@ -1072,7 +1155,9 @@ export class ComprehensiveTransfer {
|
|
1072
1155
|
MessageFormatter.success(`Updated membership ${membership.$id} roles to match source`, { prefix: "Transfer" });
|
1073
1156
|
}
|
1074
1157
|
catch (updateError) {
|
1075
|
-
MessageFormatter.error(`Failed to update roles for membership ${membership.$id}`, updateError instanceof Error
|
1158
|
+
MessageFormatter.error(`Failed to update roles for membership ${membership.$id}`, updateError instanceof Error
|
1159
|
+
? updateError
|
1160
|
+
: new Error(String(updateError)), { prefix: "Transfer" });
|
1076
1161
|
}
|
1077
1162
|
}
|
1078
1163
|
else {
|
@@ -1114,15 +1199,25 @@ export class ComprehensiveTransfer {
|
|
1114
1199
|
}
|
1115
1200
|
printSummary() {
|
1116
1201
|
const duration = Math.round((Date.now() - this.startTime) / 1000);
|
1117
|
-
MessageFormatter.info("=== COMPREHENSIVE TRANSFER SUMMARY ===", {
|
1202
|
+
MessageFormatter.info("=== COMPREHENSIVE TRANSFER SUMMARY ===", {
|
1203
|
+
prefix: "Transfer",
|
1204
|
+
});
|
1118
1205
|
MessageFormatter.info(`Total Time: ${duration}s`, { prefix: "Transfer" });
|
1119
1206
|
MessageFormatter.info(`Users: ${this.results.users.transferred} transferred, ${this.results.users.skipped} skipped, ${this.results.users.failed} failed`, { prefix: "Transfer" });
|
1120
1207
|
MessageFormatter.info(`Teams: ${this.results.teams.transferred} transferred, ${this.results.teams.skipped} skipped, ${this.results.teams.failed} failed`, { prefix: "Transfer" });
|
1121
1208
|
MessageFormatter.info(`Databases: ${this.results.databases.transferred} transferred, ${this.results.databases.skipped} skipped, ${this.results.databases.failed} failed`, { prefix: "Transfer" });
|
1122
1209
|
MessageFormatter.info(`Buckets: ${this.results.buckets.transferred} transferred, ${this.results.buckets.skipped} skipped, ${this.results.buckets.failed} failed`, { prefix: "Transfer" });
|
1123
1210
|
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
|
-
|
1211
|
+
const totalTransferred = this.results.users.transferred +
|
1212
|
+
this.results.teams.transferred +
|
1213
|
+
this.results.databases.transferred +
|
1214
|
+
this.results.buckets.transferred +
|
1215
|
+
this.results.functions.transferred;
|
1216
|
+
const totalFailed = this.results.users.failed +
|
1217
|
+
this.results.teams.failed +
|
1218
|
+
this.results.databases.failed +
|
1219
|
+
this.results.buckets.failed +
|
1220
|
+
this.results.functions.failed;
|
1126
1221
|
if (totalFailed === 0) {
|
1127
1222
|
MessageFormatter.success(`All ${totalTransferred} items transferred successfully!`, { prefix: "Transfer" });
|
1128
1223
|
}
|