appwrite-utils-cli 1.0.6 → 1.0.8
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 +64 -0
- package/dist/config/yamlConfig.js +0 -9
- package/dist/interactiveCLI.d.ts +1 -0
- package/dist/interactiveCLI.js +185 -21
- package/dist/migrations/comprehensiveTransfer.d.ts +66 -0
- package/dist/migrations/comprehensiveTransfer.js +366 -0
- package/dist/migrations/yaml/YamlImportConfigLoader.d.ts +4 -4
- package/dist/utils/loadConfigs.js +5 -5
- package/package.json +1 -1
- package/src/config/yamlConfig.ts +0 -9
- package/src/interactiveCLI.ts +196 -21
- package/src/migrations/comprehensiveTransfer.ts +505 -0
- package/src/utils/loadConfigs.ts +6 -5
@@ -0,0 +1,505 @@
|
|
1
|
+
import { converterFunctions, tryAwaitWithRetry } from "appwrite-utils";
|
2
|
+
import {
|
3
|
+
Client,
|
4
|
+
Databases,
|
5
|
+
Storage,
|
6
|
+
Users,
|
7
|
+
Functions,
|
8
|
+
type Models,
|
9
|
+
Query,
|
10
|
+
} from "node-appwrite";
|
11
|
+
import { InputFile } from "node-appwrite/file";
|
12
|
+
import { MessageFormatter } from "../shared/messageFormatter.js";
|
13
|
+
import { ProgressManager } from "../shared/progressManager.js";
|
14
|
+
import { getClient } from "../utils/getClientFromConfig.js";
|
15
|
+
import {
|
16
|
+
transferDatabaseLocalToLocal,
|
17
|
+
transferDatabaseLocalToRemote,
|
18
|
+
transferStorageLocalToLocal,
|
19
|
+
transferStorageLocalToRemote,
|
20
|
+
transferUsersLocalToRemote
|
21
|
+
} from "./transfer.js";
|
22
|
+
import {
|
23
|
+
deployLocalFunction
|
24
|
+
} from "../functions/deployments.js";
|
25
|
+
import { listFunctions, downloadLatestFunctionDeployment } from "../functions/methods.js";
|
26
|
+
import pLimit from "p-limit";
|
27
|
+
import chalk from "chalk";
|
28
|
+
import { join } from "node:path";
|
29
|
+
import fs from "node:fs";
|
30
|
+
|
31
|
+
export interface ComprehensiveTransferOptions {
|
32
|
+
sourceEndpoint: string;
|
33
|
+
sourceProject: string;
|
34
|
+
sourceKey: string;
|
35
|
+
targetEndpoint: string;
|
36
|
+
targetProject: string;
|
37
|
+
targetKey: string;
|
38
|
+
transferUsers?: boolean;
|
39
|
+
transferDatabases?: boolean;
|
40
|
+
transferBuckets?: boolean;
|
41
|
+
transferFunctions?: boolean;
|
42
|
+
concurrencyLimit?: number; // 5-100 in steps of 5
|
43
|
+
dryRun?: boolean;
|
44
|
+
}
|
45
|
+
|
46
|
+
export interface TransferResults {
|
47
|
+
users: { transferred: number; skipped: number; failed: number };
|
48
|
+
databases: { transferred: number; skipped: number; failed: number };
|
49
|
+
buckets: { transferred: number; skipped: number; failed: number };
|
50
|
+
functions: { transferred: number; skipped: number; failed: number };
|
51
|
+
totalTime: number;
|
52
|
+
}
|
53
|
+
|
54
|
+
export class ComprehensiveTransfer {
|
55
|
+
private sourceClient: Client;
|
56
|
+
private targetClient: Client;
|
57
|
+
private sourceUsers: Users;
|
58
|
+
private targetUsers: Users;
|
59
|
+
private sourceDatabases: Databases;
|
60
|
+
private targetDatabases: Databases;
|
61
|
+
private sourceStorage: Storage;
|
62
|
+
private targetStorage: Storage;
|
63
|
+
private sourceFunctions: Functions;
|
64
|
+
private targetFunctions: Functions;
|
65
|
+
private limit: ReturnType<typeof pLimit>;
|
66
|
+
private userLimit: ReturnType<typeof pLimit>;
|
67
|
+
private fileLimit: ReturnType<typeof pLimit>;
|
68
|
+
private results: TransferResults;
|
69
|
+
private startTime: number;
|
70
|
+
private tempDir: string;
|
71
|
+
|
72
|
+
constructor(private options: ComprehensiveTransferOptions) {
|
73
|
+
this.sourceClient = getClient(
|
74
|
+
options.sourceEndpoint,
|
75
|
+
options.sourceProject,
|
76
|
+
options.sourceKey
|
77
|
+
);
|
78
|
+
this.targetClient = getClient(
|
79
|
+
options.targetEndpoint,
|
80
|
+
options.targetProject,
|
81
|
+
options.targetKey
|
82
|
+
);
|
83
|
+
|
84
|
+
this.sourceUsers = new Users(this.sourceClient);
|
85
|
+
this.targetUsers = new Users(this.targetClient);
|
86
|
+
this.sourceDatabases = new Databases(this.sourceClient);
|
87
|
+
this.targetDatabases = new Databases(this.targetClient);
|
88
|
+
this.sourceStorage = new Storage(this.sourceClient);
|
89
|
+
this.targetStorage = new Storage(this.targetClient);
|
90
|
+
this.sourceFunctions = new Functions(this.sourceClient);
|
91
|
+
this.targetFunctions = new Functions(this.targetClient);
|
92
|
+
|
93
|
+
const baseLimit = options.concurrencyLimit || 10;
|
94
|
+
this.limit = pLimit(baseLimit);
|
95
|
+
|
96
|
+
// Different rate limits for different operations to prevent API throttling
|
97
|
+
// Users: Half speed (more sensitive operations)
|
98
|
+
// Files: Quarter speed (most bandwidth intensive)
|
99
|
+
this.userLimit = pLimit(Math.max(1, Math.floor(baseLimit / 2)));
|
100
|
+
this.fileLimit = pLimit(Math.max(1, Math.floor(baseLimit / 4)));
|
101
|
+
this.results = {
|
102
|
+
users: { transferred: 0, skipped: 0, failed: 0 },
|
103
|
+
databases: { transferred: 0, skipped: 0, failed: 0 },
|
104
|
+
buckets: { transferred: 0, skipped: 0, failed: 0 },
|
105
|
+
functions: { transferred: 0, skipped: 0, failed: 0 },
|
106
|
+
totalTime: 0,
|
107
|
+
};
|
108
|
+
this.startTime = Date.now();
|
109
|
+
this.tempDir = join(process.cwd(), ".appwrite-transfer-temp");
|
110
|
+
}
|
111
|
+
|
112
|
+
async execute(): Promise<TransferResults> {
|
113
|
+
try {
|
114
|
+
MessageFormatter.info("Starting comprehensive transfer", { prefix: "Transfer" });
|
115
|
+
|
116
|
+
if (this.options.dryRun) {
|
117
|
+
MessageFormatter.info("DRY RUN MODE - No actual changes will be made", { prefix: "Transfer" });
|
118
|
+
}
|
119
|
+
|
120
|
+
// Show rate limiting configuration
|
121
|
+
const baseLimit = this.options.concurrencyLimit || 10;
|
122
|
+
const userLimit = Math.max(1, Math.floor(baseLimit / 2));
|
123
|
+
const fileLimit = Math.max(1, Math.floor(baseLimit / 4));
|
124
|
+
|
125
|
+
MessageFormatter.info(`Rate limits: General=${baseLimit}, Users=${userLimit}, Files=${fileLimit}`, { prefix: "Transfer" });
|
126
|
+
|
127
|
+
// Ensure temp directory exists
|
128
|
+
if (!fs.existsSync(this.tempDir)) {
|
129
|
+
fs.mkdirSync(this.tempDir, { recursive: true });
|
130
|
+
}
|
131
|
+
|
132
|
+
// Execute transfers in the correct order
|
133
|
+
if (this.options.transferUsers !== false) {
|
134
|
+
await this.transferAllUsers();
|
135
|
+
}
|
136
|
+
|
137
|
+
if (this.options.transferDatabases !== false) {
|
138
|
+
await this.transferAllDatabases();
|
139
|
+
}
|
140
|
+
|
141
|
+
if (this.options.transferBuckets !== false) {
|
142
|
+
await this.transferAllBuckets();
|
143
|
+
}
|
144
|
+
|
145
|
+
if (this.options.transferFunctions !== false) {
|
146
|
+
await this.transferAllFunctions();
|
147
|
+
}
|
148
|
+
|
149
|
+
this.results.totalTime = Date.now() - this.startTime;
|
150
|
+
this.printSummary();
|
151
|
+
|
152
|
+
return this.results;
|
153
|
+
} catch (error) {
|
154
|
+
MessageFormatter.error("Comprehensive transfer failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
155
|
+
throw error;
|
156
|
+
} finally {
|
157
|
+
// Clean up temp directory
|
158
|
+
if (fs.existsSync(this.tempDir)) {
|
159
|
+
fs.rmSync(this.tempDir, { recursive: true, force: true });
|
160
|
+
}
|
161
|
+
}
|
162
|
+
}
|
163
|
+
|
164
|
+
private async transferAllUsers(): Promise<void> {
|
165
|
+
MessageFormatter.info("Starting user transfer phase", { prefix: "Transfer" });
|
166
|
+
|
167
|
+
if (this.options.dryRun) {
|
168
|
+
const usersList = await this.sourceUsers.list([Query.limit(1)]);
|
169
|
+
MessageFormatter.info(`DRY RUN: Would transfer ${usersList.total} users`, { prefix: "Transfer" });
|
170
|
+
return;
|
171
|
+
}
|
172
|
+
|
173
|
+
try {
|
174
|
+
// Use the existing user transfer function
|
175
|
+
// Note: The rate limiting is handled at the API level, not per-user
|
176
|
+
// since user operations are already sequential in the existing implementation
|
177
|
+
await transferUsersLocalToRemote(
|
178
|
+
this.sourceUsers,
|
179
|
+
this.options.targetEndpoint,
|
180
|
+
this.options.targetProject,
|
181
|
+
this.options.targetKey
|
182
|
+
);
|
183
|
+
|
184
|
+
// Get actual count for results
|
185
|
+
const usersList = await this.sourceUsers.list([Query.limit(1)]);
|
186
|
+
this.results.users.transferred = usersList.total;
|
187
|
+
|
188
|
+
MessageFormatter.success(`User transfer completed`, { prefix: "Transfer" });
|
189
|
+
} catch (error) {
|
190
|
+
MessageFormatter.error("User transfer failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
191
|
+
this.results.users.failed = 1;
|
192
|
+
}
|
193
|
+
}
|
194
|
+
|
195
|
+
private async transferAllDatabases(): Promise<void> {
|
196
|
+
MessageFormatter.info("Starting database transfer phase", { prefix: "Transfer" });
|
197
|
+
|
198
|
+
try {
|
199
|
+
const sourceDatabases = await this.sourceDatabases.list();
|
200
|
+
const targetDatabases = await this.targetDatabases.list();
|
201
|
+
|
202
|
+
if (this.options.dryRun) {
|
203
|
+
MessageFormatter.info(`DRY RUN: Would transfer ${sourceDatabases.databases.length} databases`, { prefix: "Transfer" });
|
204
|
+
return;
|
205
|
+
}
|
206
|
+
|
207
|
+
const transferTasks = sourceDatabases.databases.map(db =>
|
208
|
+
this.limit(async () => {
|
209
|
+
try {
|
210
|
+
// Check if database exists in target
|
211
|
+
const existingDb = targetDatabases.databases.find(tdb => tdb.$id === db.$id);
|
212
|
+
|
213
|
+
if (!existingDb) {
|
214
|
+
// Create database in target
|
215
|
+
await this.targetDatabases.create(db.$id, db.name, db.enabled);
|
216
|
+
MessageFormatter.success(`Created database: ${db.name}`, { prefix: "Transfer" });
|
217
|
+
}
|
218
|
+
|
219
|
+
// Transfer database content
|
220
|
+
await transferDatabaseLocalToRemote(
|
221
|
+
this.sourceDatabases,
|
222
|
+
this.options.targetEndpoint,
|
223
|
+
this.options.targetProject,
|
224
|
+
this.options.targetKey,
|
225
|
+
db.$id,
|
226
|
+
db.$id
|
227
|
+
);
|
228
|
+
|
229
|
+
this.results.databases.transferred++;
|
230
|
+
MessageFormatter.success(`Database ${db.name} transferred successfully`, { prefix: "Transfer" });
|
231
|
+
} catch (error) {
|
232
|
+
MessageFormatter.error(`Database ${db.name} transfer failed`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
233
|
+
this.results.databases.failed++;
|
234
|
+
}
|
235
|
+
})
|
236
|
+
);
|
237
|
+
|
238
|
+
await Promise.all(transferTasks);
|
239
|
+
MessageFormatter.success("Database transfer phase completed", { prefix: "Transfer" });
|
240
|
+
} catch (error) {
|
241
|
+
MessageFormatter.error("Database transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
242
|
+
}
|
243
|
+
}
|
244
|
+
|
245
|
+
private async transferAllBuckets(): Promise<void> {
|
246
|
+
MessageFormatter.info("Starting bucket transfer phase", { prefix: "Transfer" });
|
247
|
+
|
248
|
+
try {
|
249
|
+
const sourceBuckets = await this.sourceStorage.listBuckets();
|
250
|
+
const targetBuckets = await this.targetStorage.listBuckets();
|
251
|
+
|
252
|
+
if (this.options.dryRun) {
|
253
|
+
let totalFiles = 0;
|
254
|
+
for (const bucket of sourceBuckets.buckets) {
|
255
|
+
const files = await this.sourceStorage.listFiles(bucket.$id, [Query.limit(1)]);
|
256
|
+
totalFiles += files.total;
|
257
|
+
}
|
258
|
+
MessageFormatter.info(`DRY RUN: Would transfer ${sourceBuckets.buckets.length} buckets with ${totalFiles} files`, { prefix: "Transfer" });
|
259
|
+
return;
|
260
|
+
}
|
261
|
+
|
262
|
+
const transferTasks = sourceBuckets.buckets.map(bucket =>
|
263
|
+
this.limit(async () => {
|
264
|
+
try {
|
265
|
+
// Check if bucket exists in target
|
266
|
+
const existingBucket = targetBuckets.buckets.find(tb => tb.$id === bucket.$id);
|
267
|
+
|
268
|
+
if (!existingBucket) {
|
269
|
+
// Create bucket in target
|
270
|
+
await this.targetStorage.createBucket(
|
271
|
+
bucket.$id,
|
272
|
+
bucket.name,
|
273
|
+
bucket.$permissions,
|
274
|
+
bucket.fileSecurity,
|
275
|
+
bucket.enabled,
|
276
|
+
bucket.maximumFileSize,
|
277
|
+
bucket.allowedFileExtensions,
|
278
|
+
bucket.compression as any,
|
279
|
+
bucket.encryption,
|
280
|
+
bucket.antivirus
|
281
|
+
);
|
282
|
+
MessageFormatter.success(`Created bucket: ${bucket.name}`, { prefix: "Transfer" });
|
283
|
+
}
|
284
|
+
|
285
|
+
// Transfer bucket files with enhanced validation
|
286
|
+
await this.transferBucketFiles(bucket.$id, bucket.$id);
|
287
|
+
|
288
|
+
this.results.buckets.transferred++;
|
289
|
+
MessageFormatter.success(`Bucket ${bucket.name} transferred successfully`, { prefix: "Transfer" });
|
290
|
+
} catch (error) {
|
291
|
+
MessageFormatter.error(`Bucket ${bucket.name} transfer failed`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
292
|
+
this.results.buckets.failed++;
|
293
|
+
}
|
294
|
+
})
|
295
|
+
);
|
296
|
+
|
297
|
+
await Promise.all(transferTasks);
|
298
|
+
MessageFormatter.success("Bucket transfer phase completed", { prefix: "Transfer" });
|
299
|
+
} catch (error) {
|
300
|
+
MessageFormatter.error("Bucket transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
301
|
+
}
|
302
|
+
}
|
303
|
+
|
304
|
+
private async transferBucketFiles(sourceBucketId: string, targetBucketId: string): Promise<void> {
|
305
|
+
let lastFileId: string | undefined;
|
306
|
+
let transferredFiles = 0;
|
307
|
+
|
308
|
+
while (true) {
|
309
|
+
const queries = [Query.limit(50)]; // Smaller batch size for better rate limiting
|
310
|
+
if (lastFileId) {
|
311
|
+
queries.push(Query.cursorAfter(lastFileId));
|
312
|
+
}
|
313
|
+
|
314
|
+
const files = await this.sourceStorage.listFiles(sourceBucketId, queries);
|
315
|
+
if (files.files.length === 0) break;
|
316
|
+
|
317
|
+
// Process files with rate limiting
|
318
|
+
const fileTasks = files.files.map(file =>
|
319
|
+
this.fileLimit(async () => {
|
320
|
+
try {
|
321
|
+
// Check if file already exists
|
322
|
+
try {
|
323
|
+
await this.targetStorage.getFile(targetBucketId, file.$id);
|
324
|
+
MessageFormatter.info(`File ${file.name} already exists, skipping`, { prefix: "Transfer" });
|
325
|
+
return;
|
326
|
+
} catch (error) {
|
327
|
+
// File doesn't exist, proceed with transfer
|
328
|
+
}
|
329
|
+
|
330
|
+
// Download file with validation
|
331
|
+
const fileData = await this.validateAndDownloadFile(sourceBucketId, file.$id);
|
332
|
+
if (!fileData) {
|
333
|
+
MessageFormatter.warning(`File ${file.name} failed validation, skipping`, { prefix: "Transfer" });
|
334
|
+
return;
|
335
|
+
}
|
336
|
+
|
337
|
+
// Upload file to target
|
338
|
+
const fileToCreate = InputFile.fromBuffer(
|
339
|
+
new Uint8Array(fileData),
|
340
|
+
file.name
|
341
|
+
);
|
342
|
+
|
343
|
+
await this.targetStorage.createFile(
|
344
|
+
targetBucketId,
|
345
|
+
file.$id,
|
346
|
+
fileToCreate,
|
347
|
+
file.$permissions
|
348
|
+
);
|
349
|
+
|
350
|
+
transferredFiles++;
|
351
|
+
MessageFormatter.success(`Transferred file: ${file.name}`, { prefix: "Transfer" });
|
352
|
+
} catch (error) {
|
353
|
+
MessageFormatter.error(`Failed to transfer file ${file.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
354
|
+
}
|
355
|
+
})
|
356
|
+
);
|
357
|
+
|
358
|
+
await Promise.all(fileTasks);
|
359
|
+
|
360
|
+
if (files.files.length < 50) break;
|
361
|
+
lastFileId = files.files[files.files.length - 1].$id;
|
362
|
+
}
|
363
|
+
|
364
|
+
MessageFormatter.info(`Transferred ${transferredFiles} files from bucket ${sourceBucketId}`, { prefix: "Transfer" });
|
365
|
+
}
|
366
|
+
|
367
|
+
private async validateAndDownloadFile(bucketId: string, fileId: string): Promise<ArrayBuffer | null> {
|
368
|
+
let attempts = 3;
|
369
|
+
while (attempts > 0) {
|
370
|
+
try {
|
371
|
+
const fileData = await this.sourceStorage.getFileDownload(bucketId, fileId);
|
372
|
+
|
373
|
+
// Basic validation - ensure file is not empty and not too large
|
374
|
+
if (fileData.byteLength === 0) {
|
375
|
+
MessageFormatter.warning(`File ${fileId} is empty`, { prefix: "Transfer" });
|
376
|
+
return null;
|
377
|
+
}
|
378
|
+
|
379
|
+
if (fileData.byteLength > 50 * 1024 * 1024) { // 50MB limit
|
380
|
+
MessageFormatter.warning(`File ${fileId} is too large (${fileData.byteLength} bytes)`, { prefix: "Transfer" });
|
381
|
+
return null;
|
382
|
+
}
|
383
|
+
|
384
|
+
return fileData;
|
385
|
+
} catch (error) {
|
386
|
+
attempts--;
|
387
|
+
MessageFormatter.warning(`Error downloading file ${fileId}, attempts left: ${attempts}`, { prefix: "Transfer" });
|
388
|
+
if (attempts === 0) {
|
389
|
+
MessageFormatter.error(`Failed to download file ${fileId} after all attempts`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
390
|
+
return null;
|
391
|
+
}
|
392
|
+
// Wait before retry
|
393
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * (4 - attempts)));
|
394
|
+
}
|
395
|
+
}
|
396
|
+
return null;
|
397
|
+
}
|
398
|
+
|
399
|
+
private async transferAllFunctions(): Promise<void> {
|
400
|
+
MessageFormatter.info("Starting function transfer phase", { prefix: "Transfer" });
|
401
|
+
|
402
|
+
try {
|
403
|
+
const sourceFunctions = await listFunctions(this.sourceClient, [Query.limit(1000)]);
|
404
|
+
const targetFunctions = await listFunctions(this.targetClient, [Query.limit(1000)]);
|
405
|
+
|
406
|
+
if (this.options.dryRun) {
|
407
|
+
MessageFormatter.info(`DRY RUN: Would transfer ${sourceFunctions.functions.length} functions`, { prefix: "Transfer" });
|
408
|
+
return;
|
409
|
+
}
|
410
|
+
|
411
|
+
const transferTasks = sourceFunctions.functions.map(func =>
|
412
|
+
this.limit(async () => {
|
413
|
+
try {
|
414
|
+
// Check if function exists in target
|
415
|
+
const existingFunc = targetFunctions.functions.find(tf => tf.$id === func.$id);
|
416
|
+
|
417
|
+
if (existingFunc) {
|
418
|
+
MessageFormatter.info(`Function ${func.name} already exists, skipping creation`, { prefix: "Transfer" });
|
419
|
+
this.results.functions.skipped++;
|
420
|
+
return;
|
421
|
+
}
|
422
|
+
|
423
|
+
// Download function from source
|
424
|
+
const functionPath = await this.downloadFunction(func);
|
425
|
+
if (!functionPath) {
|
426
|
+
MessageFormatter.error(`Failed to download function ${func.name}`, undefined, { prefix: "Transfer" });
|
427
|
+
this.results.functions.failed++;
|
428
|
+
return;
|
429
|
+
}
|
430
|
+
|
431
|
+
// Deploy function to target
|
432
|
+
const functionConfig = {
|
433
|
+
$id: func.$id,
|
434
|
+
name: func.name,
|
435
|
+
runtime: func.runtime as any,
|
436
|
+
execute: func.execute,
|
437
|
+
events: func.events,
|
438
|
+
enabled: func.enabled,
|
439
|
+
logging: func.logging,
|
440
|
+
entrypoint: func.entrypoint,
|
441
|
+
commands: func.commands,
|
442
|
+
scopes: func.scopes as any,
|
443
|
+
timeout: func.timeout,
|
444
|
+
schedule: func.schedule,
|
445
|
+
installationId: func.installationId,
|
446
|
+
providerRepositoryId: func.providerRepositoryId,
|
447
|
+
providerBranch: func.providerBranch,
|
448
|
+
providerSilentMode: func.providerSilentMode,
|
449
|
+
providerRootDirectory: func.providerRootDirectory,
|
450
|
+
specification: func.specification as any,
|
451
|
+
dirPath: functionPath,
|
452
|
+
};
|
453
|
+
|
454
|
+
await deployLocalFunction(this.targetClient, func.name, functionConfig);
|
455
|
+
|
456
|
+
this.results.functions.transferred++;
|
457
|
+
MessageFormatter.success(`Function ${func.name} transferred successfully`, { prefix: "Transfer" });
|
458
|
+
} catch (error) {
|
459
|
+
MessageFormatter.error(`Function ${func.name} transfer failed`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
460
|
+
this.results.functions.failed++;
|
461
|
+
}
|
462
|
+
})
|
463
|
+
);
|
464
|
+
|
465
|
+
await Promise.all(transferTasks);
|
466
|
+
MessageFormatter.success("Function transfer phase completed", { prefix: "Transfer" });
|
467
|
+
} catch (error) {
|
468
|
+
MessageFormatter.error("Function transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
469
|
+
}
|
470
|
+
}
|
471
|
+
|
472
|
+
private async downloadFunction(func: Models.Function): Promise<string | null> {
|
473
|
+
try {
|
474
|
+
const { path } = await downloadLatestFunctionDeployment(
|
475
|
+
this.sourceClient,
|
476
|
+
func.$id,
|
477
|
+
this.tempDir
|
478
|
+
);
|
479
|
+
return path;
|
480
|
+
} catch (error) {
|
481
|
+
MessageFormatter.error(`Failed to download function ${func.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
482
|
+
return null;
|
483
|
+
}
|
484
|
+
}
|
485
|
+
|
486
|
+
private printSummary(): void {
|
487
|
+
const duration = Math.round((Date.now() - this.startTime) / 1000);
|
488
|
+
|
489
|
+
MessageFormatter.info("=== COMPREHENSIVE TRANSFER SUMMARY ===", { prefix: "Transfer" });
|
490
|
+
MessageFormatter.info(`Total Time: ${duration}s`, { prefix: "Transfer" });
|
491
|
+
MessageFormatter.info(`Users: ${this.results.users.transferred} transferred, ${this.results.users.skipped} skipped, ${this.results.users.failed} failed`, { prefix: "Transfer" });
|
492
|
+
MessageFormatter.info(`Databases: ${this.results.databases.transferred} transferred, ${this.results.databases.skipped} skipped, ${this.results.databases.failed} failed`, { prefix: "Transfer" });
|
493
|
+
MessageFormatter.info(`Buckets: ${this.results.buckets.transferred} transferred, ${this.results.buckets.skipped} skipped, ${this.results.buckets.failed} failed`, { prefix: "Transfer" });
|
494
|
+
MessageFormatter.info(`Functions: ${this.results.functions.transferred} transferred, ${this.results.functions.skipped} skipped, ${this.results.functions.failed} failed`, { prefix: "Transfer" });
|
495
|
+
|
496
|
+
const totalTransferred = this.results.users.transferred + this.results.databases.transferred + this.results.buckets.transferred + this.results.functions.transferred;
|
497
|
+
const totalFailed = this.results.users.failed + this.results.databases.failed + this.results.buckets.failed + this.results.functions.failed;
|
498
|
+
|
499
|
+
if (totalFailed === 0) {
|
500
|
+
MessageFormatter.success(`All ${totalTransferred} items transferred successfully!`, { prefix: "Transfer" });
|
501
|
+
} else {
|
502
|
+
MessageFormatter.warning(`${totalTransferred} items transferred, ${totalFailed} failed`, { prefix: "Transfer" });
|
503
|
+
}
|
504
|
+
}
|
505
|
+
}
|
package/src/utils/loadConfigs.ts
CHANGED
@@ -7,6 +7,7 @@ import chalk from "chalk";
|
|
7
7
|
import { findYamlConfig, loadYamlConfig } from "../config/yamlConfig.js";
|
8
8
|
import yaml from "js-yaml";
|
9
9
|
import { z } from "zod";
|
10
|
+
import { MessageFormatter } from "../shared/messageFormatter.js";
|
10
11
|
|
11
12
|
/**
|
12
13
|
* Recursively searches for configuration files starting from the given directory.
|
@@ -88,7 +89,6 @@ const findAppwriteConfigTS = (dir: string, depth: number = 0): string | null =>
|
|
88
89
|
// First check current directory for appwriteConfig.ts
|
89
90
|
for (const entry of entries) {
|
90
91
|
if (entry.isFile() && entry.name === "appwriteConfig.ts") {
|
91
|
-
console.log(`Found appwriteConfig.ts at: ${path.join(dir, entry.name)}`);
|
92
92
|
return path.join(dir, entry.name);
|
93
93
|
}
|
94
94
|
}
|
@@ -102,7 +102,6 @@ const findAppwriteConfigTS = (dir: string, depth: number = 0): string | null =>
|
|
102
102
|
}
|
103
103
|
} catch (error) {
|
104
104
|
// Ignore directory access errors
|
105
|
-
console.log(`Error accessing directory ${dir}:`, error);
|
106
105
|
}
|
107
106
|
|
108
107
|
return null;
|
@@ -149,7 +148,6 @@ export const loadConfigWithPath = async (
|
|
149
148
|
const unregister = register(); // Register tsx enhancement
|
150
149
|
|
151
150
|
try {
|
152
|
-
console.log(`Loading TypeScript config from: ${configPath}`);
|
153
151
|
const configUrl = pathToFileURL(configPath).href;
|
154
152
|
const configModule = (await import(configUrl));
|
155
153
|
config = configModule.default?.default || configModule.default || configModule;
|
@@ -239,7 +237,6 @@ export const loadConfig = async (
|
|
239
237
|
// First try to find and load YAML config
|
240
238
|
const yamlConfigPath = findYamlConfig(configDir);
|
241
239
|
if (yamlConfigPath) {
|
242
|
-
console.log(`Loading YAML config from: ${yamlConfigPath}`);
|
243
240
|
config = await loadYamlConfig(yamlConfigPath);
|
244
241
|
actualConfigPath = yamlConfigPath;
|
245
242
|
}
|
@@ -253,7 +250,6 @@ export const loadConfig = async (
|
|
253
250
|
const unregister = register(); // Register tsx enhancement
|
254
251
|
|
255
252
|
try {
|
256
|
-
console.log(`Loading TypeScript config from: ${configPath}`);
|
257
253
|
const configUrl = pathToFileURL(configPath).href;
|
258
254
|
const configModule = (await import(configUrl));
|
259
255
|
config = configModule.default?.default || configModule.default || configModule;
|
@@ -332,6 +328,11 @@ export const loadConfig = async (
|
|
332
328
|
config.collections = config.collections || [];
|
333
329
|
}
|
334
330
|
|
331
|
+
// Log successful config loading
|
332
|
+
if (actualConfigPath) {
|
333
|
+
MessageFormatter.success(`Loaded config from: ${actualConfigPath}`, { prefix: "Config" });
|
334
|
+
}
|
335
|
+
|
335
336
|
return config;
|
336
337
|
};
|
337
338
|
|