appwrite-utils-cli 0.0.37 → 0.0.39

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.
Files changed (35) hide show
  1. package/README.md +1 -0
  2. package/dist/migrations/attributes.js +21 -20
  3. package/dist/migrations/collections.js +10 -13
  4. package/dist/migrations/converters.js +15 -1
  5. package/dist/migrations/dataLoader.d.ts +12 -21
  6. package/dist/migrations/dataLoader.js +168 -58
  7. package/dist/migrations/databases.js +2 -1
  8. package/dist/migrations/importController.js +6 -24
  9. package/dist/migrations/importDataActions.d.ts +9 -1
  10. package/dist/migrations/importDataActions.js +14 -2
  11. package/dist/migrations/indexes.js +2 -1
  12. package/dist/migrations/migrationHelper.js +2 -1
  13. package/dist/migrations/queue.js +3 -2
  14. package/dist/migrations/setupDatabase.js +10 -12
  15. package/dist/migrations/storage.js +14 -13
  16. package/dist/migrations/users.js +14 -36
  17. package/dist/utils/helperFunctions.d.ts +10 -1
  18. package/dist/utils/helperFunctions.js +27 -0
  19. package/dist/utilsController.js +2 -1
  20. package/package.json +2 -2
  21. package/src/migrations/attributes.ts +204 -143
  22. package/src/migrations/collections.ts +49 -33
  23. package/src/migrations/converters.ts +17 -1
  24. package/src/migrations/dataLoader.ts +232 -63
  25. package/src/migrations/databases.ts +4 -1
  26. package/src/migrations/importController.ts +13 -27
  27. package/src/migrations/importDataActions.ts +17 -3
  28. package/src/migrations/indexes.ts +4 -1
  29. package/src/migrations/migrationHelper.ts +9 -5
  30. package/src/migrations/queue.ts +5 -6
  31. package/src/migrations/setupDatabase.ts +35 -18
  32. package/src/migrations/storage.ts +50 -36
  33. package/src/migrations/users.ts +28 -38
  34. package/src/utils/helperFunctions.ts +33 -1
  35. package/src/utilsController.ts +3 -1
@@ -2,7 +2,7 @@ import { Databases, ID, Query, Storage } from "node-appwrite";
2
2
  import { createOrUpdateAttribute } from "./attributes.js";
3
3
  import { createOrUpdateCollections, generateSchemas, wipeDatabase, } from "./collections.js";
4
4
  import { getMigrationCollectionSchemas } from "./backup.js";
5
- import { areCollectionNamesSame, toCamelCase } from "../utils/index.js";
5
+ import { areCollectionNamesSame, toCamelCase, tryAwaitWithRetry, } from "../utils/index.js";
6
6
  import { backupDatabase, initOrGetBackupStorage, initOrGetDocumentStorage, wipeDocumentStorage, } from "./storage.js";
7
7
  import {} from "appwrite-utils";
8
8
  import { nameToIdMapping } from "./queue.js";
