appwrite-utils-cli 0.0.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.
Files changed (86) hide show
  1. package/README.md +80 -0
  2. package/dist/main.d.ts +2 -0
  3. package/dist/main.js +74 -0
  4. package/dist/migrations/afterImportActions.d.ts +12 -0
  5. package/dist/migrations/afterImportActions.js +196 -0
  6. package/dist/migrations/attributes.d.ts +4 -0
  7. package/dist/migrations/attributes.js +158 -0
  8. package/dist/migrations/backup.d.ts +621 -0
  9. package/dist/migrations/backup.js +159 -0
  10. package/dist/migrations/collections.d.ts +16 -0
  11. package/dist/migrations/collections.js +207 -0
  12. package/dist/migrations/converters.d.ts +179 -0
  13. package/dist/migrations/converters.js +575 -0
  14. package/dist/migrations/dbHelpers.d.ts +5 -0
  15. package/dist/migrations/dbHelpers.js +54 -0
  16. package/dist/migrations/importController.d.ts +44 -0
  17. package/dist/migrations/importController.js +312 -0
  18. package/dist/migrations/importDataActions.d.ts +44 -0
  19. package/dist/migrations/importDataActions.js +219 -0
  20. package/dist/migrations/indexes.d.ts +4 -0
  21. package/dist/migrations/indexes.js +18 -0
  22. package/dist/migrations/logging.d.ts +2 -0
  23. package/dist/migrations/logging.js +14 -0
  24. package/dist/migrations/migrationHelper.d.ts +18 -0
  25. package/dist/migrations/migrationHelper.js +66 -0
  26. package/dist/migrations/queue.d.ts +13 -0
  27. package/dist/migrations/queue.js +79 -0
  28. package/dist/migrations/relationships.d.ts +90 -0
  29. package/dist/migrations/relationships.js +209 -0
  30. package/dist/migrations/schema.d.ts +3142 -0
  31. package/dist/migrations/schema.js +485 -0
  32. package/dist/migrations/schemaStrings.d.ts +12 -0
  33. package/dist/migrations/schemaStrings.js +261 -0
  34. package/dist/migrations/setupDatabase.d.ts +7 -0
  35. package/dist/migrations/setupDatabase.js +151 -0
  36. package/dist/migrations/storage.d.ts +8 -0
  37. package/dist/migrations/storage.js +241 -0
  38. package/dist/migrations/users.d.ts +11 -0
  39. package/dist/migrations/users.js +114 -0
  40. package/dist/migrations/validationRules.d.ts +43 -0
  41. package/dist/migrations/validationRules.js +42 -0
  42. package/dist/schemas/authUser.d.ts +62 -0
  43. package/dist/schemas/authUser.js +17 -0
  44. package/dist/setup.d.ts +2 -0
  45. package/dist/setup.js +5 -0
  46. package/dist/types.d.ts +9 -0
  47. package/dist/types.js +5 -0
  48. package/dist/utils/configSchema.json +742 -0
  49. package/dist/utils/helperFunctions.d.ts +34 -0
  50. package/dist/utils/helperFunctions.js +72 -0
  51. package/dist/utils/index.d.ts +2 -0
  52. package/dist/utils/index.js +2 -0
  53. package/dist/utils/setupFiles.d.ts +2 -0
  54. package/dist/utils/setupFiles.js +276 -0
  55. package/dist/utilsController.d.ts +30 -0
  56. package/dist/utilsController.js +106 -0
  57. package/package.json +34 -0
  58. package/src/main.ts +77 -0
  59. package/src/migrations/afterImportActions.ts +300 -0
  60. package/src/migrations/attributes.ts +315 -0
  61. package/src/migrations/backup.ts +189 -0
  62. package/src/migrations/collections.ts +303 -0
  63. package/src/migrations/converters.ts +628 -0
  64. package/src/migrations/dbHelpers.ts +89 -0
  65. package/src/migrations/importController.ts +509 -0
  66. package/src/migrations/importDataActions.ts +313 -0
  67. package/src/migrations/indexes.ts +37 -0
  68. package/src/migrations/logging.ts +15 -0
  69. package/src/migrations/migrationHelper.ts +100 -0
  70. package/src/migrations/queue.ts +119 -0
  71. package/src/migrations/relationships.ts +336 -0
  72. package/src/migrations/schema.ts +590 -0
  73. package/src/migrations/schemaStrings.ts +310 -0
  74. package/src/migrations/setupDatabase.ts +219 -0
  75. package/src/migrations/storage.ts +351 -0
  76. package/src/migrations/users.ts +148 -0
  77. package/src/migrations/validationRules.ts +63 -0
  78. package/src/schemas/authUser.ts +23 -0
  79. package/src/setup.ts +8 -0
  80. package/src/types.ts +14 -0
  81. package/src/utils/configSchema.json +742 -0
  82. package/src/utils/helperFunctions.ts +111 -0
  83. package/src/utils/index.ts +2 -0
  84. package/src/utils/setupFiles.ts +295 -0
  85. package/src/utilsController.ts +173 -0
  86. package/tsconfig.json +37 -0
