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,509 @@
1
+ import {
2
+ ID,
3
+ Query,
4
+ type Databases,
5
+ type Models,
6
+ type Storage,
7
+ } from "node-appwrite";
8
+ import type {
9
+ AppwriteConfig,
10
+ ConfigCollection,
11
+ ConfigDatabase,
12
+ ImportDef,
13
+ AttributeMappings,
14
+ } from "./schema.js";
15
+ import type { ImportDataActions } from "./importDataActions.js";
16
+ import { checkForCollection } from "./collections.js";
17
+ import path from "path";
18
+ import fs from "fs";
19
+ import { convertObjectByAttributeMappings } from "./converters.js";
20
+ import _ from "lodash";
21
+ import { documentExists } from "./collections.js";
22
+ import { areCollectionNamesSame } from "../utils/index.js";
23
+ import type { SetupOptions } from "../utilsController.js";
24
+ import { resolveAndUpdateRelationships } from "./relationships.js";
25
+ import { AuthUserCreateSchema } from "../types.js";
26
+ import { UsersController } from "./users.js";
27
+
28
+ export class ImportController {
29
+ private config: AppwriteConfig;
30
+ private database: Databases;
31
+ private storage: Storage;
32
+ private appwriteFolderPath: string;
33
+ private importDataActions: ImportDataActions;
34
+ private setupOptions: SetupOptions;
35
+ private documentCache: Map<string, any>;
36
+ private batchLimit: number = 25; // Define batch size limit
37
+ private postImportActionsQueue: {
38
+ context: any;
39
+ finalItem: any;
40
+ attributeMappings: AttributeMappings;
41
+ }[] = [];
42
+
43
+ constructor(
44
+ config: AppwriteConfig,
45
+ database: Databases,
46
+ storage: Storage,
47
+ appwriteFolderPath: string,
48
+ importDataActions: ImportDataActions,
49
+ setupOptions: SetupOptions
50
+ ) {
51
+ this.config = config;
52
+ this.database = database;
53
+ this.storage = storage;
54
+ this.appwriteFolderPath = appwriteFolderPath;
55
+ this.importDataActions = importDataActions;
56
+ this.setupOptions = setupOptions;
57
+ this.documentCache = new Map();
58
+ }
59
+
60
+ async run() {
61
+ const databasesToRun = this.config.databases
62
+ .filter(
63
+ (db) =>
64
+ (areCollectionNamesSame(db.name, this.config!.databases[0].name) &&
65
+ this.setupOptions.runProd) ||
66
+ (areCollectionNamesSame(db.name, this.config!.databases[1].name) &&
67
+ this.setupOptions.runStaging) ||
68
+ (areCollectionNamesSame(db.name, this.config!.databases[2].name) &&
69
+ this.setupOptions.runDev)
70
+ )
71
+ .map((db) => db.name);
72
+
73
+ for (let db of this.config.databases) {
74
+ if (
75
+ db.name.toLowerCase().trim().replace(" ", "") === "migrations" ||
76
+ !databasesToRun.includes(db.name)
77
+ ) {
78
+ continue;
79
+ }
80
+ if (!db.$id) {
81
+ const databases = await this.database!.list([
82
+ Query.equal("name", db.name),
83
+ ]);
84
+ if (databases.databases.length > 0) {
85
+ db.$id = databases.databases[0].$id;
86
+ }
87
+ }
88
+ console.log(`---------------------------------`);
89
+ console.log(`Starting import data for database: ${db.name}`);
90
+ console.log(`---------------------------------`);
91
+ await this.importCollections(db);
92
+ await resolveAndUpdateRelationships(db.$id, this.database!, this.config!);
93
+ await this.executePostImportActions();
94
+ console.log(`---------------------------------`);
95
+ console.log(`Finished import data for database: ${db.name}`);
96
+ console.log(`---------------------------------`);
97
+ }
98
+ }
99
+
100
+ async importCollections(db: ConfigDatabase) {
101
+ const maxParallel = 3; // Maximum number of collections to process in parallel
102
+ let activePromises: Promise<void>[] = []; // Array to keep track of active promises
103
+
104
+ for (const collection of this.config.collections) {
105
+ // Function that returns a promise for processing a single collection
106
+ const processCollection = async (col: ConfigCollection) => {
107
+ let isMembersCollection = false;
108
+ if (
109
+ this.config.usersCollectionName.toLowerCase().replace(" ", "") ===
110
+ col.name.toLowerCase().replace(" ", "")
111
+ ) {
112
+ isMembersCollection = true;
113
+ }
114
+ const collectionExists = await checkForCollection(
115
+ this.database,
116
+ db.$id,
117
+ col
118
+ );
119
+ if (!collectionExists) {
120
+ console.warn(`No collection found for ${col.name}`);
121
+ return; // Skip this iteration
122
+ }
123
+
124
+ const updatedCollection = { ...col, $id: collectionExists.$id };
125
+ await this.processImportDefinitions(
126
+ db,
127
+ updatedCollection,
128
+ isMembersCollection
129
+ );
130
+ };
131
+
132
+ // Add the current collection's processing promise to the activePromises array
133
+ activePromises.push(processCollection(collection));
134
+
135
+ // If the number of active promises reaches the limit, wait for one to finish
136
+ if (activePromises.length >= maxParallel) {
137
+ await Promise.race(activePromises).then(() => {
138
+ // Remove the promise that finished from the activePromises array
139
+ activePromises = activePromises.filter(
140
+ (p) => p !== Promise.race(activePromises)
141
+ );
142
+ });
143
+ }
144
+ }
145
+
146
+ // After the loop, wait for the remaining promises to finish
147
+ await Promise.all(activePromises);
148
+ }
149
+
150
+ async processImportDefinitions(
151
+ db: ConfigDatabase,
152
+ collection: ConfigCollection,
153
+ isMembersCollection: boolean = false
154
+ ) {
155
+ this.documentCache.clear();
156
+ const updateDefs: ImportDef[] = collection.importDefs.filter(
157
+ (def) => def.type === "update"
158
+ );
159
+ const createDefs: ImportDef[] = collection.importDefs.filter(
160
+ (def) => def.type === "create" || !def.type
161
+ );
162
+
163
+ // Process create import definitions first
164
+ for (const importDef of createDefs) {
165
+ const dataToImport = await this.loadData(importDef);
166
+ if (!dataToImport) continue;
167
+
168
+ console.log(
169
+ `Processing create definitions for collection ID: ${collection.$id}`
170
+ );
171
+ await this.processBatch(
172
+ db,
173
+ collection,
174
+ importDef,
175
+ dataToImport,
176
+ updateDefs,
177
+ isMembersCollection
178
+ );
179
+ }
180
+
181
+ // Process update import definitions
182
+ for (const importDef of updateDefs) {
183
+ const dataToImport = await this.loadData(importDef);
184
+ if (!dataToImport) continue;
185
+
186
+ console.log(
187
+ `Processing update definitions for collection ID: ${collection.$id}`
188
+ );
189
+ await this.processBatch(db, collection, importDef, dataToImport);
190
+ }
191
+ }
192
+
193
+ async loadData(importDef: ImportDef): Promise<any[]> {
194
+ const filePath = path.resolve(this.appwriteFolderPath, importDef.filePath);
195
+ if (!fs.existsSync(filePath)) {
196
+ console.error(`File not found: ${filePath}`);
197
+ return [];
198
+ }
199
+
200
+ const rawData = fs.readFileSync(filePath, "utf8");
201
+ return importDef.basePath
202
+ ? JSON.parse(rawData)[importDef.basePath]
203
+ : JSON.parse(rawData);
204
+ }
205
+
206
+ createContext(db: ConfigDatabase, collection: ConfigCollection, item: any) {
207
+ return {
208
+ ...item, // Spread the item data for easy access to its properties
209
+ dbId: db.$id,
210
+ dbName: db.name,
211
+ collId: collection.$id,
212
+ collName: collection.name,
213
+ docId: "", // Initially empty, will be filled once the document is created or identified
214
+ createdDoc: {}, // Initially null, to be updated when the document is created
215
+ };
216
+ }
217
+
218
+ async transformData(
219
+ item: any,
220
+ attributeMappings: AttributeMappings
221
+ ): Promise<any> {
222
+ const convertedItem = convertObjectByAttributeMappings(
223
+ item,
224
+ attributeMappings
225
+ );
226
+ return this.importDataActions.runConverterFunctions(
227
+ convertedItem,
228
+ attributeMappings
229
+ );
230
+ }
231
+
232
+ async processBatch(
233
+ db: ConfigDatabase,
234
+ collection: ConfigCollection,
235
+ importDef: ImportDef,
236
+ dataToImport: any[],
237
+ updateDefs: ImportDef[] = [],
238
+ isMembersCollection: boolean = false
239
+ ) {
240
+ for (let i = 0; i < dataToImport.length; i += this.batchLimit) {
241
+ const batch = dataToImport.slice(i, i + this.batchLimit);
242
+ const results = await Promise.allSettled(
243
+ batch.map(async (item: any) => {
244
+ let context = this.createContext(db, collection, item);
245
+ let finalItem = await this.transformData(
246
+ item,
247
+ importDef.attributeMappings
248
+ );
249
+ let createIdToUse: string | undefined = undefined;
250
+ let associatedDoc: Models.Document | undefined;
251
+ if (
252
+ isMembersCollection &&
253
+ (finalItem.hasOwnProperty("email") || item.hasOwnProperty("phone"))
254
+ ) {
255
+ console.log("Found members collection, creating user...");
256
+ const usersController = new UsersController(
257
+ this.config,
258
+ this.database
259
+ );
260
+ const userToCreate = AuthUserCreateSchema.safeParse({
261
+ ...finalItem,
262
+ });
263
+ if (!userToCreate.success) {
264
+ console.error(userToCreate.error);
265
+ return;
266
+ }
267
+ const user = await usersController.createUserAndReturn(
268
+ userToCreate.data
269
+ );
270
+ createIdToUse = user.$id;
271
+ context = { ...context, ...user };
272
+ console.log(
273
+ "Created user, deleting keys in finalItem that exist in user..."
274
+ );
275
+ const associatedDocFound = await this.database.listDocuments(
276
+ db.$id,
277
+ context.collId,
278
+ [Query.equal("$id", createIdToUse)]
279
+ );
280
+ if (associatedDocFound.documents.length > 0) {
281
+ associatedDoc = associatedDocFound.documents[0];
282
+ }
283
+ // Delete keys in finalItem that also exist in user
284
+ let deletedKeys: string[] = [];
285
+ Object.keys(finalItem).forEach((key) => {
286
+ if (user.hasOwnProperty(key)) {
287
+ delete finalItem[key];
288
+ deletedKeys.push(key);
289
+ }
290
+ });
291
+ console.log(
292
+ `Set createIdToUse to ${createIdToUse}. Deleted keys: ${deletedKeys.join(
293
+ ", "
294
+ )}.`
295
+ );
296
+ } else if (isMembersCollection) {
297
+ console.log(
298
+ `Skipping user & contact creation for ${item} due to lack of email...`
299
+ );
300
+ }
301
+
302
+ context = { ...context, ...finalItem };
303
+
304
+ if (
305
+ !(await this.importDataActions.validateItem(
306
+ finalItem,
307
+ importDef.attributeMappings,
308
+ context
309
+ ))
310
+ ) {
311
+ console.error("Validation failed for item:", finalItem);
312
+ return;
313
+ }
314
+
315
+ let afterContext;
316
+ if (
317
+ (importDef.type === "create" || !importDef.type) &&
318
+ !associatedDoc
319
+ ) {
320
+ const createdContext = await this.handleCreate(
321
+ context,
322
+ finalItem,
323
+ updateDefs,
324
+ createIdToUse
325
+ );
326
+ if (createdContext) {
327
+ afterContext = createdContext;
328
+ }
329
+ } else {
330
+ const updatedContext = await this.handleUpdate(
331
+ context,
332
+ finalItem,
333
+ importDef
334
+ );
335
+ if (updatedContext) {
336
+ afterContext = updatedContext;
337
+ }
338
+ }
339
+ if (afterContext) {
340
+ context = { ...context, ...afterContext };
341
+ }
342
+ const afterImportActionContext = structuredClone(context);
343
+ const attributeMappingsWithActions =
344
+ this.getAttributeMappingsWithActions(
345
+ importDef.attributeMappings,
346
+ context,
347
+ finalItem
348
+ );
349
+ if (attributeMappingsWithActions.some((m) => m.postImportActions)) {
350
+ this.postImportActionsQueue.push({
351
+ context: afterImportActionContext,
352
+ finalItem: finalItem,
353
+ attributeMappings: attributeMappingsWithActions,
354
+ });
355
+ }
356
+ })
357
+ );
358
+ results.forEach((result) => {
359
+ if (result.status === "rejected") {
360
+ console.error("A process batch promise was rejected:", result.reason);
361
+ }
362
+ });
363
+ }
364
+ }
365
+
366
+ async handleCreate(
367
+ context: any,
368
+ finalItem: any,
369
+ updateDefs?: ImportDef[],
370
+ id?: string
371
+ ) {
372
+ const existing = await documentExists(
373
+ this.database,
374
+ context.dbId,
375
+ context.collId,
376
+ finalItem
377
+ );
378
+ if (!existing) {
379
+ if (id) {
380
+ console.log(`Creating document with provided ID (member): ${id}`);
381
+ }
382
+ const createdDoc = await this.database.createDocument(
383
+ context.dbId,
384
+ context.collId,
385
+ id || ID.unique(),
386
+ finalItem
387
+ );
388
+ context.docId = createdDoc.$id;
389
+ context.createdDoc = createdDoc;
390
+ context = { ...context, ...createdDoc };
391
+
392
+ // Populate document cache for updates
393
+ if (updateDefs) {
394
+ updateDefs.forEach((def) => {
395
+ if (def.updateMapping) {
396
+ this.documentCache.set(
397
+ `${finalItem[def.updateMapping.targetField]}`,
398
+ context
399
+ );
400
+ }
401
+ });
402
+ }
403
+
404
+ console.log(`Created document ID: ${createdDoc.$id}`);
405
+ return context;
406
+ } else {
407
+ console.log("Document already exists, skipping creation.");
408
+ return existing;
409
+ }
410
+ }
411
+
412
+ async handleUpdate(context: any, finalItem: any, importDef: ImportDef) {
413
+ const updateMapping = importDef.updateMapping;
414
+ if (updateMapping) {
415
+ const keyToMatch = updateMapping.originalIdField;
416
+ const origId = context[keyToMatch];
417
+ const targetId = finalItem[updateMapping.targetField];
418
+ const cachedContext = this.documentCache.get(`${origId}`);
419
+ context = { ...context, ...cachedContext };
420
+
421
+ if (cachedContext) {
422
+ const updatedDoc = await this.database.updateDocument(
423
+ context.dbId,
424
+ context.collId,
425
+ context.docId,
426
+ finalItem
427
+ );
428
+ context = { ...context, ...updatedDoc };
429
+ console.log(`Updated document ID: ${updatedDoc.$id}`);
430
+ return context;
431
+ } else {
432
+ console.error(
433
+ `Document to update not found in cache targeting ${keyToMatch}:${origId}`
434
+ );
435
+ return;
436
+ }
437
+ }
438
+ }
439
+
440
+ getAttributeMappingsWithActions(
441
+ attributeMappings: AttributeMappings,
442
+ context: any,
443
+ item: any
444
+ ) {
445
+ return attributeMappings.map((mapping) => {
446
+ if (mapping.fileData) {
447
+ console.log("Adding after-import action for fileData attribute");
448
+ let mappingFilePath = this.importDataActions.resolveTemplate(
449
+ mapping.fileData.path,
450
+ context,
451
+ item
452
+ );
453
+ if (!mappingFilePath.toLowerCase().startsWith("http")) {
454
+ console.log(`Resolving file path: ${mappingFilePath}`);
455
+ mappingFilePath = path.resolve(
456
+ this.appwriteFolderPath,
457
+ mappingFilePath
458
+ );
459
+ }
460
+ const afterImportAction = {
461
+ action: "createFileAndUpdateField",
462
+ params: [
463
+ "{dbId}",
464
+ "{collId}",
465
+ "{docId}",
466
+ mapping.targetKey,
467
+ `${this.config!.documentBucketId}_${context.dbName
468
+ .toLowerCase()
469
+ .replace(" ", "")}`, // Assuming 'images' is your bucket ID
470
+ mappingFilePath,
471
+ mapping.fileData.name,
472
+ ],
473
+ };
474
+ const postImportActions = mapping.postImportActions
475
+ ? [...mapping.postImportActions, afterImportAction]
476
+ : [afterImportAction];
477
+ return { ...mapping, postImportActions };
478
+ }
479
+ return mapping;
480
+ });
481
+ }
482
+
483
+ async executePostImportActions() {
484
+ const results = await Promise.allSettled(
485
+ this.postImportActionsQueue.map(async (action) => {
486
+ const { context, finalItem, attributeMappings } = action;
487
+ console.log(
488
+ `Executing post-import actions for document: ${context.docId}`
489
+ );
490
+ return this.importDataActions.executeAfterImportActions(
491
+ finalItem,
492
+ attributeMappings,
493
+ context
494
+ );
495
+ })
496
+ );
497
+
498
+ results.forEach((result) => {
499
+ if (result.status === "rejected") {
500
+ console.error(
501
+ "A post-import action promise was rejected:",
502
+ result.reason
503
+ );
504
+ }
505
+ });
506
+
507
+ this.postImportActionsQueue = [];
508
+ }
509
+ }