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
@@ -3,6 +3,7 @@ import { BatchSchema, OperationSchema, type Operation } from "./backup.js";
3
3
  import { AttributeMappingsSchema } from "appwrite-utils";
4
4
  import { z } from "zod";
5
5
  import { logger } from "./logging.js";
6
+ import { tryAwaitWithRetry } from "src/utils/helperFunctions.js";
6
7
 
7
8
  /**
8
9
  * Object that contains the context for an action that needs to be executed after import
@@ -148,11 +149,14 @@ export const updateOperation = async (
148
149
  operationId: string,
149
150
  updateFields: any
150
151
  ) => {
151
- await database.updateDocument(
152
- "migrations",
153
- "currentOperations",
154
- operationId,
155
- updateFields
152
+ await tryAwaitWithRetry(
153
+ async () =>
154
+ await database.updateDocument(
155
+ "migrations",
156
+ "currentOperations",
157
+ operationId,
158
+ updateFields
159
+ )
156
160
  );
157
161
  };
158
162
 
@@ -3,6 +3,7 @@ import type { Attribute } from "appwrite-utils";
3
3
  import { createOrUpdateAttribute } from "./attributes.js";
4
4
  import _ from "lodash";
5
5
  import { fetchAndCacheCollectionByName } from "./collections.js";
6
+ import { tryAwaitWithRetry } from "../utils/helperFunctions.js";
6
7
 
7
8
  export interface QueuedOperation {
8
9
  type: "attribute";
@@ -37,9 +38,8 @@ export const processQueue = async (db: Databases, dbId: string) => {
37
38
  if (operation.collectionId) {
38
39
  console.log(`\tFetching collection by ID: ${operation.collectionId}`);
39
40
  try {
40
- collectionFound = await db.getCollection(
41
- dbId,
42
- operation.collectionId
41
+ collectionFound = await tryAwaitWithRetry(
42
+ async () => await db.getCollection(dbId, operation.collectionId!)
43
43
  );
44
44
  } catch (e) {
45
45
  console.log(
@@ -70,9 +70,8 @@ export const processQueue = async (db: Databases, dbId: string) => {
70
70
  // Handle non-relationship operations with a specified collectionId
71
71
  console.log(`\tFetching collection by ID: ${operation.collectionId}`);
72
72
  try {
73
- collectionFound = await db.getCollection(
74
- dbId,
75
- operation.collectionId
73
+ collectionFound = await tryAwaitWithRetry(
74
+ async () => await db.getCollection(dbId, operation.collectionId!)
76
75
  );
77
76
  } catch (e) {
78
77
  console.log(
@@ -6,7 +6,11 @@ import {
6
6
  wipeDatabase,
7
7
  } from "./collections.js";
8
8
  import { getMigrationCollectionSchemas } from "./backup.js";
9
- import { areCollectionNamesSame, toCamelCase } from "../utils/index.js";
9
+ import {
10
+ areCollectionNamesSame,
11
+ toCamelCase,
12
+ tryAwaitWithRetry,
13
+ } from "../utils/index.js";
10
14
  import {
11
15
  backupDatabase,
12
16
  initOrGetBackupStorage,
@@ -28,16 +32,18 @@ export const setupMigrationDatabase = async (config: AppwriteConfig) => {
28
32
  const dbCollections: Models.Collection[] = [];
29
33
  const migrationCollectionsSetup = getMigrationCollectionSchemas();
30
34
  try {
31
- db = await database.get("migrations");
35
+ db = await tryAwaitWithRetry(async () => await database.get("migrations"));
32
36
  console.log("Migrations database found");
33
37
  } catch (e) {
34
- db = await database.create("migrations", "Migrations", true);
38
+ db = await tryAwaitWithRetry(
39
+ async () => await database.create("migrations", "Migrations", true)
40
+ );
35
41
  console.log("Migrations database created");
36
42
  }
37
43
  if (db) {
38
- const collectionsPulled = await database.listCollections(db.$id, [
39
- Query.limit(25),
40
- ]);
44
+ const collectionsPulled = await tryAwaitWithRetry(
45
+ async () => await database.listCollections(db.$id, [Query.limit(25)])
46
+ );
41
47
  dbCollections.push(...collectionsPulled.collections);
42
48
  }
43
49
  console.log(`Collections in migrations database: ${dbCollections.length}`);
@@ -49,19 +55,24 @@ export const setupMigrationDatabase = async (config: AppwriteConfig) => {
49
55
  const collectionId = toCamelCase(collectionName); // Convert name to toCamelCase for the ID
50
56
  let collectionFound: Models.Collection | null = null;
51
57
  try {
52
- collectionFound = await database.getCollection(db.$id, collectionId);
58
+ collectionFound = await tryAwaitWithRetry(
59
+ async () => await database.getCollection(db.$id, collectionId)
60
+ );
53
61
  } catch (e) {
54
62
  console.log(`Collection not found: ${collectionId}`);
55
63
  }
56
64
  if (!collectionFound) {
57
65
  // Create the collection with the provided configuration
58
- collectionFound = await database.createCollection(
59
- db.$id,
60
- collectionId,
61
- collectionName,
62
- undefined,
63
- collection.documentSecurity,
64
- collection.enabled
66
+ collectionFound = await tryAwaitWithRetry(
67
+ async () =>
68
+ await database.createCollection(
69
+ db.$id,
70
+ collectionId,
71
+ collectionName,
72
+ undefined,
73
+ collection.documentSecurity,
74
+ collection.enabled
75
+ )
65
76
  );
66
77
  }
67
78
  for (const attribute of attributes) {
@@ -87,11 +98,15 @@ export const ensureDatabasesExist = async (config: AppwriteConfig) => {
87
98
  });
88
99
  const dbNames = databasesToEnsure.map((db) => db.name);
89
100
 
90
- const existingDatabases = await database.list([Query.equal("name", dbNames)]);
101
+ const existingDatabases = await tryAwaitWithRetry(
102
+ async () => await database.list([Query.equal("name", dbNames)])
103
+ );
91
104
 
92
105
  for (const db of databasesToEnsure) {
93
106
  if (!existingDatabases.databases.some((d) => d.name === db.name)) {
94
- await database.create(db.$id || ID.unique(), db.name, true);
107
+ await tryAwaitWithRetry(
108
+ async () => await database.create(db.$id || ID.unique(), db.name, true)
109
+ );
95
110
  console.log(`${db.name} database created`);
96
111
  }
97
112
  }
@@ -106,12 +121,14 @@ export const wipeOtherDatabases = async (
106
121
  );
107
122
  databasesToKeep.push("migrations");
108
123
  console.log(`Databases to keep: ${databasesToKeep.join(", ")}`);
109
- const allDatabases = await database.list([Query.limit(500)]);
124
+ const allDatabases = await tryAwaitWithRetry(
125
+ async () => await database.list([Query.limit(500)])
126
+ );
110
127
  for (const db of allDatabases.databases) {
111
128
  if (
112
129
  !databasesToKeep.includes(db.name.toLowerCase().trim().replace(" ", ""))
113
130
  ) {
114
- await database.delete(db.$id);
131
+ await tryAwaitWithRetry(async () => await database.delete(db.$id));
115
132
  console.log(`Deleted database: ${db.name}`);
116
133
  }
117
134
  }
@@ -10,6 +10,7 @@ import {
10
10
  import { type OperationCreate, type BackupCreate } from "./backup.js";
11
11
  import { splitIntoBatches } from "./migrationHelper.js";
12
12
  import type { AppwriteConfig } from "appwrite-utils";
13
+ import { tryAwaitWithRetry } from "../utils/helperFunctions.js";
13
14
 
14
15
  export const logOperation = async (
15
16
  db: Databases,
@@ -21,11 +22,14 @@ export const logOperation = async (
21
22
  let operation;
22
23
  if (operationId) {
23
24
  // Update existing operation log
24
- operation = await db.updateDocument(
25
- "migrations",
26
- "currentOperations",
27
- operationId,
28
- operationDetails
25
+ operation = await tryAwaitWithRetry(
26
+ async () =>
27
+ await db.updateDocument(
28
+ "migrations",
29
+ "currentOperations",
30
+ operationId,
31
+ operationDetails
32
+ )
29
33
  );
30
34
  } else {
31
35
  // Create new operation log
@@ -46,14 +50,15 @@ export const logOperation = async (
46
50
 
47
51
  export const initOrGetBackupStorage = async (storage: Storage) => {
48
52
  try {
49
- const backupStorage = await storage.getBucket("backupStorage");
53
+ const backupStorage = await tryAwaitWithRetry(
54
+ async () => await storage.getBucket("backupStorage")
55
+ );
50
56
  return backupStorage;
51
57
  } catch (e) {
52
58
  // ID backupStorage
53
59
  // Name Backups Storage
54
- const backupStorage = await storage.createBucket(
55
- "backupStorage",
56
- "Backups Storage"
60
+ const backupStorage = await tryAwaitWithRetry(
61
+ async () => await storage.createBucket("backupStorage", "Backups Storage")
57
62
  );
58
63
  return backupStorage;
59
64
  }
@@ -65,21 +70,27 @@ export const initOrGetDocumentStorage = async (
65
70
  dbName: string
66
71
  ) => {
67
72
  try {
68
- await storage.getBucket(
69
- `${config.documentBucketId}_${dbName.toLowerCase().replace(" ", "")}`
73
+ await tryAwaitWithRetry(
74
+ async () =>
75
+ await storage.getBucket(
76
+ `${config.documentBucketId}_${dbName.toLowerCase().replace(" ", "")}`
77
+ )
70
78
  );
71
79
  } catch (e) {
72
80
  // ID documentStorage
73
81
  // Name Document Storage
74
- const documentStorage = await storage.createBucket(
75
- `${config.documentBucketId}_${dbName.toLowerCase().replace(" ", "")}`,
76
- `Document Storage ${dbName}`,
77
- [
78
- Permission.read("any"),
79
- Permission.create("users"),
80
- Permission.update("users"),
81
- Permission.delete("users"),
82
- ]
82
+ const documentStorage = await tryAwaitWithRetry(
83
+ async () =>
84
+ await storage.createBucket(
85
+ `${config.documentBucketId}_${dbName.toLowerCase().replace(" ", "")}`,
86
+ `Document Storage ${dbName}`,
87
+ [
88
+ Permission.read("any"),
89
+ Permission.create("users"),
90
+ Permission.update("users"),
91
+ Permission.delete("users"),
92
+ ]
93
+ )
83
94
  );
84
95
  return documentStorage;
85
96
  }
@@ -102,7 +113,9 @@ export const wipeDocumentStorage = async (
102
113
  if (lastFileId) {
103
114
  queries.push(Query.cursorAfter(lastFileId));
104
115
  }
105
- const filesPulled = await storage.listFiles(bucketId, queries);
116
+ const filesPulled = await tryAwaitWithRetry(
117
+ async () => await storage.listFiles(bucketId, queries)
118
+ );
106
119
  if (filesPulled.files.length === 0) {
107
120
  console.log("No files found, done!");
108
121
  moreFiles = false;
@@ -177,7 +190,7 @@ export const backupDatabase = async (
177
190
  // Fetch and backup the database details
178
191
  let db: Models.Database;
179
192
  try {
180
- db = await database.get(databaseId);
193
+ db = await tryAwaitWithRetry(async () => await database.get(databaseId));
181
194
  } catch (e) {
182
195
  console.error(`Error fetching database: ${e}`);
183
196
  await logOperation(
@@ -205,10 +218,13 @@ export const backupDatabase = async (
205
218
  let total = 0; // Initialize total to 0, will be updated dynamically
206
219
 
207
220
  while (moreCollections) {
208
- const collectionResponse = await database.listCollections(databaseId, [
209
- Query.limit(500), // Adjust the limit as needed
210
- ...(lastCollectionId ? [Query.cursorAfter(lastCollectionId)] : []),
211
- ]);
221
+ const collectionResponse = await tryAwaitWithRetry(
222
+ async () =>
223
+ await database.listCollections(databaseId, [
224
+ Query.limit(500), // Adjust the limit as needed
225
+ ...(lastCollectionId ? [Query.cursorAfter(lastCollectionId)] : []),
226
+ ])
227
+ );
212
228
 
213
229
  total += collectionResponse.collections.length; // Update total with number of collections
214
230
 
@@ -218,9 +234,8 @@ export const backupDatabase = async (
218
234
  } of collectionResponse.collections) {
219
235
  let collectionDocumentCount = 0; // Initialize document count for the current collection
220
236
  try {
221
- const collection = await database.getCollection(
222
- databaseId,
223
- collectionId
237
+ const collection = await tryAwaitWithRetry(
238
+ async () => await database.getCollection(databaseId, collectionId)
224
239
  );
225
240
  progress++;
226
241
  data.collections.push(JSON.stringify(collection));
@@ -230,13 +245,12 @@ export const backupDatabase = async (
230
245
  let moreDocuments = true;
231
246
 
232
247
  while (moreDocuments) {
233
- const documentResponse = await database.listDocuments(
234
- databaseId,
235
- collectionId,
236
- [
237
- Query.limit(500), // Adjust the limit as needed
238
- ...(lastDocumentId ? [Query.cursorAfter(lastDocumentId)] : []),
239
- ]
248
+ const documentResponse = await tryAwaitWithRetry(
249
+ async () =>
250
+ await database.listDocuments(databaseId, collectionId, [
251
+ Query.limit(500), // Adjust the limit as needed
252
+ ...(lastDocumentId ? [Query.cursorAfter(lastDocumentId)] : []),
253
+ ])
240
254
  );
241
255
 
242
256
  total += documentResponse.documents.length; // Update total with number of documents
@@ -15,6 +15,7 @@ import {
15
15
  import _ from "lodash";
16
16
  import { logger } from "./logging.js";
17
17
  import { splitIntoBatches } from "./migrationHelper.js";
18
+ import { tryAwaitWithRetry } from "../utils/helperFunctions.js";
18
19
 
19
20
  export class UsersController {
20
21
  private config: AppwriteConfig;
@@ -37,49 +38,32 @@ export class UsersController {
37
38
  }
38
39
 
39
40
  async wipeUsers() {
40
- const users = await this.users.list([Query.limit(200)]);
41
- const allUsers = users.users;
42
- let lastDocumentId: string | undefined;
43
- if (users.users.length >= 200) {
44
- lastDocumentId = users.users[users.users.length - 1].$id;
45
- }
46
- while (lastDocumentId) {
47
- const moreUsers = await this.users.list([
48
- Query.limit(200),
49
- Query.cursorAfter(lastDocumentId),
50
- ]);
51
- allUsers.push(...moreUsers.users);
52
- if (moreUsers.users.length < 200) {
53
- break;
54
- }
55
- lastDocumentId = moreUsers.users[moreUsers.users.length - 1].$id;
56
- }
41
+ const allUsers = await this.getAllUsers();
57
42
  console.log("Deleting all users...");
58
- const createBatches = (finalData: any[]) => {
59
- let maxBatchLength = 5;
43
+
44
+ const createBatches = (finalData: any[], batchSize: number) => {
60
45
  const finalBatches: any[][] = [];
61
- for (let i = 0; i < finalData.length; i++) {
62
- if (i % maxBatchLength === 0) {
63
- finalBatches.push([]);
64
- }
65
- finalBatches[finalBatches.length - 1].push(finalData[i]);
46
+ for (let i = 0; i < finalData.length; i += batchSize) {
47
+ finalBatches.push(finalData.slice(i, i + batchSize));
66
48
  }
67
49
  return finalBatches;
68
50
  };
69
- // const userPromises: Promise<string>[] = [];
51
+
70
52
  let usersDeleted = 0;
71
- for (const user of allUsers) {
72
- await this.users.delete(user.$id);
73
- usersDeleted++;
53
+ const batchedUserPromises = createBatches(allUsers, 50); // Batch size of 10
54
+
55
+ for (const batch of batchedUserPromises) {
56
+ console.log(`Deleting ${batch.length} users...`);
57
+ await Promise.all(
58
+ batch.map((user) =>
59
+ tryAwaitWithRetry(async () => await this.users.delete(user.$id))
60
+ )
61
+ );
62
+ usersDeleted += batch.length;
74
63
  if (usersDeleted % 100 === 0) {
75
64
  console.log(`Deleted ${usersDeleted} users...`);
76
65
  }
77
66
  }
78
- // const batchedUserPromises = createBatches(userPromises);
79
- // for (const batch of batchedUserPromises) {
80
- // console.log(`Deleting ${batch.length} users...`);
81
- // await Promise.all(batch);
82
- // }
83
67
  }
84
68
 
85
69
  async getAllUsers() {
@@ -92,10 +76,13 @@ export class UsersController {
92
76
  let lastDocumentId = users.users[users.users.length - 1].$id;
93
77
  allUsers.push(...users.users);
94
78
  while (lastDocumentId) {
95
- const moreUsers = await this.users.list([
96
- Query.limit(200),
97
- Query.cursorAfter(lastDocumentId),
98
- ]);
79
+ const moreUsers = await tryAwaitWithRetry(
80
+ async () =>
81
+ await this.users.list([
82
+ Query.limit(200),
83
+ Query.cursorAfter(lastDocumentId),
84
+ ])
85
+ );
99
86
  allUsers.push(...moreUsers.users);
100
87
  lastDocumentId = moreUsers.users[moreUsers.users.length - 1].$id;
101
88
  if (moreUsers.users.length < 200) {
@@ -137,7 +124,10 @@ export class UsersController {
137
124
  return user;
138
125
  } catch (e) {
139
126
  if (e instanceof AppwriteException) {
140
- if (e.message.includes("fetch failed")) {
127
+ if (
128
+ e.message.toLowerCase().includes("fetch failed") ||
129
+ e.message.toLowerCase().includes("server error")
130
+ ) {
141
131
  const numberOfAttempts = numAttempts || 0;
142
132
  if (numberOfAttempts > 5) {
143
133
  throw e;
@@ -1,4 +1,4 @@
1
- import type { Models, Storage } from "node-appwrite";
1
+ import { AppwriteException, type Models, type Storage } from "node-appwrite";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import type { CollectionImportData } from "../migrations/dataLoader.js";
@@ -125,3 +125,35 @@ export const finalizeByAttributeMap = async (
125
125
  ...item.finalData,
126
126
  });
127
127
  };
128
+
129
+ export let numTimesFailedTotal = 0;
130
+
131
+ /**
132
+ * Tries to execute the given createFunction and retries up to 5 times if it fails.
133
+ *
134
+ * @param {() => Promise<any>} createFunction - The function to be executed.
135
+ * @param {number} [attemptNum=0] - The number of attempts made so far (default: 0).
136
+ * @return {Promise<any>} - A promise that resolves to the result of the createFunction or rejects with an error if it fails after 5 attempts.
137
+ */
138
+ export const tryAwaitWithRetry = async <T>(
139
+ createFunction: () => Promise<T>,
140
+ attemptNum: number = 0
141
+ ): Promise<T> => {
142
+ try {
143
+ return await createFunction();
144
+ } catch (error) {
145
+ if (
146
+ error instanceof AppwriteException &&
147
+ (error.message.toLowerCase().includes("fetch failed") ||
148
+ error.message.toLowerCase().includes("server error"))
149
+ ) {
150
+ numTimesFailedTotal++;
151
+ console.log(`Fetch failed on attempt ${attemptNum}. Retrying...`);
152
+ if (attemptNum > 5) {
153
+ throw error;
154
+ }
155
+ return tryAwaitWithRetry(createFunction, attemptNum + 1);
156
+ }
157
+ throw error;
158
+ }
159
+ };
@@ -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 {
10
10
  type AfterImportActions,
@@ -187,6 +187,8 @@ export class UtilsController {
187
187
  console.log("Import data complete.");
188
188
  }
189
189
 
190
+ console.log("Total number of times Fetch failed: ", numTimesFailedTotal);
191
+
190
192
  // if (options.checkDuplicates) {
191
193
  // await this.checkDuplicates();
192
194
  // }