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 CHANGED
@@ -327,6 +327,70 @@ This updated CLI ensures that developers have robust tools at their fingertips t
327
327
 
328
328
  ## Changelog
329
329
 
330
+ ### 1.0.8 - Comprehensive Transfer System with Enhanced Rate Limiting
331
+
332
+ **🚀 Complete Cross-Instance Transfer Solution**
333
+
334
+ #### Comprehensive Transfer System
335
+ - **New CLI Option**: `🚀 Comprehensive transfer (users → databases → buckets → functions)` in interactive mode
336
+ - **Orchestrated Transfer Flow**: Proper execution order (users → databases → buckets → functions) for dependency management
337
+ - **Cross-Instance Support**: Transfer entire Appwrite configurations between different instances/projects
338
+ - **Selective Transfer**: Choose which components to transfer (users, databases, buckets, functions)
339
+ - **Dry Run Mode**: Test transfers without making actual changes
340
+
341
+ #### Enhanced Rate Limiting Strategy
342
+ - **Configurable Limits**: 5 to 100 concurrent operations in steps of 5
343
+ - **Differentiated Rates**: Smart rate limiting based on operation type:
344
+ - **General Operations**: Full rate (databases, functions)
345
+ - **User Operations**: Half rate (more sensitive operations)
346
+ - **File Operations**: Quarter rate (most bandwidth intensive)
347
+ - **Visual Feedback**: Real-time rate limit display during transfers
348
+ - **Intelligent Scaling**: Automatic calculation of optimal rates for different operations
349
+
350
+ #### File Transfer Enhancements
351
+ - **File Validation**: Comprehensive integrity checking (empty files, size limits)
352
+ - **Retry Logic**: Exponential backoff for failed file transfers
353
+ - **Error Handling**: Graceful handling of corrupt/invalid files
354
+ - **Progress Tracking**: Real-time progress for large file transfers
355
+
356
+ #### Function Transfer Integration
357
+ - **Automated Function Migration**: Download from source, redeploy to target
358
+ - **Temporary Management**: Automatic cleanup of downloaded function code
359
+ - **Existing Code Integration**: Leverages existing deployment infrastructure
360
+ - **Configuration Preservation**: Maintains function settings and variables
361
+
362
+ #### User Experience Improvements
363
+ - **Password Reset Warnings**: Clear notifications about Appwrite password limitations
364
+ - **Interactive Configuration**: Step-by-step prompts for source/target setup
365
+ - **Comprehensive Reporting**: Detailed transfer summaries with statistics
366
+ - **Smart Confirmations**: Risk-based confirmations for destructive operations
367
+
368
+ #### Technical Implementation
369
+ - **Rate Limiting**: Uses p-limit for concurrent operation control
370
+ - **Error Resilience**: Robust error handling with detailed user feedback
371
+ - **Memory Management**: Efficient processing of large datasets
372
+ - **Progress Tracking**: Real-time progress bars with ETA calculations
373
+
374
+ #### Usage Examples
375
+ ```bash
376
+ # Interactive mode - select comprehensive transfer
377
+ npx appwrite-utils-cli@latest appwrite-migrate --it
378
+
379
+ # Example rate limiting at 20 concurrent:
380
+ # - General operations: 20 concurrent
381
+ # - User operations: 10 concurrent
382
+ # - File operations: 5 concurrent
383
+ ```
384
+
385
+ **Benefits**:
386
+ - Complete Appwrite instance migration capability
387
+ - Intelligent rate limiting prevents API throttling
388
+ - Enhanced file transfer reliability
389
+ - Comprehensive progress tracking and reporting
390
+ - Maintains data integrity across transfers
391
+
392
+ ### 1.0.7 - Forgot to remove debug logs
393
+
330
394
  ### 1.0.6 - Cross-Language Constants Generation
331
395
 
332
396
  **🚀 Enhanced Developer Experience with Multi-Language Constants**
