appwrite-utils-cli 0.0.37 → 0.0.38
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 +1 -0
- package/dist/migrations/attributes.js +21 -20
- package/dist/migrations/collections.js +10 -13
- package/dist/migrations/dataLoader.d.ts +14 -23
- package/dist/migrations/dataLoader.js +248 -170
- package/dist/migrations/databases.js +2 -1
- package/dist/migrations/importController.js +6 -24
- package/dist/migrations/importDataActions.d.ts +1 -1
- package/dist/migrations/importDataActions.js +3 -2
- package/dist/migrations/indexes.js +2 -1
- package/dist/migrations/migrationHelper.js +2 -1
- package/dist/migrations/queue.js +3 -2
- package/dist/migrations/setupDatabase.js +10 -12
- package/dist/migrations/storage.js +14 -13
- package/dist/migrations/users.js +14 -36
- package/dist/utils/helperFunctions.d.ts +10 -1
- package/dist/utils/helperFunctions.js +27 -0
- package/dist/utilsController.js +2 -1
- package/package.json +2 -2
- package/src/migrations/attributes.ts +204 -143
- package/src/migrations/collections.ts +49 -33
- package/src/migrations/dataLoader.ts +377 -240
- package/src/migrations/databases.ts +4 -1
- package/src/migrations/importController.ts +13 -27
- package/src/migrations/importDataActions.ts +6 -3
- package/src/migrations/indexes.ts +4 -1
- package/src/migrations/migrationHelper.ts +9 -5
- package/src/migrations/queue.ts +5 -6
- package/src/migrations/setupDatabase.ts +35 -18
- package/src/migrations/storage.ts +50 -36
- package/src/migrations/users.ts +28 -38
- package/src/utils/helperFunctions.ts +33 -1
- package/src/utilsController.ts +3 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { ID, Query, } from "node-appwrite";
|
|
1
|
+
import { AppwriteException, ID, Query, } from "node-appwrite";
|
|
2
2
|
import _ from "lodash";
|
|
3
|
-
import { areCollectionNamesSame } from "../utils/index.js";
|
|
3
|
+
import { areCollectionNamesSame, tryAwaitWithRetry } from "../utils/index.js";
|
|
4
4
|
import { resolveAndUpdateRelationships } from "./relationships.js";
|
|
5
5
|
import { UsersController } from "./users.js";
|
|
6
6
|
import { logger } from "./logging.js";
|
|
@@ -71,7 +71,7 @@ export class ImportController {
|
|
|
71
71
|
dataLoader.getCollectionKey(collection.name);
|
|
72
72
|
const importOperationId = dataLoader.collectionImportOperations.get(dataLoader.getCollectionKey(collection.name));
|
|
73
73
|
const createBatches = (finalData) => {
|
|
74
|
-
let maxBatchLength =
|
|
74
|
+
let maxBatchLength = 25;
|
|
75
75
|
const finalBatches = [];
|
|
76
76
|
for (let i = 0; i < finalData.length; i++) {
|
|
77
77
|
if (i % maxBatchLength === 0) {
|
|
@@ -109,7 +109,7 @@ export class ImportController {
|
|
|
109
109
|
.map((item) => {
|
|
110
110
|
return usersController.createUserAndReturn(item.finalData);
|
|
111
111
|
});
|
|
112
|
-
await Promise.
|
|
112
|
+
const promiseResults = await Promise.allSettled(userBatchPromises);
|
|
113
113
|
for (const item of batch) {
|
|
114
114
|
if (item && item.finalData) {
|
|
115
115
|
dataLoader.userExistsMap.set(item.finalData.userId ||
|
|
@@ -118,26 +118,8 @@ export class ImportController {
|
|
|
118
118
|
item.context.docId, true);
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
|
-
console.log("Finished importing users batch"
|
|
121
|
+
console.log("Finished importing users batch");
|
|
122
122
|
}
|
|
123
|
-
// for (let i = 0; i < usersData.length; i++) {
|
|
124
|
-
// const user = usersData[i];
|
|
125
|
-
// if (user.finalData) {
|
|
126
|
-
// const userId =
|
|
127
|
-
// user.finalData.userId ||
|
|
128
|
-
// user.context.userId ||
|
|
129
|
-
// user.context.docId;
|
|
130
|
-
// if (!dataLoader.userExistsMap.has(userId)) {
|
|
131
|
-
// if (!user.finalData.userId) {
|
|
132
|
-
// user.finalData.userId = userId;
|
|
133
|
-
// }
|
|
134
|
-
// await usersController.createUserAndReturn(user.finalData);
|
|
135
|
-
// dataLoader.userExistsMap.set(userId, true);
|
|
136
|
-
// } else {
|
|
137
|
-
// console.log("Skipped existing user: ", userId);
|
|
138
|
-
// }
|
|
139
|
-
// }
|
|
140
|
-
// }
|
|
141
123
|
console.log("Finished importing users");
|
|
142
124
|
}
|
|
143
125
|
}
|
|
@@ -173,7 +155,7 @@ export class ImportController {
|
|
|
173
155
|
if (!item.finalData) {
|
|
174
156
|
return Promise.resolve();
|
|
175
157
|
}
|
|
176
|
-
return this.database.createDocument(db.$id, collection.$id, id, item.finalData);
|
|
158
|
+
return tryAwaitWithRetry(async () => await this.database.createDocument(db.$id, collection.$id, id, item.finalData));
|
|
177
159
|
});
|
|
178
160
|
// Wait for all promises in the current batch to resolve
|
|
179
161
|
await Promise.allSettled(batchPromises);
|
|
@@ -20,7 +20,7 @@ export declare class ImportDataActions {
|
|
|
20
20
|
*/
|
|
21
21
|
validateItem(item: any, attributeMap: AttributeMappings, context: {
|
|
22
22
|
[key: string]: any;
|
|
23
|
-
}):
|
|
23
|
+
}): boolean;
|
|
24
24
|
executeAfterImportActions(item: any, attributeMap: AttributeMappings, context: {
|
|
25
25
|
[key: string]: any;
|
|
26
26
|
}): Promise<void>;
|
|
@@ -5,6 +5,7 @@ import { convertObjectBySchema } from "./converters.js";
|
|
|
5
5
|
import {} from "appwrite-utils";
|
|
6
6
|
import { afterImportActions } from "./afterImportActions.js";
|
|
7
7
|
import { logger } from "./logging.js";
|
|
8
|
+
import { tryAwaitWithRetry } from "../utils/helperFunctions.js";
|
|
8
9
|
export class ImportDataActions {
|
|
9
10
|
db;
|
|
10
11
|
storage;
|
|
@@ -68,7 +69,7 @@ export class ImportDataActions {
|
|
|
68
69
|
* @param context The context for resolving templated parameters in validation rules.
|
|
69
70
|
* @returns A promise that resolves to true if the item is valid, false otherwise.
|
|
70
71
|
*/
|
|
71
|
-
|
|
72
|
+
validateItem(item, attributeMap, context) {
|
|
72
73
|
for (const mapping of attributeMap) {
|
|
73
74
|
const { validationActions } = mapping;
|
|
74
75
|
if (!validationActions ||
|
|
@@ -111,7 +112,7 @@ export class ImportDataActions {
|
|
|
111
112
|
const { action, params } = actionDef;
|
|
112
113
|
console.log(`Executing post-import action '${action}' for attribute '${mapping.targetKey}' with params ${params.join(", ")}...`);
|
|
113
114
|
try {
|
|
114
|
-
await this.executeAction(action, params, context, item);
|
|
115
|
+
await tryAwaitWithRetry(async () => await this.executeAction(action, params, context, item));
|
|
115
116
|
}
|
|
116
117
|
catch (error) {
|
|
117
118
|
logger.error(`Failed to execute post-import action '${action}' for attribute '${mapping.targetKey}':`, error);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { indexSchema } from "appwrite-utils";
|
|
2
2
|
import { Databases, Query } from "node-appwrite";
|
|
3
|
+
import { tryAwaitWithRetry } from "../utils/helperFunctions.js";
|
|
3
4
|
// import {}
|
|
4
5
|
export const createOrUpdateIndex = async (dbId, db, collectionId, index) => {
|
|
5
6
|
const existingIndex = await db.listIndexes(dbId, collectionId, [
|
|
@@ -13,6 +14,6 @@ export const createOrUpdateIndex = async (dbId, db, collectionId, index) => {
|
|
|
13
14
|
};
|
|
14
15
|
export const createOrUpdateIndexes = async (dbId, db, collectionId, indexes) => {
|
|
15
16
|
for (const index of indexes) {
|
|
16
|
-
await createOrUpdateIndex(dbId, db, collectionId, index);
|
|
17
|
+
await tryAwaitWithRetry(async () => await createOrUpdateIndex(dbId, db, collectionId, index));
|
|
17
18
|
}
|
|
18
19
|
};
|
|
@@ -3,6 +3,7 @@ import { BatchSchema, OperationSchema } 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
|
* Object that contains the context for an action that needs to be executed after import
|
|
8
9
|
* Used in the afterImportActionsDefinitions
|
|
@@ -90,7 +91,7 @@ export const findOrCreateOperation = async (database, collectionId, operationTyp
|
|
|
90
91
|
}
|
|
91
92
|
};
|
|
92
93
|
export const updateOperation = async (database, operationId, updateFields) => {
|
|
93
|
-
await database.updateDocument("migrations", "currentOperations", operationId, updateFields);
|
|
94
|
+
await tryAwaitWithRetry(async () => await database.updateDocument("migrations", "currentOperations", operationId, updateFields));
|
|
94
95
|
};
|
|
95
96
|
// Actual max 1073741824
|
|
96
97
|
export const maxDataLength = 1073741820;
|
package/dist/migrations/queue.js
CHANGED
|
@@ -2,6 +2,7 @@ import { Query } from "node-appwrite";
|
|
|
2
2
|
import { createOrUpdateAttribute } from "./attributes.js";
|
|
3
3
|
import _ from "lodash";
|
|
4
4
|
import { fetchAndCacheCollectionByName } from "./collections.js";
|
|
5
|
+
import { tryAwaitWithRetry } from "../utils/helperFunctions.js";
|
|
5
6
|
export const queuedOperations = [];
|
|
6
7
|
export const nameToIdMapping = new Map();
|
|
7
8
|
export const enqueueOperation = (operation) => {
|
|
@@ -24,7 +25,7 @@ export const processQueue = async (db, dbId) => {
|
|
|
24
25
|
if (operation.collectionId) {
|
|
25
26
|
console.log(`\tFetching collection by ID: ${operation.collectionId}`);
|
|
26
27
|
try {
|
|
27
|
-
collectionFound = await db.getCollection(dbId, operation.collectionId);
|
|
28
|
+
collectionFound = await tryAwaitWithRetry(async () => await db.getCollection(dbId, operation.collectionId));
|
|
28
29
|
}
|
|
29
30
|
catch (e) {
|
|
30
31
|
console.log(`\tCollection not found by ID: ${operation.collectionId}`);
|
|
@@ -47,7 +48,7 @@ export const processQueue = async (db, dbId) => {
|
|
|
47
48
|
// Handle non-relationship operations with a specified collectionId
|
|
48
49
|
console.log(`\tFetching collection by ID: ${operation.collectionId}`);
|
|
49
50
|
try {
|
|
50
|
-
collectionFound = await db.getCollection(dbId, operation.collectionId);
|
|
51
|
+
collectionFound = await tryAwaitWithRetry(async () => await db.getCollection(dbId, operation.collectionId));
|
|
51
52
|
}
|
|
52
53
|
catch (e) {
|
|
53
54
|
console.log(`\tCollection not found by ID: ${operation.collectionId}`);
|
|
@@ -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 = [];
|
package/dist/migrations/users.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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
|
+
};
|
package/dist/utilsController.js
CHANGED
|
@@ -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 {
|
|
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.
|
|
4
|
+
"version": "0.0.38",
|
|
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.
|
|
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",
|