@@ -0,0 +1,312 @@
1
+ import { ID, Query, } from "node-appwrite";
2
+ import { checkForCollection } from "./collections.js";
3
+ import path from "path";
4
+ import fs from "fs";
5
+ import { convertObjectByAttributeMappings } from "./converters.js";
6
+ import _ from "lodash";
7
+ import { documentExists } from "./collections.js";
8
+ import { areCollectionNamesSame } from "../utils/index.js";
9
+ import { resolveAndUpdateRelationships } from "./relationships.js";
10
+ import { AuthUserCreateSchema } from "../types.js";
11
+ import { UsersController } from "./users.js";
12
+ export class ImportController {
13
+ config;
14
+ database;
15
+ storage;
16
+ appwriteFolderPath;
17
+ importDataActions;
18
+ setupOptions;
19
+ documentCache;
20
+ batchLimit = 25; // Define batch size limit
21
+ postImportActionsQueue = [];
22
+ constructor(config, database, storage, appwriteFolderPath, importDataActions, setupOptions) {
23
+ this.config = config;
24
+ this.database = database;
25
+ this.storage = storage;
26
+ this.appwriteFolderPath = appwriteFolderPath;
27
+ this.importDataActions = importDataActions;
28
+ this.setupOptions = setupOptions;
29
+ this.documentCache = new Map();
30
+ }
31
+ async run() {
32
+ const databasesToRun = this.config.databases
33
+ .filter((db) => (areCollectionNamesSame(db.name, this.config.databases[0].name) &&
34
+ this.setupOptions.runProd) ||
35
+ (areCollectionNamesSame(db.name, this.config.databases[1].name) &&
36
+ this.setupOptions.runStaging) ||
37
+ (areCollectionNamesSame(db.name, this.config.databases[2].name) &&
38
+ this.setupOptions.runDev))
39
+ .map((db) => db.name);
40
+ for (let db of this.config.databases) {
41
+ if (db.name.toLowerCase().trim().replace(" ", "") === "migrations" ||
42
+ !databasesToRun.includes(db.name)) {
43
+ continue;
44
+ }
45
+ if (!db.$id) {
46
+ const databases = await this.database.list([
47
+ Query.equal("name", db.name),
48
+ ]);
49
+ if (databases.databases.length > 0) {
50
+ db.$id = databases.databases[0].$id;
51
+ }
52
+ }
53
+ console.log(`---------------------------------`);
54
+ console.log(`Starting import data for database: ${db.name}`);
55
+ console.log(`---------------------------------`);
56
+ await this.importCollections(db);
57
+ await resolveAndUpdateRelationships(db.$id, this.database, this.config);
58
+ await this.executePostImportActions();
59
+ console.log(`---------------------------------`);
60
+ console.log(`Finished import data for database: ${db.name}`);
61
+ console.log(`---------------------------------`);
62
+ }
63
+ }
64
+ async importCollections(db) {
65
+ const maxParallel = 3; // Maximum number of collections to process in parallel
66
+ let activePromises = []; // Array to keep track of active promises
67
+ for (const collection of this.config.collections) {
68
+ // Function that returns a promise for processing a single collection
69
+ const processCollection = async (col) => {
70
+ let isMembersCollection = false;
71
+ if (this.config.usersCollectionName.toLowerCase().replace(" ", "") ===
72
+ col.name.toLowerCase().replace(" ", "")) {
73
+ isMembersCollection = true;
74
+ }
75
+ const collectionExists = await checkForCollection(this.database, db.$id, col);
76
+ if (!collectionExists) {
77
+ console.warn(`No collection found for ${col.name}`);
78
+ return; // Skip this iteration
79
+ }
80
+ const updatedCollection = { ...col, $id: collectionExists.$id };
81
+ await this.processImportDefinitions(db, updatedCollection, isMembersCollection);
82
+ };
83
+ // Add the current collection's processing promise to the activePromises array
84
+ activePromises.push(processCollection(collection));
85
+ // If the number of active promises reaches the limit, wait for one to finish
86
+ if (activePromises.length >= maxParallel) {
87
+ await Promise.race(activePromises).then(() => {
88
+ // Remove the promise that finished from the activePromises array
89
+ activePromises = activePromises.filter((p) => p !== Promise.race(activePromises));
90
+ });
91
+ }
92
+ }
93
+ // After the loop, wait for the remaining promises to finish
94
+ await Promise.all(activePromises);
95
+ }
96
+ async processImportDefinitions(db, collection, isMembersCollection = false) {
97
+ this.documentCache.clear();
98
+ const updateDefs = collection.importDefs.filter((def) => def.type === "update");
99
+ const createDefs = collection.importDefs.filter((def) => def.type === "create" || !def.type);
100
+ // Process create import definitions first
101
+ for (const importDef of createDefs) {
102
+ const dataToImport = await this.loadData(importDef);
103
+ if (!dataToImport)
104
+ continue;
105
+ console.log(`Processing create definitions for collection ID: ${collection.$id}`);
106
+ await this.processBatch(db, collection, importDef, dataToImport, updateDefs, isMembersCollection);
107
+ }
108
+ // Process update import definitions
109
+ for (const importDef of updateDefs) {
110
+ const dataToImport = await this.loadData(importDef);
111
+ if (!dataToImport)
112
+ continue;
113
+ console.log(`Processing update definitions for collection ID: ${collection.$id}`);
114
+ await this.processBatch(db, collection, importDef, dataToImport);
115
+ }
116
+ }
117
+ async loadData(importDef) {
118
+ const filePath = path.resolve(this.appwriteFolderPath, importDef.filePath);
119
+ if (!fs.existsSync(filePath)) {
120
+ console.error(`File not found: ${filePath}`);
121
+ return [];
122
+ }
123
+ const rawData = fs.readFileSync(filePath, "utf8");
124
+ return importDef.basePath
125
+ ? JSON.parse(rawData)[importDef.basePath]
126
+ : JSON.parse(rawData);
127
+ }
128
+ createContext(db, collection, item) {
129
+ return {
130
+ ...item, // Spread the item data for easy access to its properties
131
+ dbId: db.$id,
132
+ dbName: db.name,
133
+ collId: collection.$id,
134
+ collName: collection.name,
135
+ docId: "", // Initially empty, will be filled once the document is created or identified
136
+ createdDoc: {}, // Initially null, to be updated when the document is created
137
+ };
138
+ }
139
+ async transformData(item, attributeMappings) {
140
+ const convertedItem = convertObjectByAttributeMappings(item, attributeMappings);
141
+ return this.importDataActions.runConverterFunctions(convertedItem, attributeMappings);
142
+ }
143
+ async processBatch(db, collection, importDef, dataToImport, updateDefs = [], isMembersCollection = false) {
144
+ for (let i = 0; i < dataToImport.length; i += this.batchLimit) {
145
+ const batch = dataToImport.slice(i, i + this.batchLimit);
146
+ const results = await Promise.allSettled(batch.map(async (item) => {
147
+ let context = this.createContext(db, collection, item);
148
+ let finalItem = await this.transformData(item, importDef.attributeMappings);
149
+ let createIdToUse = undefined;
150
+ let associatedDoc;
151
+ if (isMembersCollection &&
152
+ (finalItem.hasOwnProperty("email") || item.hasOwnProperty("phone"))) {
153
+ console.log("Found members collection, creating user...");
154
+ const usersController = new UsersController(this.config, this.database);
155
+ const userToCreate = AuthUserCreateSchema.safeParse({
156
+ ...finalItem,
157
+ });
158
+ if (!userToCreate.success) {
159
+ console.error(userToCreate.error);
160
+ return;
161
+ }
162
+ const user = await usersController.createUserAndReturn(userToCreate.data);
163
+ createIdToUse = user.$id;
164
+ context = { ...context, ...user };
165
+ console.log("Created user, deleting keys in finalItem that exist in user...");
166
+ const associatedDocFound = await this.database.listDocuments(db.$id, context.collId, [Query.equal("$id", createIdToUse)]);
167
+ if (associatedDocFound.documents.length > 0) {
168
+ associatedDoc = associatedDocFound.documents[0];
169
+ }
170
+ // Delete keys in finalItem that also exist in user
171
+ let deletedKeys = [];
172
+ Object.keys(finalItem).forEach((key) => {
173
+ if (user.hasOwnProperty(key)) {
174
+ delete finalItem[key];
175
+ deletedKeys.push(key);
176
+ }
177
+ });
178
+ console.log(`Set createIdToUse to ${createIdToUse}. Deleted keys: ${deletedKeys.join(", ")}.`);
179
+ }
180
+ else if (isMembersCollection) {
181
+ console.log(`Skipping user & contact creation for ${item} due to lack of email...`);
182
+ }
183
+ context = { ...context, ...finalItem };
184
+ if (!(await this.importDataActions.validateItem(finalItem, importDef.attributeMappings, context))) {
185
+ console.error("Validation failed for item:", finalItem);
186
+ return;
187
+ }
188
+ let afterContext;
189
+ if ((importDef.type === "create" || !importDef.type) &&
190
+ !associatedDoc) {
191
+ const createdContext = await this.handleCreate(context, finalItem, updateDefs, createIdToUse);
192
+ if (createdContext) {
193
+ afterContext = createdContext;
194
+ }
195
+ }
196
+ else {
197
+ const updatedContext = await this.handleUpdate(context, finalItem, importDef);
198
+ if (updatedContext) {
199
+ afterContext = updatedContext;
200
+ }
201
+ }
202
+ if (afterContext) {
203
+ context = { ...context, ...afterContext };
204
+ }
205
+ const afterImportActionContext = structuredClone(context);
206
+ const attributeMappingsWithActions = this.getAttributeMappingsWithActions(importDef.attributeMappings, context, finalItem);
207
+ if (attributeMappingsWithActions.some((m) => m.postImportActions)) {
208
+ this.postImportActionsQueue.push({
209
+ context: afterImportActionContext,
210
+ finalItem: finalItem,
211
+ attributeMappings: attributeMappingsWithActions,
212
+ });
213
+ }
214
+ }));
215
+ results.forEach((result) => {
216
+ if (result.status === "rejected") {
217
+ console.error("A process batch promise was rejected:", result.reason);
218
+ }
219
+ });
220
+ }
221
+ }
222
+ async handleCreate(context, finalItem, updateDefs, id) {
223
+ const existing = await documentExists(this.database, context.dbId, context.collId, finalItem);
224
+ if (!existing) {
225
+ if (id) {
226
+ console.log(`Creating document with provided ID (member): ${id}`);
227
+ }
228
+ const createdDoc = await this.database.createDocument(context.dbId, context.collId, id || ID.unique(), finalItem);
229
+ context.docId = createdDoc.$id;
230
+ context.createdDoc = createdDoc;
231
+ context = { ...context, ...createdDoc };
232
+ // Populate document cache for updates
233
+ if (updateDefs) {
234
+ updateDefs.forEach((def) => {
235
+ if (def.updateMapping) {
236
+ this.documentCache.set(`${finalItem[def.updateMapping.targetField]}`, context);
237
+ }
238
+ });
239
+ }
240
+ console.log(`Created document ID: ${createdDoc.$id}`);
241
+ return context;
242
+ }
243
+ else {
244
+ console.log("Document already exists, skipping creation.");
245
+ return existing;
246
+ }
247
+ }
248
+ async handleUpdate(context, finalItem, importDef) {
249
+ const updateMapping = importDef.updateMapping;
250
+ if (updateMapping) {
251
+ const keyToMatch = updateMapping.originalIdField;
252
+ const origId = context[keyToMatch];
253
+ const targetId = finalItem[updateMapping.targetField];
254
+ const cachedContext = this.documentCache.get(`${origId}`);
255
+ context = { ...context, ...cachedContext };
256
+ if (cachedContext) {
257
+ const updatedDoc = await this.database.updateDocument(context.dbId, context.collId, context.docId, finalItem);
258
+ context = { ...context, ...updatedDoc };
259
+ console.log(`Updated document ID: ${updatedDoc.$id}`);
260
+ return context;
261
+ }
262
+ else {
263
+ console.error(`Document to update not found in cache targeting ${keyToMatch}:${origId}`);
264
+ return;
265
+ }
266
+ }
267
+ }
268
+ getAttributeMappingsWithActions(attributeMappings, context, item) {
269
+ return attributeMappings.map((mapping) => {
270
+ if (mapping.fileData) {
271
+ console.log("Adding after-import action for fileData attribute");
272
+ let mappingFilePath = this.importDataActions.resolveTemplate(mapping.fileData.path, context, item);
273
+ if (!mappingFilePath.toLowerCase().startsWith("http")) {
274
+ console.log(`Resolving file path: ${mappingFilePath}`);
275
+ mappingFilePath = path.resolve(this.appwriteFolderPath, mappingFilePath);
276
+ }
277
+ const afterImportAction = {
278
+ action: "createFileAndUpdateField",
279
+ params: [
280
+ "{dbId}",
281
+ "{collId}",
282
+ "{docId}",
283
+ mapping.targetKey,
284
+ `${this.config.documentBucketId}_${context.dbName
285
+ .toLowerCase()
286
+ .replace(" ", "")}`, // Assuming 'images' is your bucket ID
287
+ mappingFilePath,
288
+ mapping.fileData.name,
289
+ ],
290
+ };
291
+ const postImportActions = mapping.postImportActions
292
+ ? [...mapping.postImportActions, afterImportAction]
293
+ : [afterImportAction];
294
+ return { ...mapping, postImportActions };
295
+ }
296
+ return mapping;
297
+ });
298
+ }
299
+ async executePostImportActions() {
300
+ const results = await Promise.allSettled(this.postImportActionsQueue.map(async (action) => {
301
+ const { context, finalItem, attributeMappings } = action;
302
+ console.log(`Executing post-import actions for document: ${context.docId}`);
303
+ return this.importDataActions.executeAfterImportActions(finalItem, attributeMappings, context);
304
+ }));
305
+ results.forEach((result) => {
306
+ if (result.status === "rejected") {
307
+ console.error("A post-import action promise was rejected:", result.reason);
308
+ }
309
+ });
310
+ this.postImportActionsQueue = [];
311
+ }
312
+ }
@@ -0,0 +1,44 @@
1
+ import { type Databases, type Storage } from "node-appwrite";
2
+ import type { AppwriteConfig } from "./schema.js";
3
+ import { type ValidationRules } from "./validationRules.js";
4
+ import { type ConverterFunctions } from "./converters.js";
5
+ import { type AfterImportActions } from "./afterImportActions.js";
6
+ type AttributeMappings = AppwriteConfig["collections"][number]["importDefs"][number]["attributeMappings"];
7
+ export declare class ImportDataActions {
8
+ private db;
9
+ private storage;
10
+ private config;
11
+ private converterDefinitions;
12
+ private validityRuleDefinitions;
13
+ private afterImportActionsDefinitions;
14
+ constructor(db: Databases, storage: Storage, config: AppwriteConfig, converterDefinitions: ConverterFunctions, validityRuleDefinitions: ValidationRules, afterImportActionsDefinitions: AfterImportActions);
15
+ runConverterFunctions(item: any, attributeMappings: AttributeMappings): Promise<any>;
16
+ /**
17
+ * Validates a single data item based on defined validation rules.
18
+ * @param item The data item to validate.
19
+ * @param context The context for resolving templated parameters in validation rules.
20
+ * @returns A promise that resolves to true if the item is valid, false otherwise.
21
+ */
22
+ validateItem(item: any, attributeMap: AttributeMappings, context: {
23
+ [key: string]: any;
24
+ }): Promise<boolean>;
25
+ executeAfterImportActions(item: any, attributeMap: AttributeMappings, context: {
26
+ [key: string]: any;
27
+ }): Promise<void>;
28
+ executeAction(actionName: string, params: any[], // Accepts any type, including objects
29
+ context: {
30
+ [key: string]: any;
31
+ }, item: any): Promise<void>;
32
+ /**
33
+ * Resolves a templated string or object using the provided context and current data item.
34
+ * If the template is a string that starts and ends with "{}", it replaces it with the corresponding value from item or context.
35
+ * If the template is an object, it recursively resolves its properties.
36
+ * @param template The templated string or object.
37
+ * @param context The context for resolving the template.
38
+ * @param item The current data item being processed.
39
+ */
40
+ resolveTemplate(template: any, context: {
41
+ [key: string]: any;
42
+ }, item: any): any;
43
+ }
44
+ export {};
@@ -0,0 +1,219 @@
1
+ import { ID, InputFile, Query, } from "node-appwrite";
2
+ import { validationRules } from "./validationRules.js";
3
+ import { converterFunctions, convertObjectBySchema, } from "./converters.js";
4
+ import { afterImportActions, } from "./afterImportActions.js";
5
+ import { logger } from "./logging.js";
6
+ export class ImportDataActions {
7
+ db;
8
+ storage;
9
+ config;
10
+ converterDefinitions;
11
+ validityRuleDefinitions;
12
+ afterImportActionsDefinitions;
13
+ constructor(db, storage, config, converterDefinitions, validityRuleDefinitions, afterImportActionsDefinitions) {
14
+ this.db = db;
15
+ this.storage = storage;
16
+ this.config = config;
17
+ this.converterDefinitions = converterDefinitions;
18
+ this.validityRuleDefinitions = validityRuleDefinitions;
19
+ this.afterImportActionsDefinitions = afterImportActionsDefinitions;
20
+ }
21
+ async runConverterFunctions(item, attributeMappings) {
22
+ const conversionSchema = attributeMappings.reduce((schema, mapping) => {
23
+ schema[mapping.targetKey] = (originalValue) => {
24
+ return mapping.converters.reduce((value, converterName) => {
25
+ let shouldProcessAsArray = false;
26
+ if ((converterName.includes("[Arr]") ||
27
+ converterName.includes("[arr]")) &&
28
+ Array.isArray(value)) {
29
+ shouldProcessAsArray = true;
30
+ converterName = converterName
31
+ .replace("[Arr]", "")
32
+ .replace("[arr]", "");
33
+ }
34
+ else if ((!Array.isArray(value) && converterName.includes("[Arr]")) ||
35
+ converterName.includes("[arr]")) {
36
+ converterName = converterName
37
+ .replace("[Arr]", "")
38
+ .replace("[arr]", "");
39
+ }
40
+ const converterFunction = converterFunctions[converterName];
41
+ if (converterFunction) {
42
+ if (Array.isArray(value) && !shouldProcessAsArray) {
43
+ return value.map((item) => converterFunction(item));
44
+ }
45
+ else {
46
+ return converterFunction(value);
47
+ }
48
+ }
49
+ else {
50
+ logger.warn(`Converter function '${converterName}' is not defined.`);
51
+ return value;
52
+ }
53
+ }, originalValue);
54
+ };
55
+ return schema;
56
+ }, {});
57
+ // Convert the item using the constructed schema
58
+ const convertedItem = convertObjectBySchema(item, conversionSchema);
59
+ // Merge the converted item back into the original item object
60
+ Object.assign(item, convertedItem);
61
+ console.log("Converted item:", item);
62
+ return item;
63
+ }
64
+ /**
65
+ * Validates a single data item based on defined validation rules.
66
+ * @param item The data item to validate.
67
+ * @param context The context for resolving templated parameters in validation rules.
68
+ * @returns A promise that resolves to true if the item is valid, false otherwise.
69
+ */
70
+ async validateItem(item, attributeMap, context) {
71
+ for (const mapping of attributeMap) {
72
+ const { validationActions } = mapping;
73
+ if (!validationActions ||
74
+ !Array.isArray(validationActions) ||
75
+ !validationActions.length) {
76
+ console.warn("No validation actions defined for the item, assuming true");
77
+ return true; // Assume items without validation actions as valid.
78
+ }
79
+ for (const ruleDef of validationActions) {
80
+ const { action, params } = ruleDef;
81
+ const validationRule = validationRules[action];
82
+ if (!validationRule) {
83
+ logger.warn(`Validation rule '${action}' is not defined.`);
84
+ continue; // Optionally, consider undefined rules as a validation failure.
85
+ }
86
+ // Resolve templated parameters
87
+ const resolvedParams = params.map((param) => this.resolveTemplate(param, context, item));
88
+ // Apply the validation rule
89
+ let isValid = false;
90
+ if (Array.isArray(item)) {
91
+ isValid = item.every((item) => validationRule(item, ...resolvedParams));
92
+ }
93
+ else {
94
+ isValid = validationRule(item, ...resolvedParams);
95
+ }
96
+ if (!isValid) {
97
+ logger.error(`Validation failed for rule '${action}' with params ${params.join(", ")}`);
98
+ return false; // Stop validation on first failure
99
+ }
100
+ }
101
+ }
102
+ return true; // The item passed all validations
103
+ }
104
+ async executeAfterImportActions(item, attributeMap, context) {
105
+ for (const mapping of attributeMap) {
106
+ const { postImportActions } = mapping;
107
+ if (!postImportActions || !Array.isArray(postImportActions)) {
108
+ console.warn(`No post-import actions defined for attribute: ${mapping.targetKey}`, postImportActions);
109
+ continue; // Skip to the next attribute if no actions are defined
110
+ }
111
+ for (const actionDef of postImportActions) {
112
+ const { action, params } = actionDef;
113
+ console.log(`Executing post-import action '${action}' for attribute '${mapping.targetKey}' with params ${params.join(", ")}...`);
114
+ logger.info(`Executing post-import action '${action}' for attribute '${mapping.targetKey}' with params ${params.join(", ")}...`);
115
+ try {
116
+ await this.executeAction(action, params, context, item);
117
+ }
118
+ catch (error) {
119
+ logger.error(`Failed to execute post-import action '${action}' for attribute '${mapping.targetKey}':`, error);
120
+ }
121
+ }
122
+ }
123
+ }
124
+ async executeAction(actionName, params, // Accepts any type, including objects
125
+ context, item) {
126
+ const actionMethod = afterImportActions[actionName];
127
+ if (typeof actionMethod === "function") {
128
+ try {
129
+ // Resolve parameters, handling both strings and objects
130
+ const resolvedParams = params.map((param) => {
131
+ // Directly resolve each param, whether it's an object or a string
132
+ return this.resolveTemplate(param, context, item);
133
+ });
134
+ // Execute the action with resolved parameters
135
+ // Parameters are passed as-is, with objects treated as single parameters
136
+ logger.info(`Executing action '${actionName}' from context: ${JSON.stringify(context, null, 2)} with params:`, resolvedParams);
137
+ await actionMethod(this.config, ...resolvedParams);
138
+ }
139
+ catch (error) {
140
+ logger.error(`Error executing action '${actionName}':`, error);
141
+ throw new Error(`Execution failed for action '${actionName}': ${error.message}`);
142
+ }
143
+ }
144
+ else {
145
+ logger.warn(`Action '${actionName}' is not defined.`);
146
+ throw new Error(`Action '${actionName}' is not defined.`);
147
+ }
148
+ }
149
+ /**
150
+ * Resolves a templated string or object using the provided context and current data item.
151
+ * If the template is a string that starts and ends with "{}", it replaces it with the corresponding value from item or context.
152
+ * If the template is an object, it recursively resolves its properties.
153
+ * @param template The templated string or object.
154
+ * @param context The context for resolving the template.
155
+ * @param item The current data item being processed.
156
+ */
157
+ resolveTemplate(template, context, item) {
158
+ // Function to recursively resolve paths, including handling [any] notation
159
+ const resolvePath = (path, currentContext) => {
160
+ const anyKeyRegex = /\[any\]/g;
161
+ let pathParts = path.split(".").filter(Boolean);
162
+ return pathParts.reduce((acc, part, index) => {
163
+ // Handle [any] part by iterating over all elements if it's an object or an array
164
+ if (part === "[any]") {
165
+ if (Array.isArray(acc)) {
166
+ return acc
167
+ .map((item) => item[pathParts[index + 1]])
168
+ .filter((item) => item !== undefined);
169
+ }
170
+ else if (typeof acc === "object") {
171
+ return Object.values(acc)
172
+ .map((item) => item[pathParts[index + 1]])
173
+ .filter((item) => item !== undefined);
174
+ }
175
+ }
176
+ else {
177
+ return acc?.[part];
178
+ }
179
+ }, currentContext);
180
+ };
181
+ if (typeof template === "string") {
182
+ // Matches placeholders in the template
183
+ const regex = /\{([^}]+)\}/g;
184
+ let match;
185
+ let resolvedString = template;
186
+ while ((match = regex.exec(template)) !== null) {
187
+ const path = match[1];
188
+ // Resolve the path, handling [any] notation and arrays/objects
189
+ const resolvedValue = resolvePath(path, { ...context, ...item });
190
+ if (resolvedValue !== undefined) {
191
+ // If it's an array (from [any] notation), join the values; adjust as needed
192
+ const value = Array.isArray(resolvedValue)
193
+ ? resolvedValue.join(", ")
194
+ : resolvedValue;
195
+ resolvedString = resolvedString.replace(match[0], value);
196
+ }
197
+ else {
198
+ logger.warn(`Failed to resolve ${template} in context: `, context);
199
+ }
200
+ }
201
+ // console.log(`Resolved string: ${resolvedString}`);
202
+ return resolvedString;
203
+ }
204
+ else if (typeof template === "object" && template !== null) {
205
+ // Recursively resolve templates for each property in the object
206
+ const resolvedObject = Array.isArray(template) ? [] : {};
207
+ for (const key in template) {
208
+ const resolvedValue = this.resolveTemplate(template[key], context, item);
209
+ if (resolvedValue !== undefined) {
210
+ // Only assign if resolvedValue is not undefined
211
+ resolvedObject[key] = resolvedValue;
212
+ }
213
+ }
214
+ return resolvedObject;
215
+ }
216
+ // console.log(`Template is not a string or object: ${template}`);
217
+ return template;
218
+ }
219
+ }
@@ -0,0 +1,4 @@
1
+ import { type Index } from "./schema.js";
2
+ import { Databases, type Models } from "node-appwrite";
3
+ export declare const createOrUpdateIndex: (dbId: string, db: Databases, collectionId: string, index: Index) => Promise<Models.Index>;
4
+ export declare const createOrUpdateIndexes: (dbId: string, db: Databases, collectionId: string, indexes: Index[]) => Promise<void>;
@@ -0,0 +1,18 @@
1
+ import { indexSchema } from "./schema.js";
2
+ import { Databases, Query } from "node-appwrite";
3
+ // import {}
4
+ export const createOrUpdateIndex = async (dbId, db, collectionId, index) => {
5
+ const existingIndex = await db.listIndexes(dbId, collectionId, [
6
+ Query.equal("key", index.key),
7
+ ]);
8
+ if (existingIndex.total > 0) {
9
+ await db.deleteIndex(dbId, collectionId, existingIndex.indexes[0].key);
10
+ }
11
+ const newIndex = await db.createIndex(dbId, collectionId, index.key, index.type, index.attributes, index.orders);
12
+ return newIndex;
13
+ };
14
+ export const createOrUpdateIndexes = async (dbId, db, collectionId, indexes) => {
15
+ for (const index of indexes) {
16
+ await createOrUpdateIndex(dbId, db, collectionId, index);
17
+ }
18
+ };
@@ -0,0 +1,2 @@
1
+ import winston from "winston";
2
+ export declare const logger: winston.Logger;
@@ -0,0 +1,14 @@
1
+ import winston from "winston";
2
+ export const logger = winston.createLogger({
3
+ level: "info",
4
+ format: winston.format.prettyPrint(),
5
+ defaultMeta: { service: "appwrite-utils-cli" },
6
+ transports: [
7
+ //
8
+ // - Write all logs with importance level of `error` or less to `error.log`
9
+ // - Write all logs with importance level of `info` or less to `combined.log`
10
+ //
11
+ new winston.transports.File({ filename: "error.log", level: "error" }),
12
+ new winston.transports.File({ filename: "combined.log" }),
13
+ ],
14
+ });
@@ -0,0 +1,18 @@
1
+ import { type Databases } from "node-appwrite";
2
+ export declare const findOrCreateOperation: (database: Databases, collectionId: string, operationType: string) => Promise<{
3
+ error: string;
4
+ status: "error" | "pending" | "in_progress" | "completed";
5
+ $id: string;
6
+ $createdAt: string;
7
+ $updatedAt: string;
8
+ collectionId: string;
9
+ operationType: string;
10
+ progress: number;
11
+ total: number;
12
+ data?: any;
13
+ batches?: string[] | undefined;
14
+ }>;
15
+ export declare const updateOperation: (database: Databases, operationId: string, updateFields: any) => Promise<void>;
16
+ export declare const maxDataLength = 1073741820;
17
+ export declare const maxBatchItems = 100;
18
+ export declare const splitIntoBatches: (data: any[]) => any[][];