@@ -243,17 +243,13 @@ export const findYamlConfig = (startDir) => {
243
243
  path.join(startDir, "appwrite.yaml"),
244
244
  path.join(startDir, "appwrite.yml"),
245
245
  ];
246
- console.log(`DEBUG: Checking YAML paths in ${startDir}:`);
247
246
  for (const configPath of possiblePaths) {
248
- console.log(` - ${configPath}: ${fs.existsSync(configPath)}`);
249
247
  if (fs.existsSync(configPath)) {
250
248
  return configPath;
251
249
  }
252
250
  }
253
251
  // Recursively search subdirectories for .appwrite folders
254
- console.log(`DEBUG: Starting recursive search from ${startDir}`);
255
252
  const yamlConfigInSubdirs = findYamlConfigRecursive(startDir);
256
- console.log(`DEBUG: Recursive search result: ${yamlConfigInSubdirs}`);
257
253
  if (yamlConfigInSubdirs) {
258
254
  return yamlConfigInSubdirs;
259
255
  }
@@ -316,23 +312,18 @@ const shouldIgnoreDirectory = (dirName) => {
316
312
  const findYamlConfigRecursive = (dir, depth = 0) => {
317
313
  // Limit search depth to prevent infinite recursion
318
314
  if (depth > 5) {
319
- console.log(`DEBUG: Stopping search at depth ${depth} in ${dir}`);
320
315
  return null;
321
316
  }
322
317
  if (shouldIgnoreDirectory(path.basename(dir))) {
323
- console.log(`DEBUG: Ignoring directory ${dir}`);
324
318
  return null;
325
319
  }
326
320
  try {
327
321
  const entries = fs.readdirSync(dir, { withFileTypes: true });
328
- console.log(`DEBUG: Searching directory ${dir} at depth ${depth}, found ${entries.length} entries`);
329
322
  for (const entry of entries) {
330
323
  if (entry.isDirectory() && !shouldIgnoreDirectory(entry.name)) {
331
324
  const fullPath = path.join(dir, entry.name);
332
- console.log(`DEBUG: Checking subdirectory: ${fullPath}`);
333
325
  // Check if this is an .appwrite directory
334
326
  if (entry.name === ".appwrite") {
335
- console.log(`DEBUG: Found .appwrite directory at ${fullPath}`);
336
327
  const configPaths = [
337
328
  path.join(fullPath, "appwriteConfig.yaml"),
338
329
  path.join(fullPath, "appwriteConfig.yml"),
@@ -34,4 +34,5 @@ export declare class InteractiveCLI {
34
34
  private detectConfigurationType;
35
35
  private buildChoicesList;
36
36
  private migrateTypeScriptConfig;
37
+ private comprehensiveTransfer;
37
38
  }
@@ -6,6 +6,7 @@ import { fetchAllCollections } from "./collections/methods.js";
6
6
  import { listBuckets, createBucket } from "./storage/methods.js";
7
7
  import { Databases, Storage, Client, Compression, Query, Functions, } from "node-appwrite";
8
8
  import { getClient } from "./utils/getClientFromConfig.js";
9
+ import { ComprehensiveTransfer } from "./migrations/comprehensiveTransfer.js";
9
10
  import { AppwriteFunctionSchema, parseAttribute, PermissionToAppwritePermission, RuntimeSchema, permissionSchema, } from "appwrite-utils";
10
11
  import { ulid } from "ulidx";
11
12
  import chalk from "chalk";
@@ -24,24 +25,25 @@ import { findYamlConfig, addFunctionToYamlConfig } from "./config/yamlConfig.js"
24
25
  var CHOICES;
25
26
  (function (CHOICES) {
26
27
  CHOICES["MIGRATE_CONFIG"] = "\uD83D\uDD04 Migrate TypeScript config to YAML (.appwrite structure)";
27
- CHOICES["CREATE_COLLECTION_CONFIG"] = "Create collection config file";
28
- CHOICES["CREATE_FUNCTION"] = "Create a new function, from scratch or using a template";
29
- CHOICES["DEPLOY_FUNCTION"] = "Deploy function(s)";
30
- CHOICES["DELETE_FUNCTION"] = "Delete function";
31
- CHOICES["SETUP_DIRS_FILES"] = "Setup directories and files";
32
- CHOICES["SETUP_DIRS_FILES_WITH_EXAMPLE_DATA"] = "Setup directories and files with example data";
33
- CHOICES["SYNC_DB"] = "Push local config to Appwrite";
34
- CHOICES["SYNCHRONIZE_CONFIGURATIONS"] = "Synchronize configurations - Pull from Appwrite and write to local config";
35
- CHOICES["TRANSFER_DATA"] = "Transfer data";
36
- CHOICES["BACKUP_DATABASE"] = "Backup database";
37
- CHOICES["WIPE_DATABASE"] = "Wipe database";
38
- CHOICES["WIPE_COLLECTIONS"] = "Wipe collections";
39
- CHOICES["GENERATE_SCHEMAS"] = "Generate schemas";
28
+ CHOICES["CREATE_COLLECTION_CONFIG"] = "\uD83D\uDCC4 Create collection config file";
29
+ CHOICES["CREATE_FUNCTION"] = "\u26A1 Create a new function, from scratch or using a template";
30
+ CHOICES["DEPLOY_FUNCTION"] = "\uD83D\uDE80 Deploy function(s)";
31
+ CHOICES["DELETE_FUNCTION"] = "\uD83D\uDDD1\uFE0F Delete function";
32
+ CHOICES["SETUP_DIRS_FILES"] = "\uD83D\uDCC1 Setup directories and files";
33
+ CHOICES["SETUP_DIRS_FILES_WITH_EXAMPLE_DATA"] = "\uD83D\uDCC1\u2728 Setup directories and files with example data";
34
+ CHOICES["SYNC_DB"] = "\u2B06\uFE0F Push local config to Appwrite";
35
+ CHOICES["SYNCHRONIZE_CONFIGURATIONS"] = "\uD83D\uDD04 Synchronize configurations - Pull from Appwrite and write to local config";
36
+ CHOICES["TRANSFER_DATA"] = "\uD83D\uDCE6 Transfer data";
37
+ CHOICES["COMPREHENSIVE_TRANSFER"] = "\uD83D\uDE80 Comprehensive transfer (users \u2192 databases \u2192 buckets \u2192 functions)";
38
+ CHOICES["BACKUP_DATABASE"] = "\uD83D\uDCBE Backup database";
39
+ CHOICES["WIPE_DATABASE"] = "\uD83E\uDDF9 Wipe database";
40
+ CHOICES["WIPE_COLLECTIONS"] = "\uD83E\uDDF9 Wipe collections";
41
+ CHOICES["GENERATE_SCHEMAS"] = "\uD83C\uDFD7\uFE0F Generate schemas";
40
42
  CHOICES["GENERATE_CONSTANTS"] = "\uD83D\uDCCB Generate cross-language constants (TypeScript, Python, PHP, Dart, etc.)";
41
- CHOICES["IMPORT_DATA"] = "Import data";
42
- CHOICES["RELOAD_CONFIG"] = "Reload configuration files";
43
- CHOICES["UPDATE_FUNCTION_SPEC"] = "Update function specifications";
44
- CHOICES["EXIT"] = "Exit";
43
+ CHOICES["IMPORT_DATA"] = "\uD83D\uDCE5 Import data";
44
+ CHOICES["RELOAD_CONFIG"] = "\uD83D\uDD04 Reload configuration files";
45
+ CHOICES["UPDATE_FUNCTION_SPEC"] = "\u2699\uFE0F Update function specifications";
46
+ CHOICES["EXIT"] = "\uD83D\uDC4B Exit";
45
47
  })(CHOICES || (CHOICES = {}));
46
48
  export class InteractiveCLI {
47
49
  currentDir;
@@ -109,6 +111,9 @@ export class InteractiveCLI {
109
111
  await this.initControllerIfNeeded();
110
112
  await this.transferData();
111
113
  break;
114
+ case CHOICES.COMPREHENSIVE_TRANSFER:
115
+ await this.comprehensiveTransfer();
116
+ break;
112
117
  case CHOICES.BACKUP_DATABASE:
113
118
  await this.initControllerIfNeeded();
114
119
  await this.backupDatabase();
@@ -1396,13 +1401,13 @@ export class InteractiveCLI {
1396
1401
  }));
1397
1402
  }
1398
1403
  async reloadConfig() {
1399
- console.log(chalk.yellow("Reloading configuration files..."));
1404
+ MessageFormatter.progress("Reloading configuration files...", { prefix: "Config" });
1400
1405
  try {
1401
1406
  await this.controller.reloadConfig();
1402
- console.log(chalk.green("Configuration files reloaded successfully."));
1407
+ MessageFormatter.success("Configuration files reloaded successfully", { prefix: "Config" });
1403
1408
  }
1404
1409
  catch (error) {
1405
- console.error(chalk.red("Error reloading configuration files:"), error);
1410
+ MessageFormatter.error("Failed to reload configuration files", error instanceof Error ? error : new Error(String(error)), { prefix: "Config" });
1406
1411
  }
1407
1412
  }
1408
1413
  async updateFunctionSpec() {
@@ -1452,7 +1457,6 @@ export class InteractiveCLI {
1452
1457
  try {
1453
1458
  // Check for YAML config first
1454
1459
  const yamlConfigPath = findYamlConfig(this.currentDir);
1455
- console.log(`DEBUG: YAML config search from ${this.currentDir}, found: ${yamlConfigPath}`);
1456
1460
  if (yamlConfigPath) {
1457
1461
  this.isUsingTypeScriptConfig = false;
1458
1462
  MessageFormatter.info("Using YAML configuration", { prefix: "Config" });
@@ -1509,4 +1513,164 @@ export class InteractiveCLI {
1509
1513
  MessageFormatter.error("Migration failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Migration" });
1510
1514
  }
1511
1515
  }
1516
+ async comprehensiveTransfer() {
1517
+ MessageFormatter.info("Starting comprehensive transfer configuration...", { prefix: "Transfer" });
1518
+ try {
1519
+ // Get source configuration
1520
+ const sourceConfig = await inquirer.prompt([
1521
+ {
1522
+ type: "input",
1523
+ name: "sourceEndpoint",
1524
+ message: "Enter the source Appwrite endpoint:",
1525
+ validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
1526
+ },
1527
+ {
1528
+ type: "input",
1529
+ name: "sourceProject",
1530
+ message: "Enter the source project ID:",
1531
+ validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
1532
+ },
1533
+ {
1534
+ type: "password",
1535
+ name: "sourceKey",
1536
+ message: "Enter the source API key:",
1537
+ validate: (input) => input.trim() !== "" || "API key cannot be empty",
1538
+ },
1539
+ ]);
1540
+ // Get target configuration
1541
+ const targetConfig = await inquirer.prompt([
1542
+ {
1543
+ type: "input",
1544
+ name: "targetEndpoint",
1545
+ message: "Enter the target Appwrite endpoint:",
1546
+ validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
1547
+ },
1548
+ {
1549
+ type: "input",
1550
+ name: "targetProject",
1551
+ message: "Enter the target project ID:",
1552
+ validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
1553
+ },
1554
+ {
1555
+ type: "password",
1556
+ name: "targetKey",
1557
+ message: "Enter the target API key:",
1558
+ validate: (input) => input.trim() !== "" || "API key cannot be empty",
1559
+ },
1560
+ ]);
1561
+ // Get transfer options
1562
+ const transferOptions = await inquirer.prompt([
1563
+ {
1564
+ type: "checkbox",
1565
+ name: "transferTypes",
1566
+ message: "Select what to transfer:",
1567
+ choices: [
1568
+ { name: "👥 Users", value: "users", checked: true },
1569
+ { name: "🗄️ Databases", value: "databases", checked: true },
1570
+ { name: "📦 Storage Buckets", value: "buckets", checked: true },
1571
+ { name: "⚡ Functions", value: "functions", checked: true },
1572
+ ],
1573
+ validate: (input) => input.length > 0 || "Select at least one transfer type",
1574
+ },
1575
+ {
1576
+ type: "list",
1577
+ name: "concurrencyLimit",
1578
+ message: "Select concurrency limit:",
1579
+ choices: [
1580
+ { name: "5 (Conservative) - Users: 2, Files: 1", value: 5 },
1581
+ { name: "10 (Balanced) - Users: 5, Files: 2", value: 10 },
1582
+ { name: "15 - Users: 7, Files: 3", value: 15 },
1583
+ { name: "20 - Users: 10, Files: 5", value: 20 },
1584
+ { name: "25 - Users: 12, Files: 6", value: 25 },
1585
+ { name: "30 - Users: 15, Files: 7", value: 30 },
1586
+ { name: "35 - Users: 17, Files: 8", value: 35 },
1587
+ { name: "40 - Users: 20, Files: 10", value: 40 },
1588
+ { name: "45 - Users: 22, Files: 11", value: 45 },
1589
+ { name: "50 - Users: 25, Files: 12", value: 50 },
1590
+ { name: "55 - Users: 27, Files: 13", value: 55 },
1591
+ { name: "60 - Users: 30, Files: 15", value: 60 },
1592
+ { name: "65 - Users: 32, Files: 16", value: 65 },
1593
+ { name: "70 - Users: 35, Files: 17", value: 70 },
1594
+ { name: "75 - Users: 37, Files: 18", value: 75 },
1595
+ { name: "80 - Users: 40, Files: 20", value: 80 },
1596
+ { name: "85 - Users: 42, Files: 21", value: 85 },
1597
+ { name: "90 - Users: 45, Files: 22", value: 90 },
1598
+ { name: "95 - Users: 47, Files: 23", value: 95 },
1599
+ { name: "100 (Aggressive) - Users: 50, Files: 25", value: 100 },
1600
+ ],
1601
+ default: 10,
1602
+ },
1603
+ {
1604
+ type: "confirm",
1605
+ name: "dryRun",
1606
+ message: "Run in dry-run mode (no actual changes)?",
1607
+ default: false,
1608
+ },
1609
+ ]);
1610
+ // Confirmation
1611
+ const { confirmed } = await inquirer.prompt([
1612
+ {
1613
+ type: "confirm",
1614
+ name: "confirmed",
1615
+ message: `Are you sure you want to ${transferOptions.dryRun ? "dry-run" : "perform"} comprehensive transfer from ${sourceConfig.sourceEndpoint} to ${targetConfig.targetEndpoint}?`,
1616
+ default: false,
1617
+ },
1618
+ ]);
1619
+ if (!confirmed) {
1620
+ MessageFormatter.info("Transfer cancelled by user", { prefix: "Transfer" });
1621
+ return;
1622
+ }
1623
+ // Important password warning
1624
+ if (transferOptions.transferTypes.includes("users") && !transferOptions.dryRun) {
1625
+ MessageFormatter.warning("IMPORTANT: User passwords cannot be transferred due to Appwrite security limitations.", { prefix: "Transfer" });
1626
+ MessageFormatter.warning("Users will need to reset their passwords after transfer.", { prefix: "Transfer" });
1627
+ const { continueWithUsers } = await inquirer.prompt([
1628
+ {
1629
+ type: "confirm",
1630
+ name: "continueWithUsers",
1631
+ message: "Continue with user transfer knowing passwords will be reset?",
1632
+ default: false,
1633
+ },
1634
+ ]);
1635
+ if (!continueWithUsers) {
1636
+ // Remove users from transfer types
1637
+ transferOptions.transferTypes = transferOptions.transferTypes.filter((type) => type !== "users");
1638
+ if (transferOptions.transferTypes.length === 0) {
1639
+ MessageFormatter.info("No transfer types selected, cancelling", { prefix: "Transfer" });
1640
+ return;
1641
+ }
1642
+ }
1643
+ }
1644
+ // Execute comprehensive transfer
1645
+ const comprehensiveTransferOptions = {
1646
+ sourceEndpoint: sourceConfig.sourceEndpoint,
1647
+ sourceProject: sourceConfig.sourceProject,
1648
+ sourceKey: sourceConfig.sourceKey,
1649
+ targetEndpoint: targetConfig.targetEndpoint,
1650
+ targetProject: targetConfig.targetProject,
1651
+ targetKey: targetConfig.targetKey,
1652
+ transferUsers: transferOptions.transferTypes.includes("users"),
1653
+ transferDatabases: transferOptions.transferTypes.includes("databases"),
1654
+ transferBuckets: transferOptions.transferTypes.includes("buckets"),
1655
+ transferFunctions: transferOptions.transferTypes.includes("functions"),
1656
+ concurrencyLimit: transferOptions.concurrencyLimit,
1657
+ dryRun: transferOptions.dryRun,
1658
+ };
1659
+ const transfer = new ComprehensiveTransfer(comprehensiveTransferOptions);
1660
+ const results = await transfer.execute();
1661
+ // Display results
1662
+ if (transferOptions.dryRun) {
1663
+ MessageFormatter.success("Dry run completed successfully!", { prefix: "Transfer" });
1664
+ }
1665
+ else {
1666
+ MessageFormatter.success("Comprehensive transfer completed!", { prefix: "Transfer" });
1667
+ if (transferOptions.transferTypes.includes("users") && results.users.transferred > 0) {
1668
+ MessageFormatter.info("Remember to notify users about password reset requirements", { prefix: "Transfer" });
1669
+ }
1670
+ }
1671
+ }
1672
+ catch (error) {
1673
+ MessageFormatter.error("Comprehensive transfer failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
1674
+ }
1675
+ }
1512
1676
  }
@@ -0,0 +1,66 @@
1
+ export interface ComprehensiveTransferOptions {
2
+ sourceEndpoint: string;
3
+ sourceProject: string;
4
+ sourceKey: string;
5
+ targetEndpoint: string;
6
+ targetProject: string;
7
+ targetKey: string;
8
+ transferUsers?: boolean;
9
+ transferDatabases?: boolean;
10
+ transferBuckets?: boolean;
11
+ transferFunctions?: boolean;
12
+ concurrencyLimit?: number;
13
+ dryRun?: boolean;
14
+ }
15
+ export interface TransferResults {
16
+ users: {
17
+ transferred: number;
18
+ skipped: number;
19
+ failed: number;
20
+ };
21
+ databases: {
22
+ transferred: number;
23
+ skipped: number;
24
+ failed: number;
25
+ };
26
+ buckets: {
27
+ transferred: number;
28
+ skipped: number;
29
+ failed: number;
30
+ };
31
+ functions: {
32
+ transferred: number;
33
+ skipped: number;
34
+ failed: number;
35
+ };
36
+ totalTime: number;
37
+ }
38
+ export declare class ComprehensiveTransfer {
39
+ private options;
40
+ private sourceClient;
41
+ private targetClient;
42
+ private sourceUsers;
43
+ private targetUsers;
44
+ private sourceDatabases;
45
+ private targetDatabases;
46
+ private sourceStorage;
47
+ private targetStorage;
48
+ private sourceFunctions;
49
+ private targetFunctions;
50
+ private limit;
51
+ private userLimit;
52
+ private fileLimit;
53
+ private results;
54
+ private startTime;
55
+ private tempDir;
56
+ constructor(options: ComprehensiveTransferOptions);
57
+ execute(): Promise<TransferResults>;
58
+ private transferAllUsers;
59
+ private transferAllDatabases;
60
+ private transferAllBuckets;
61
+ private transferBucketFiles;
62
+ private validateAndDownloadFile;
63
+ private transferAllFunctions;
64
+ private downloadFunction;
65
+ private printSummary;
66
+ }