appwrite-utils-cli 1.0.9 → 1.1.1

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.
@@ -23,6 +23,7 @@ import {
23
23
  import { delay, tryAwaitWithRetry } from "../utils/helperFunctions.js";
24
24
  import { MessageFormatter } from "../shared/messageFormatter.js";
25
25
  import { ProgressManager } from "../shared/progressManager.js";
26
+ import chalk from "chalk";
26
27
 
27
28
  export const documentExists = async (
28
29
  db: Databases,
@@ -615,6 +616,123 @@ export const transferDocumentsBetweenDbsLocalToLocal = async (
615
616
  );
616
617
  };
617
618
 
619
+ /**
620
+ * Enhanced document transfer with fault tolerance and exponential backoff
621
+ */
622
+ const transferDocumentWithRetry = async (
623
+ db: Databases,
624
+ dbId: string,
625
+ collectionId: string,
626
+ documentId: string,
627
+ documentData: any,
628
+ permissions: string[],
629
+ maxRetries: number = 3,
630
+ retryCount: number = 0
631
+ ): Promise<boolean> => {
632
+ try {
633
+ await db.createDocument(
634
+ dbId,
635
+ collectionId,
636
+ documentId,
637
+ documentData,
638
+ permissions
639
+ );
640
+ return true;
641
+ } catch (error: any) {
642
+ // Check if document already exists
643
+ if (error.code === 409 || error.message?.includes('already exists')) {
644
+ console.log(chalk.yellow(`Document ${documentId} already exists, skipping...`));
645
+ return true;
646
+ }
647
+
648
+ if (retryCount < maxRetries) {
649
+ // Calculate exponential backoff: 1s, 2s, 4s
650
+ const exponentialDelay = Math.min(1000 * Math.pow(2, retryCount), 8000);
651
+ console.log(chalk.yellow(`Retrying document ${documentId} (attempt ${retryCount + 1}/${maxRetries}, backoff: ${exponentialDelay}ms)`));
652
+
653
+ await delay(exponentialDelay);
654
+
655
+ return await transferDocumentWithRetry(
656
+ db,
657
+ dbId,
658
+ collectionId,
659
+ documentId,
660
+ documentData,
661
+ permissions,
662
+ maxRetries,
663
+ retryCount + 1
664
+ );
665
+ }
666
+
667
+ console.log(chalk.red(`Failed to transfer document ${documentId} after ${maxRetries} retries: ${error.message}`));
668
+ return false;
669
+ }
670
+ };
671
+
672
+ /**
673
+ * Enhanced batch document transfer with fault tolerance
674
+ */
675
+ const transferDocumentBatchWithRetry = async (
676
+ db: Databases,
677
+ dbId: string,
678
+ collectionId: string,
679
+ documents: any[],
680
+ batchSize: number = 10
681
+ ): Promise<{ successful: number; failed: number }> => {
682
+ let successful = 0;
683
+ let failed = 0;
684
+
685
+ // Process documents in smaller batches to avoid overwhelming the server
686
+ const documentBatches = chunk(documents, batchSize);
687
+
688
+ for (const batch of documentBatches) {
689
+ console.log(chalk.blue(`Processing batch of ${batch.length} documents...`));
690
+
691
+ const batchPromises = batch.map(async (doc) => {
692
+ const toCreateObject: Partial<typeof doc> = { ...doc };
693
+ delete toCreateObject.$databaseId;
694
+ delete toCreateObject.$collectionId;
695
+ delete toCreateObject.$createdAt;
696
+ delete toCreateObject.$updatedAt;
697
+ delete toCreateObject.$id;
698
+ delete toCreateObject.$permissions;
699
+
700
+ const result = await transferDocumentWithRetry(
701
+ db,
702
+ dbId,
703
+ collectionId,
704
+ doc.$id,
705
+ toCreateObject,
706
+ doc.$permissions || []
707
+ );
708
+
709
+ return { docId: doc.$id, success: result };
710
+ });
711
+
712
+ const results = await Promise.allSettled(batchPromises);
713
+
714
+ results.forEach((result, index) => {
715
+ if (result.status === 'fulfilled') {
716
+ if (result.value.success) {
717
+ successful++;
718
+ } else {
719
+ failed++;
720
+ }
721
+ } else {
722
+ console.log(chalk.red(`Batch promise rejected for document ${batch[index].$id}: ${result.reason}`));
723
+ failed++;
724
+ }
725
+ });
726
+
727
+ // Add delay between batches to avoid rate limiting
728
+ if (documentBatches.indexOf(batch) < documentBatches.length - 1) {
729
+ await delay(500);
730
+ }
731
+ }
732
+
733
+ return { successful, failed };
734
+ };
735
+
618
736
  export const transferDocumentsBetweenDbsLocalToRemote = async (
619
737
  localDb: Databases,
620
738
  endpoint: string,
@@ -625,100 +743,70 @@ export const transferDocumentsBetweenDbsLocalToRemote = async (
625
743
  fromCollId: string,
626
744
  toCollId: string
627
745
  ) => {
746
+ console.log(chalk.blue(`Starting enhanced document transfer from ${fromCollId} to ${toCollId}...`));
747
+
628
748
  const client = new Client()
629
749
  .setEndpoint(endpoint)
630
750
  .setProject(projectId)
631
751
  .setKey(apiKey);
632
- let totalDocumentsTransferred = 0;
752
+
633
753
  const remoteDb = new Databases(client);
634
- let fromCollDocs = await tryAwaitWithRetry(async () =>
635
- localDb.listDocuments(fromDbId, fromCollId, [Query.limit(50)])
636
- );
637
-
638
- if (fromCollDocs.documents.length === 0) {
754
+ let totalDocumentsProcessed = 0;
755
+ let totalSuccessful = 0;
756
+ let totalFailed = 0;
757
+
758
+ // Fetch documents in batches
759
+ let hasMoreDocuments = true;
760
+ let lastDocumentId: string | undefined;
761
+
762
+ while (hasMoreDocuments) {
763
+ const queries = [Query.limit(50)];
764
+ if (lastDocumentId) {
765
+ queries.push(Query.cursorAfter(lastDocumentId));
766
+ }
767
+
768
+ const fromCollDocs = await tryAwaitWithRetry(async () =>
769
+ localDb.listDocuments(fromDbId, fromCollId, queries)
770
+ );
771
+
772
+ if (fromCollDocs.documents.length === 0) {
773
+ hasMoreDocuments = false;
774
+ break;
775
+ }
776
+
777
+ console.log(chalk.blue(`Processing ${fromCollDocs.documents.length} documents...`));
778
+
779
+ const { successful, failed } = await transferDocumentBatchWithRetry(
780
+ remoteDb,
781
+ toDbId,
782
+ toCollId,
783
+ fromCollDocs.documents
784
+ );
785
+
786
+ totalDocumentsProcessed += fromCollDocs.documents.length;
787
+ totalSuccessful += successful;
788
+ totalFailed += failed;
789
+
790
+ // Check if we have more documents to process
791
+ if (fromCollDocs.documents.length < 50) {
792
+ hasMoreDocuments = false;
793
+ } else {
794
+ lastDocumentId = fromCollDocs.documents[fromCollDocs.documents.length - 1].$id;
795
+ }
796
+
797
+ console.log(chalk.gray(`Batch complete: ${successful} successful, ${failed} failed`));
798
+ }
799
+
800
+ if (totalDocumentsProcessed === 0) {
639
801
  MessageFormatter.info(`No documents found in collection ${fromCollId}`, { prefix: "Transfer" });
640
802
  return;
641
- } else if (fromCollDocs.documents.length < 50) {
642
- const batchedPromises = fromCollDocs.documents.map((doc) => {
643
- const toCreateObject: Partial<typeof doc> = {
644
- ...doc,
645
- };
646
- delete toCreateObject.$databaseId;
647
- delete toCreateObject.$collectionId;
648
- delete toCreateObject.$createdAt;
649
- delete toCreateObject.$updatedAt;
650
- delete toCreateObject.$id;
651
- delete toCreateObject.$permissions;
652
- return tryAwaitWithRetry(async () =>
653
- remoteDb.createDocument(
654
- toDbId,
655
- toCollId,
656
- doc.$id,
657
- toCreateObject,
658
- doc.$permissions
659
- )
660
- );
661
- });
662
- await Promise.all(batchedPromises);
663
- totalDocumentsTransferred += fromCollDocs.documents.length;
803
+ }
804
+
805
+ const message = `Total documents processed: ${totalDocumentsProcessed}, successful: ${totalSuccessful}, failed: ${totalFailed}`;
806
+
807
+ if (totalFailed > 0) {
808
+ MessageFormatter.warning(message, { prefix: "Transfer" });
664
809
  } else {
665
- const batchedPromises = fromCollDocs.documents.map((doc) => {
666
- const toCreateObject: Partial<typeof doc> = {
667
- ...doc,
668
- };
669
- delete toCreateObject.$databaseId;
670
- delete toCreateObject.$collectionId;
671
- delete toCreateObject.$createdAt;
672
- delete toCreateObject.$updatedAt;
673
- delete toCreateObject.$id;
674
- delete toCreateObject.$permissions;
675
- return tryAwaitWithRetry(async () =>
676
- remoteDb.createDocument(
677
- toDbId,
678
- toCollId,
679
- doc.$id,
680
- toCreateObject,
681
- doc.$permissions
682
- )
683
- );
684
- });
685
- await Promise.all(batchedPromises);
686
- totalDocumentsTransferred += fromCollDocs.documents.length;
687
- while (fromCollDocs.documents.length === 50) {
688
- fromCollDocs = await tryAwaitWithRetry(async () =>
689
- localDb.listDocuments(fromDbId, fromCollId, [
690
- Query.limit(50),
691
- Query.cursorAfter(
692
- fromCollDocs.documents[fromCollDocs.documents.length - 1].$id
693
- ),
694
- ])
695
- );
696
- const batchedPromises = fromCollDocs.documents.map((doc) => {
697
- const toCreateObject: Partial<typeof doc> = {
698
- ...doc,
699
- };
700
- delete toCreateObject.$databaseId;
701
- delete toCreateObject.$collectionId;
702
- delete toCreateObject.$createdAt;
703
- delete toCreateObject.$updatedAt;
704
- delete toCreateObject.$id;
705
- delete toCreateObject.$permissions;
706
- return tryAwaitWithRetry(async () =>
707
- remoteDb.createDocument(
708
- toDbId,
709
- toCollId,
710
- doc.$id,
711
- toCreateObject,
712
- doc.$permissions
713
- )
714
- );
715
- });
716
- await Promise.all(batchedPromises);
717
- totalDocumentsTransferred += fromCollDocs.documents.length;
718
- }
810
+ MessageFormatter.success(message, { prefix: "Transfer" });
719
811
  }
720
- MessageFormatter.success(
721
- `Total documents transferred from database ${fromDbId} to database ${toDbId} -- collection ${fromCollId} to collection ${toCollId}: ${totalDocumentsTransferred}`,
722
- { prefix: "Transfer" }
723
- );
724
812
  };
@@ -2052,49 +2052,144 @@ export class InteractiveCLI {
2052
2052
  MessageFormatter.info("Starting comprehensive transfer configuration...", { prefix: "Transfer" });
2053
2053
 
2054
2054
  try {
2055
- // Get source configuration
2056
- const sourceConfig = await inquirer.prompt([
2057
- {
2058
- type: "input",
2059
- name: "sourceEndpoint",
2060
- message: "Enter the source Appwrite endpoint:",
2061
- validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
2062
- },
2063
- {
2064
- type: "input",
2065
- name: "sourceProject",
2066
- message: "Enter the source project ID:",
2067
- validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
2068
- },
2069
- {
2070
- type: "password",
2071
- name: "sourceKey",
2072
- message: "Enter the source API key:",
2073
- validate: (input) => input.trim() !== "" || "API key cannot be empty",
2074
- },
2075
- ]);
2055
+ // Initialize controller to optionally load config if available (supports both YAML and TypeScript configs)\n await this.initControllerIfNeeded();\n \n // Check if user has an appwrite config for easier setup
2056
+ const hasAppwriteConfig = this.controller?.config?.appwriteEndpoint &&
2057
+ this.controller?.config?.appwriteProject &&
2058
+ this.controller?.config?.appwriteKey;
2076
2059
 
2077
- // Get target configuration
2078
- const targetConfig = await inquirer.prompt([
2079
- {
2080
- type: "input",
2081
- name: "targetEndpoint",
2082
- message: "Enter the target Appwrite endpoint:",
2083
- validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
2084
- },
2085
- {
2086
- type: "input",
2087
- name: "targetProject",
2088
- message: "Enter the target project ID:",
2089
- validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
2090
- },
2091
- {
2092
- type: "password",
2093
- name: "targetKey",
2094
- message: "Enter the target API key:",
2095
- validate: (input) => input.trim() !== "" || "API key cannot be empty",
2096
- },
2097
- ]);
2060
+ let sourceConfig: any;
2061
+ let targetConfig: any;
2062
+
2063
+ if (hasAppwriteConfig) {
2064
+ // Offer to use existing config for source
2065
+ const { useConfigForSource } = await inquirer.prompt([
2066
+ {
2067
+ type: "confirm",
2068
+ name: "useConfigForSource",
2069
+ message: "Use your current appwriteConfig as the source?",
2070
+ default: true,
2071
+ },
2072
+ ]);
2073
+
2074
+ if (useConfigForSource) {
2075
+ sourceConfig = {
2076
+ sourceEndpoint: this.controller!.config!.appwriteEndpoint,
2077
+ sourceProject: this.controller!.config!.appwriteProject,
2078
+ sourceKey: this.controller!.config!.appwriteKey,
2079
+ };
2080
+ MessageFormatter.info(`Using config source: ${sourceConfig.sourceEndpoint}`, { prefix: "Transfer" });
2081
+ } else {
2082
+ // Get source configuration manually
2083
+ sourceConfig = await inquirer.prompt([
2084
+ {
2085
+ type: "input",
2086
+ name: "sourceEndpoint",
2087
+ message: "Enter the source Appwrite endpoint:",
2088
+ validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
2089
+ },
2090
+ {
2091
+ type: "input",
2092
+ name: "sourceProject",
2093
+ message: "Enter the source project ID:",
2094
+ validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
2095
+ },
2096
+ {
2097
+ type: "password",
2098
+ name: "sourceKey",
2099
+ message: "Enter the source API key:",
2100
+ validate: (input) => input.trim() !== "" || "API key cannot be empty",
2101
+ },
2102
+ ]);
2103
+ }
2104
+
2105
+ // Offer to use existing config for target
2106
+ const { useConfigForTarget } = await inquirer.prompt([
2107
+ {
2108
+ type: "confirm",
2109
+ name: "useConfigForTarget",
2110
+ message: "Use your current appwriteConfig as the target?",
2111
+ default: false,
2112
+ },
2113
+ ]);
2114
+
2115
+ if (useConfigForTarget) {
2116
+ targetConfig = {
2117
+ targetEndpoint: this.controller!.config!.appwriteEndpoint,
2118
+ targetProject: this.controller!.config!.appwriteProject,
2119
+ targetKey: this.controller!.config!.appwriteKey,
2120
+ };
2121
+ MessageFormatter.info(`Using config target: ${targetConfig.targetEndpoint}`, { prefix: "Transfer" });
2122
+ } else {
2123
+ // Get target configuration manually
2124
+ targetConfig = await inquirer.prompt([
2125
+ {
2126
+ type: "input",
2127
+ name: "targetEndpoint",
2128
+ message: "Enter the target Appwrite endpoint:",
2129
+ validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
2130
+ },
2131
+ {
2132
+ type: "input",
2133
+ name: "targetProject",
2134
+ message: "Enter the target project ID:",
2135
+ validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
2136
+ },
2137
+ {
2138
+ type: "password",
2139
+ name: "targetKey",
2140
+ message: "Enter the target API key:",
2141
+ validate: (input) => input.trim() !== "" || "API key cannot be empty",
2142
+ },
2143
+ ]);
2144
+ }
2145
+ } else {
2146
+ // No appwrite config found, get both configurations manually
2147
+ MessageFormatter.info("No appwriteConfig found, please enter source and target configurations manually", { prefix: "Transfer" });
2148
+
2149
+ // Get source configuration
2150
+ sourceConfig = await inquirer.prompt([
2151
+ {
2152
+ type: "input",
2153
+ name: "sourceEndpoint",
2154
+ message: "Enter the source Appwrite endpoint:",
2155
+ validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
2156
+ },
2157
+ {
2158
+ type: "input",
2159
+ name: "sourceProject",
2160
+ message: "Enter the source project ID:",
2161
+ validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
2162
+ },
2163
+ {
2164
+ type: "password",
2165
+ name: "sourceKey",
2166
+ message: "Enter the source API key:",
2167
+ validate: (input) => input.trim() !== "" || "API key cannot be empty",
2168
+ },
2169
+ ]);
2170
+
2171
+ // Get target configuration
2172
+ targetConfig = await inquirer.prompt([
2173
+ {
2174
+ type: "input",
2175
+ name: "targetEndpoint",
2176
+ message: "Enter the target Appwrite endpoint:",
2177
+ validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
2178
+ },
2179
+ {
2180
+ type: "input",
2181
+ name: "targetProject",
2182
+ message: "Enter the target project ID:",
2183
+ validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
2184
+ },
2185
+ {
2186
+ type: "password",
2187
+ name: "targetKey",
2188
+ message: "Enter the target API key:",
2189
+ validate: (input) => input.trim() !== "" || "API key cannot be empty",
2190
+ },
2191
+ ]);
2192
+ }
2098
2193
 
2099
2194
  // Get transfer options
2100
2195
  const transferOptions = await inquirer.prompt([