@@ -17,17 +17,15 @@ export const setupMigrationDatabase = async (config) => {
17
17
  const dbCollections = [];
18
18
  const migrationCollectionsSetup = getMigrationCollectionSchemas();
19
19
  try {
20
- db = await database.get("migrations");
20
+ db = await tryAwaitWithRetry(async () => await database.get("migrations"));
21
21
  console.log("Migrations database found");
22
22
  }
23
23
  catch (e) {
24
- db = await database.create("migrations", "Migrations", true);
24
+ db = await tryAwaitWithRetry(async () => await database.create("migrations", "Migrations", true));
25
25
  console.log("Migrations database created");
26
26
  }
27
27
  if (db) {
28
- const collectionsPulled = await database.listCollections(db.$id, [
29
- Query.limit(25),
30
- ]);
28
+ const collectionsPulled = await tryAwaitWithRetry(async () => await database.listCollections(db.$id, [Query.limit(25)]));
31
29
  dbCollections.push(...collectionsPulled.collections);
32
30
  }
33
31
  console.log(`Collections in migrations database: ${dbCollections.length}`);
@@ -36,14 +34,14 @@ export const setupMigrationDatabase = async (config) => {
36
34
  const collectionId = toCamelCase(collectionName); // Convert name to toCamelCase for the ID
37
35
  let collectionFound = null;
38
36
  try {
39
- collectionFound = await database.getCollection(db.$id, collectionId);
37
+ collectionFound = await tryAwaitWithRetry(async () => await database.getCollection(db.$id, collectionId));
40
38
  }
41
39
  catch (e) {
42
40
  console.log(`Collection not found: ${collectionId}`);
43
41
  }
44
42
  if (!collectionFound) {
45
43
  // Create the collection with the provided configuration
46
- collectionFound = await database.createCollection(db.$id, collectionId, collectionName, undefined, collection.documentSecurity, collection.enabled);
44
+ collectionFound = await tryAwaitWithRetry(async () => await database.createCollection(db.$id, collectionId, collectionName, undefined, collection.documentSecurity, collection.enabled));
47
45
  }
48
46
  for (const attribute of attributes) {
49
47
  await createOrUpdateAttribute(database, db.$id, collectionFound, attribute);
@@ -61,10 +59,10 @@ export const ensureDatabasesExist = async (config) => {
61
59
  name: "Migrations",
62
60
  });
63
61
  const dbNames = databasesToEnsure.map((db) => db.name);
64
- const existingDatabases = await database.list([Query.equal("name", dbNames)]);
62
+ const existingDatabases = await tryAwaitWithRetry(async () => await database.list([Query.equal("name", dbNames)]));
65
63
  for (const db of databasesToEnsure) {
66
64
  if (!existingDatabases.databases.some((d) => d.name === db.name)) {
67
- await database.create(db.$id || ID.unique(), db.name, true);
65
+ await tryAwaitWithRetry(async () => await database.create(db.$id || ID.unique(), db.name, true));
68
66
  console.log(`${db.name} database created`);
69
67
  }
70
68
  }
@@ -73,10 +71,10 @@ export const wipeOtherDatabases = async (database, config) => {
73
71
  const databasesToKeep = config.databases.map((db) => db.name.toLowerCase().trim().replace(" ", ""));
74
72
  databasesToKeep.push("migrations");
75
73
  console.log(`Databases to keep: ${databasesToKeep.join(", ")}`);
76
- const allDatabases = await database.list([Query.limit(500)]);
74
+ const allDatabases = await tryAwaitWithRetry(async () => await database.list([Query.limit(500)]));
77
75
  for (const db of allDatabases.databases) {
78
76
  if (!databasesToKeep.includes(db.name.toLowerCase().trim().replace(" ", ""))) {
79
- await database.delete(db.$id);
77
+ await tryAwaitWithRetry(async () => await database.delete(db.$id));
80
78
  console.log(`Deleted database: ${db.name}`);
81
79
  }
82
80
  }
@@ -1,12 +1,13 @@
1
1
  import { Storage, Databases, Query, InputFile, ID, Permission, } from "node-appwrite";
2
2
  import {} from "./backup.js";
3
3
  import { splitIntoBatches } from "./migrationHelper.js";
4
+ import { tryAwaitWithRetry } from "../utils/helperFunctions.js";
4
5
  export const logOperation = async (db, dbId, operationDetails, operationId) => {
5
6
  try {
6
7
  let operation;
7
8
  if (operationId) {
8
9
  // Update existing operation log
9
- operation = await db.updateDocument("migrations", "currentOperations", operationId, operationDetails);
10
+ operation = await tryAwaitWithRetry(async () => await db.updateDocument("migrations", "currentOperations", operationId, operationDetails));
10
11
  }
11
12
  else {
12
13
  // Create new operation log
@@ -22,29 +23,29 @@ export const logOperation = async (db, dbId, operationDetails, operationId) => {
22
23
  };
23
24
  export const initOrGetBackupStorage = async (storage) => {
24
25
  try {
25
- const backupStorage = await storage.getBucket("backupStorage");
26
+ const backupStorage = await tryAwaitWithRetry(async () => await storage.getBucket("backupStorage"));
26
27
  return backupStorage;
27
28
  }
28
29
  catch (e) {
29
30
  // ID backupStorage
30
31
  // Name Backups Storage
31
- const backupStorage = await storage.createBucket("backupStorage", "Backups Storage");
32
+ const backupStorage = await tryAwaitWithRetry(async () => await storage.createBucket("backupStorage", "Backups Storage"));
32
33
  return backupStorage;
33
34
  }
34
35
  };
35
36
  export const initOrGetDocumentStorage = async (storage, config, dbName) => {
36
37
  try {
37
- await storage.getBucket(`${config.documentBucketId}_${dbName.toLowerCase().replace(" ", "")}`);
38
+ await tryAwaitWithRetry(async () => await storage.getBucket(`${config.documentBucketId}_${dbName.toLowerCase().replace(" ", "")}`));
38
39
  }
39
40
  catch (e) {
40
41
  // ID documentStorage
41
42
  // Name Document Storage
42
- const documentStorage = await storage.createBucket(`${config.documentBucketId}_${dbName.toLowerCase().replace(" ", "")}`, `Document Storage ${dbName}`, [
43
+ const documentStorage = await tryAwaitWithRetry(async () => await storage.createBucket(`${config.documentBucketId}_${dbName.toLowerCase().replace(" ", "")}`, `Document Storage ${dbName}`, [
43
44
  Permission.read("any"),
44
45
  Permission.create("users"),
45
46
  Permission.update("users"),
46
47
  Permission.delete("users"),
47
- ]);
48
+ ]));
48
49
  return documentStorage;
49
50
  }
50
51
  };
@@ -61,7 +62,7 @@ export const wipeDocumentStorage = async (storage, config, dbName) => {
61
62
  if (lastFileId) {
62
63
  queries.push(Query.cursorAfter(lastFileId));
63
64
  }
64
- const filesPulled = await storage.listFiles(bucketId, queries);
65
+ const filesPulled = await tryAwaitWithRetry(async () => await storage.listFiles(bucketId, queries));
65
66
  if (filesPulled.files.length === 0) {
66
67
  console.log("No files found, done!");
67
68
  moreFiles = false;
@@ -124,7 +125,7 @@ export const backupDatabase = async (database, databaseId, storage) => {
124
125
  // Fetch and backup the database details
125
126
  let db;
126
127
  try {
127
- db = await database.get(databaseId);
128
+ db = await tryAwaitWithRetry(async () => await database.get(databaseId));
128
129
  }
129
130
  catch (e) {
130
131
  console.error(`Error fetching database: ${e}`);
@@ -146,25 +147,25 @@ export const backupDatabase = async (database, databaseId, storage) => {
146
147
  let progress = 0;
147
148
  let total = 0; // Initialize total to 0, will be updated dynamically
148
149
  while (moreCollections) {
149
- const collectionResponse = await database.listCollections(databaseId, [
150
+ const collectionResponse = await tryAwaitWithRetry(async () => await database.listCollections(databaseId, [
150
151
  Query.limit(500), // Adjust the limit as needed
151
152
  ...(lastCollectionId ? [Query.cursorAfter(lastCollectionId)] : []),
152
- ]);
153
+ ]));
153
154
  total += collectionResponse.collections.length; // Update total with number of collections
154
155
  for (const { $id: collectionId, name: collectionName, } of collectionResponse.collections) {
155
156
  let collectionDocumentCount = 0; // Initialize document count for the current collection
156
157
  try {
157
- const collection = await database.getCollection(databaseId, collectionId);
158
+ const collection = await tryAwaitWithRetry(async () => await database.getCollection(databaseId, collectionId));
158
159
  progress++;
159
160
  data.collections.push(JSON.stringify(collection));
160
161
  // Initialize pagination for documents within the current collection
161
162
  let lastDocumentId = "";
162
163
  let moreDocuments = true;
163
164
  while (moreDocuments) {
164
- const documentResponse = await database.listDocuments(databaseId, collectionId, [
165
+ const documentResponse = await tryAwaitWithRetry(async () => await database.listDocuments(databaseId, collectionId, [
165
166
  Query.limit(500), // Adjust the limit as needed
166
167
  ...(lastDocumentId ? [Query.cursorAfter(lastDocumentId)] : []),
167
- ]);
168
+ ]));
168
169
  total += documentResponse.documents.length; // Update total with number of documents
169
170
  collectionDocumentCount += documentResponse.documents.length; // Update document count for the current collection
170
171
  let documentPromises = [];
@@ -3,6 +3,7 @@ import { AuthUserSchema, } from "../schemas/authUser.js";
3
3
  import _ from "lodash";
4
4
  import { logger } from "./logging.js";
5
5
  import { splitIntoBatches } from "./migrationHelper.js";
6
+ import { tryAwaitWithRetry } from "../utils/helperFunctions.js";
6
7
  export class UsersController {
7
8
  config;
8
9
  users;
@@ -22,49 +23,25 @@ export class UsersController {
22
23
  this.users = new Users(this.config.appwriteClient);
23
24
  }
24
25
  async wipeUsers() {
25
- const users = await this.users.list([Query.limit(200)]);
26
- const allUsers = users.users;
27
- let lastDocumentId;
28
- if (users.users.length >= 200) {
29
- lastDocumentId = users.users[users.users.length - 1].$id;
30
- }
31
- while (lastDocumentId) {
32
- const moreUsers = await this.users.list([
33
- Query.limit(200),
34
- Query.cursorAfter(lastDocumentId),
35
- ]);
36
- allUsers.push(...moreUsers.users);
37
- if (moreUsers.users.length < 200) {
38
- break;
39
- }
40
- lastDocumentId = moreUsers.users[moreUsers.users.length - 1].$id;
41
- }
26
+ const allUsers = await this.getAllUsers();
42
27
  console.log("Deleting all users...");
43
- const createBatches = (finalData) => {
44
- let maxBatchLength = 5;
28
+ const createBatches = (finalData, batchSize) => {
45
29
  const finalBatches = [];
46
- for (let i = 0; i < finalData.length; i++) {
47
- if (i % maxBatchLength === 0) {
48
- finalBatches.push([]);
49
- }
50
- finalBatches[finalBatches.length - 1].push(finalData[i]);
30
+ for (let i = 0; i < finalData.length; i += batchSize) {
31
+ finalBatches.push(finalData.slice(i, i + batchSize));
51
32
  }
52
33
  return finalBatches;
53
34
  };
54
- // const userPromises: Promise<string>[] = [];
55
35
  let usersDeleted = 0;
56
- for (const user of allUsers) {
57
- await this.users.delete(user.$id);
58
- usersDeleted++;
36
+ const batchedUserPromises = createBatches(allUsers, 50); // Batch size of 10
37
+ for (const batch of batchedUserPromises) {
38
+ console.log(`Deleting ${batch.length} users...`);
39
+ await Promise.all(batch.map((user) => tryAwaitWithRetry(async () => await this.users.delete(user.$id))));
40
+ usersDeleted += batch.length;
59
41
  if (usersDeleted % 100 === 0) {
60
42
  console.log(`Deleted ${usersDeleted} users...`);
61
43
  }
62
44
  }
63
- // const batchedUserPromises = createBatches(userPromises);
64
- // for (const batch of batchedUserPromises) {
65
- // console.log(`Deleting ${batch.length} users...`);
66
- // await Promise.all(batch);
67
- // }
68
45
  }
69
46
  async getAllUsers() {
70
47
  const allUsers = [];
@@ -76,10 +53,10 @@ export class UsersController {
76
53
  let lastDocumentId = users.users[users.users.length - 1].$id;
77
54
  allUsers.push(...users.users);
78
55
  while (lastDocumentId) {
79
- const moreUsers = await this.users.list([
56
+ const moreUsers = await tryAwaitWithRetry(async () => await this.users.list([
80
57
  Query.limit(200),
81
58
  Query.cursorAfter(lastDocumentId),
82
- ]);
59
+ ]));
83
60
  allUsers.push(...moreUsers.users);
84
61
  lastDocumentId = moreUsers.users[moreUsers.users.length - 1].$id;
85
62
  if (moreUsers.users.length < 200) {
@@ -113,7 +90,8 @@ export class UsersController {
113
90
  }
114
91
  catch (e) {
115
92
  if (e instanceof AppwriteException) {
116
- if (e.message.includes("fetch failed")) {
93
+ if (e.message.toLowerCase().includes("fetch failed") ||
94
+ e.message.toLowerCase().includes("server error")) {
117
95
  const numberOfAttempts = numAttempts || 0;
118
96
  if (numberOfAttempts > 5) {
119
97
  throw e;
@@ -1,4 +1,4 @@
1
- import type { Models } from "node-appwrite";
1
+ import { type Models } from "node-appwrite";
2
2
  import type { CollectionImportData } from "../migrations/dataLoader.js";
3
3
  import type { ConfigCollection } from "appwrite-utils";
4
4
  export declare const toPascalCase: (str: string) => string;
@@ -35,3 +35,12 @@ export declare const getFileViewUrl: (endpoint: string, projectId: string, bucke
35
35
  */
36
36
  export declare const getFileDownloadUrl: (endpoint: string, projectId: string, bucketId: string, fileId: string, jwt?: Models.Jwt) => string;
37
37
  export declare const finalizeByAttributeMap: (appwriteFolderPath: string, collection: ConfigCollection, item: CollectionImportData["data"][number]) => Promise<any>;
38
+ export declare let numTimesFailedTotal: number;
39
+ /**
40
+ * Tries to execute the given createFunction and retries up to 5 times if it fails.
41
+ *
42
+ * @param {() => Promise<any>} createFunction - The function to be executed.
43
+ * @param {number} [attemptNum=0] - The number of attempts made so far (default: 0).
44
+ * @return {Promise<any>} - A promise that resolves to the result of the createFunction or rejects with an error if it fails after 5 attempts.
45
+ */
46
+ export declare const tryAwaitWithRetry: <T>(createFunction: () => Promise<T>, attemptNum?: number) => Promise<T>;
@@ -1,3 +1,4 @@
1
+ import { AppwriteException } from "node-appwrite";
1
2
  import fs from "node:fs";
2
3
  import path from "node:path";
3
4
  export const toPascalCase = (str) => {
@@ -78,3 +79,29 @@ export const finalizeByAttributeMap = async (appwriteFolderPath, collection, ite
78
79
  ...item.finalData,
79
80
  });
80
81
  };
82
+ export let numTimesFailedTotal = 0;
83
+ /**
84
+ * Tries to execute the given createFunction and retries up to 5 times if it fails.
85
+ *
86
+ * @param {() => Promise<any>} createFunction - The function to be executed.
87
+ * @param {number} [attemptNum=0] - The number of attempts made so far (default: 0).
88
+ * @return {Promise<any>} - A promise that resolves to the result of the createFunction or rejects with an error if it fails after 5 attempts.
89
+ */
90
+ export const tryAwaitWithRetry = async (createFunction, attemptNum = 0) => {
91
+ try {
92
+ return await createFunction();
93
+ }
94
+ catch (error) {
95
+ if (error instanceof AppwriteException &&
96
+ (error.message.toLowerCase().includes("fetch failed") ||
97
+ error.message.toLowerCase().includes("server error"))) {
98
+ numTimesFailedTotal++;
99
+ console.log(`Fetch failed on attempt ${attemptNum}. Retrying...`);
100
+ if (attemptNum > 5) {
101
+ throw error;
102
+ }
103
+ return tryAwaitWithRetry(createFunction, attemptNum + 1);
104
+ }
105
+ throw error;
106
+ }
107
+ };
@@ -4,7 +4,7 @@ import path from "path";
4
4
  import fs from "fs";
5
5
  import { load } from "js-yaml";
6
6
  import { ImportDataActions } from "./migrations/importDataActions.js";
7
- import { readFileSync } from "./utils/helperFunctions.js";
7
+ import { numTimesFailedTotal } from "./utils/helperFunctions.js";
8
8
  import { afterImportActions } from "./migrations/afterImportActions.js";
9
9
  import { converterFunctions, validationRules, AppwriteConfigSchema, } from "appwrite-utils";
10
10
  import { ImportController } from "./migrations/importController.js";
@@ -117,6 +117,7 @@ export class UtilsController {
117
117
  await importController.run();
118
118
  console.log("Import data complete.");
119
119
  }
120
+ console.log("Total number of times Fetch failed: ", numTimesFailedTotal);
120
121
  // if (options.checkDuplicates) {
121
122
  // await this.checkDuplicates();
122
123
  // }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "appwrite-utils-cli",
3
3
  "description": "Appwrite Utility Functions to help with database management, data conversion, data import, migrations, and much more. Meant to be used as a CLI tool, I do not recommend installing this in frontend environments.",
4
- "version": "0.0.37",
4
+ "version": "0.0.39",
5
5
  "main": "src/main.ts",
6
6
  "type": "module",
7
7
  "repository": {
@@ -33,7 +33,7 @@
33
33
  },
34
34
  "dependencies": {
35
35
  "@types/inquirer": "^9.0.7",
36
- "appwrite-utils": "^0.2.5",
36
+ "appwrite-utils": "^0.2.6",
37
37
  "commander": "^12.0.0",
38
38
  "inquirer": "^9.2.20",
39
39
  "js-yaml": "^4.1.0",