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.
- package/README.md +1 -0
- package/dist/migrations/attributes.js +21 -20
- package/dist/migrations/collections.js +10 -13
- package/dist/migrations/converters.js +15 -1
- package/dist/migrations/dataLoader.d.ts +12 -21
- package/dist/migrations/dataLoader.js +168 -58
- package/dist/migrations/databases.js +2 -1
- package/dist/migrations/importController.js +6 -24
- package/dist/migrations/importDataActions.d.ts +9 -1
- package/dist/migrations/importDataActions.js +14 -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/converters.ts +17 -1
- package/src/migrations/dataLoader.ts +232 -63
- package/src/migrations/databases.ts +4 -1
- package/src/migrations/importController.ts +13 -27
- package/src/migrations/importDataActions.ts +17 -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
|
@@ -65,6 +65,7 @@ export class DataLoader {
|
|
|
65
65
|
* It iterates through the target object's keys and updates the source object if:
|
|
66
66
|
* - The source object has the key.
|
|
67
67
|
* - The target object's value for that key is not null, undefined, or an empty string.
|
|
68
|
+
* - If the target object has an array value, it concatenates the values and removes duplicates.
|
|
68
69
|
*
|
|
69
70
|
* @param source - The source object to be updated.
|
|
70
71
|
* @param target - The target object with values to update the source object.
|
|
@@ -73,29 +74,51 @@ export class DataLoader {
|
|
|
73
74
|
mergeObjects(source, update) {
|
|
74
75
|
// Create a new object to hold the merged result
|
|
75
76
|
const result = { ...source };
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
result[key] = [...new Set([...sourceArray, ...updateValue])];
|
|
83
|
-
}
|
|
84
|
-
// If the update value is an object, recursively merge
|
|
85
|
-
else if (updateValue !== null &&
|
|
86
|
-
typeof updateValue === "object" &&
|
|
87
|
-
!(updateValue instanceof Date)) {
|
|
88
|
-
result[key] = this.mergeObjects(sourceValue, updateValue);
|
|
89
|
-
}
|
|
90
|
-
// If the update value is not nullish, overwrite the source value
|
|
91
|
-
else if (updateValue !== null && updateValue !== undefined) {
|
|
92
|
-
result[key] = updateValue;
|
|
93
|
-
}
|
|
94
|
-
// If the update value is nullish, keep the original value unless it doesn't exist
|
|
95
|
-
else if (sourceValue === undefined || sourceValue === null) {
|
|
96
|
-
result[key] = updateValue;
|
|
77
|
+
// Loop through the keys of the object we care about
|
|
78
|
+
for (const [key, value] of Object.entries(source)) {
|
|
79
|
+
// Check if the key exists in the target object
|
|
80
|
+
if (!Object.hasOwn(update, key)) {
|
|
81
|
+
// If the key doesn't exist, we can just skip it like bad cheese
|
|
82
|
+
continue;
|
|
97
83
|
}
|
|
98
|
-
|
|
84
|
+
if (update[key] === value) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
// If the value ain't here, we can just do whatever man
|
|
88
|
+
if (value === undefined || value === null || value === "") {
|
|
89
|
+
// If the update key is defined
|
|
90
|
+
if (update[key] !== undefined &&
|
|
91
|
+
update[key] !== null &&
|
|
92
|
+
update[key] !== "") {
|
|
93
|
+
// might as well use it eh?
|
|
94
|
+
result[key] = update[key];
|
|
95
|
+
}
|
|
96
|
+
// ELSE if the value is an array, because it would then not be === to those things above
|
|
97
|
+
}
|
|
98
|
+
else if (Array.isArray(value)) {
|
|
99
|
+
// Get the update value
|
|
100
|
+
const updateValue = update[key];
|
|
101
|
+
// If the update value is an array, concatenate and remove duplicates
|
|
102
|
+
// and poopy data
|
|
103
|
+
if (Array.isArray(updateValue)) {
|
|
104
|
+
result[key] = [...new Set([...value, ...updateValue])].filter((item) => item !== null && item !== undefined && item !== "");
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// If the update value is not an array, just use it
|
|
108
|
+
result[key] = [...value, updateValue].filter((item) => item !== null && item !== undefined && item !== "");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else if (typeof value === "object") {
|
|
112
|
+
// If the value is an object, we need to merge it
|
|
113
|
+
if (typeof update[key] === "object") {
|
|
114
|
+
result[key] = this.mergeObjects(value, update[key]);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
// Finally, the source value is defined, and not an array, so we don't care about the update value
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
99
122
|
return result;
|
|
100
123
|
}
|
|
101
124
|
// Method to load data from a file specified in the import definition
|
|
@@ -125,8 +148,19 @@ export class DataLoader {
|
|
|
125
148
|
// Method to generate a unique ID that doesn't conflict with existing IDs
|
|
126
149
|
getTrueUniqueId(collectionName) {
|
|
127
150
|
let newId = ID.unique();
|
|
128
|
-
|
|
151
|
+
let condition = this.checkMapValuesForId(newId, collectionName) ||
|
|
152
|
+
this.userExistsMap.has(newId) ||
|
|
153
|
+
this.importMap
|
|
154
|
+
.get(this.getCollectionKey("users"))
|
|
155
|
+
?.data.some((user) => user.finalData.docId === newId || user.finalData.userId === newId);
|
|
156
|
+
while (condition) {
|
|
129
157
|
newId = ID.unique();
|
|
158
|
+
condition =
|
|
159
|
+
this.checkMapValuesForId(newId, collectionName) ||
|
|
160
|
+
this.userExistsMap.has(newId) ||
|
|
161
|
+
this.importMap
|
|
162
|
+
.get(this.getCollectionKey("users"))
|
|
163
|
+
?.data.some((user) => user.finalData.docId === newId || user.finalData.userId === newId);
|
|
130
164
|
}
|
|
131
165
|
return newId;
|
|
132
166
|
}
|
|
@@ -153,6 +187,9 @@ export class DataLoader {
|
|
|
153
187
|
transformData(item, attributeMappings) {
|
|
154
188
|
// Convert the item using the attribute mappings provided
|
|
155
189
|
const convertedItem = convertObjectByAttributeMappings(item, attributeMappings);
|
|
190
|
+
if (item["region"]) {
|
|
191
|
+
logger.info(`Converted item: ${JSON.stringify(convertedItem, null, 2)}`);
|
|
192
|
+
}
|
|
156
193
|
// Run additional converter functions on the converted item, if any
|
|
157
194
|
return this.importDataActions.runConverterFunctions(convertedItem, attributeMappings);
|
|
158
195
|
}
|
|
@@ -209,6 +246,21 @@ export class DataLoader {
|
|
|
209
246
|
this.phoneToUserIdMap.set(user.phone, user.$id);
|
|
210
247
|
}
|
|
211
248
|
this.userExistsMap.set(user.$id, true);
|
|
249
|
+
let importData = this.importMap.get(this.getCollectionKey("users"));
|
|
250
|
+
if (!importData) {
|
|
251
|
+
importData = {
|
|
252
|
+
data: [],
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
importData.data.push({
|
|
256
|
+
finalData: {
|
|
257
|
+
...user,
|
|
258
|
+
userId: user.$id,
|
|
259
|
+
docId: user.$id,
|
|
260
|
+
},
|
|
261
|
+
rawData: user,
|
|
262
|
+
});
|
|
263
|
+
this.importMap.set(this.getCollectionKey("users"), importData);
|
|
212
264
|
}
|
|
213
265
|
return allUsers;
|
|
214
266
|
}
|
|
@@ -262,7 +314,7 @@ export class DataLoader {
|
|
|
262
314
|
}
|
|
263
315
|
}
|
|
264
316
|
console.log("Running update references");
|
|
265
|
-
this.dealWithMergedUsers();
|
|
317
|
+
// this.dealWithMergedUsers();
|
|
266
318
|
this.updateOldReferencesForNew();
|
|
267
319
|
console.log("Done running update references");
|
|
268
320
|
}
|
|
@@ -280,8 +332,11 @@ export class DataLoader {
|
|
|
280
332
|
const usersCollectionKey = this.getCollectionKey(this.config.usersCollectionName);
|
|
281
333
|
const usersCollectionPrimaryKeyFields = new Set();
|
|
282
334
|
if (!this.config.collections) {
|
|
335
|
+
console.log("No collections found in configuration.");
|
|
283
336
|
return;
|
|
284
337
|
}
|
|
338
|
+
let needsUpdate = false;
|
|
339
|
+
let numUpdates = 0;
|
|
285
340
|
// Collect primary key fields from the users collection definitions
|
|
286
341
|
this.config.collections.forEach((collection) => {
|
|
287
342
|
if (this.getCollectionKey(collection.name) === usersCollectionKey) {
|
|
@@ -296,37 +351,61 @@ export class DataLoader {
|
|
|
296
351
|
});
|
|
297
352
|
}
|
|
298
353
|
});
|
|
354
|
+
console.log(`Primary key fields collected for users collection: ${[
|
|
355
|
+
...usersCollectionPrimaryKeyFields,
|
|
356
|
+
]}`);
|
|
299
357
|
// Iterate over all collections to update references based on merged users
|
|
300
358
|
this.config.collections.forEach((collection) => {
|
|
301
359
|
const collectionData = this.importMap.get(this.getCollectionKey(collection.name));
|
|
302
|
-
if (!collectionData || !collectionData.data)
|
|
360
|
+
if (!collectionData || !collectionData.data) {
|
|
361
|
+
console.log(`No data found for collection ${collection.name}`);
|
|
303
362
|
return;
|
|
363
|
+
}
|
|
304
364
|
const collectionImportDefs = collection.importDefs;
|
|
305
365
|
if (!collectionImportDefs || !collectionImportDefs.length) {
|
|
366
|
+
console.log(`No import definitions found for collection ${collection.name}`);
|
|
306
367
|
return;
|
|
307
368
|
}
|
|
308
369
|
collectionImportDefs.forEach((importDef) => {
|
|
309
370
|
importDef.idMappings?.forEach((idMapping) => {
|
|
310
371
|
if (this.getCollectionKey(idMapping.targetCollection) ===
|
|
311
372
|
usersCollectionKey) {
|
|
373
|
+
const fieldToSetKey = idMapping.fieldToSet || idMapping.sourceField;
|
|
312
374
|
const targetFieldKey = idMapping.targetFieldToMatch || idMapping.targetField;
|
|
313
375
|
if (usersCollectionPrimaryKeyFields.has(targetFieldKey)) {
|
|
376
|
+
console.log(`Processing collection ${collection.name} with target field ${targetFieldKey}`);
|
|
314
377
|
// Process each item in the collection
|
|
315
378
|
collectionData.data.forEach((item) => {
|
|
316
|
-
const oldId = item.
|
|
379
|
+
const oldId = item.finalData[idMapping.sourceField] ||
|
|
380
|
+
item.context[idMapping.sourceField];
|
|
381
|
+
if (oldId === undefined || oldId === null) {
|
|
382
|
+
console.log(`Skipping item with undefined or null oldId in collection ${collection.name}`);
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
317
385
|
const newId = this.mergedUserMap.get(`${oldId}`);
|
|
318
386
|
if (newId) {
|
|
387
|
+
needsUpdate = true;
|
|
388
|
+
numUpdates++;
|
|
389
|
+
console.log(`Updating old ID ${oldId} to new ID ${newId} in collection ${collection.name}`);
|
|
319
390
|
// Update context to use new user ID
|
|
320
|
-
item.finalData[
|
|
391
|
+
item.finalData[fieldToSetKey] = newId;
|
|
392
|
+
item.context[fieldToSetKey] = newId;
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
console.log(`No new ID found for old ID ${oldId} in mergedUserMap.`);
|
|
321
396
|
}
|
|
322
397
|
});
|
|
323
398
|
}
|
|
324
399
|
}
|
|
325
400
|
});
|
|
326
401
|
});
|
|
402
|
+
if (needsUpdate) {
|
|
403
|
+
console.log(`Updated ${numUpdates} references for collection ${collection.name}`);
|
|
404
|
+
this.importMap.set(this.getCollectionKey(collection.name), collectionData);
|
|
405
|
+
}
|
|
327
406
|
});
|
|
328
407
|
}
|
|
329
|
-
|
|
408
|
+
updateOldReferencesForNew() {
|
|
330
409
|
if (!this.config.collections) {
|
|
331
410
|
return;
|
|
332
411
|
}
|
|
@@ -348,7 +427,9 @@ export class DataLoader {
|
|
|
348
427
|
const targetFieldKey = idMapping.targetFieldToMatch || idMapping.targetField;
|
|
349
428
|
const valueToMatch = collectionData.data[i].context[idMapping.sourceField];
|
|
350
429
|
// Skip if value to match is missing or empty
|
|
351
|
-
if (!valueToMatch ||
|
|
430
|
+
if (!valueToMatch ||
|
|
431
|
+
_.isEmpty(valueToMatch) ||
|
|
432
|
+
valueToMatch === null)
|
|
352
433
|
continue;
|
|
353
434
|
const isFieldToSetArray = collectionConfig.attributes.find((attribute) => attribute.key === fieldToSetKey)?.array;
|
|
354
435
|
const targetCollectionData = this.importMap.get(targetCollectionKey);
|
|
@@ -467,7 +548,7 @@ export class DataLoader {
|
|
|
467
548
|
* @param attributeMappings - The attribute mappings for the item.
|
|
468
549
|
* @returns The transformed item with user-specific keys removed.
|
|
469
550
|
*/
|
|
470
|
-
|
|
551
|
+
prepareUserData(item, attributeMappings, primaryKeyField, newId) {
|
|
471
552
|
let transformedItem = this.transformData(item, attributeMappings);
|
|
472
553
|
const userData = AuthUserCreateSchema.safeParse(transformedItem);
|
|
473
554
|
if (!userData.success) {
|
|
@@ -515,15 +596,23 @@ export class DataLoader {
|
|
|
515
596
|
});
|
|
516
597
|
if (userFound) {
|
|
517
598
|
userFound.finalData.userId = existingId;
|
|
599
|
+
userFound.finalData.docId = existingId;
|
|
600
|
+
this.userExistsMap.set(existingId, true);
|
|
518
601
|
}
|
|
519
|
-
|
|
602
|
+
const userKeys = ["email", "phone", "name", "labels", "prefs"];
|
|
603
|
+
userKeys.forEach((key) => {
|
|
604
|
+
if (transformedItem.hasOwnProperty(key)) {
|
|
605
|
+
delete transformedItem[key];
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
return {
|
|
520
609
|
transformedItem,
|
|
521
610
|
existingId,
|
|
522
|
-
{
|
|
611
|
+
userData: {
|
|
523
612
|
rawData: userFound?.rawData,
|
|
524
613
|
finalData: userFound?.finalData,
|
|
525
614
|
},
|
|
526
|
-
|
|
615
|
+
};
|
|
527
616
|
}
|
|
528
617
|
else {
|
|
529
618
|
existingId = newId;
|
|
@@ -543,7 +632,11 @@ export class DataLoader {
|
|
|
543
632
|
this.importMap.set(this.getCollectionKey("users"), {
|
|
544
633
|
data: [...(usersMap?.data || []), userDataToAdd],
|
|
545
634
|
});
|
|
546
|
-
return
|
|
635
|
+
return {
|
|
636
|
+
transformedItem,
|
|
637
|
+
existingId,
|
|
638
|
+
userData: userDataToAdd,
|
|
639
|
+
};
|
|
547
640
|
}
|
|
548
641
|
/**
|
|
549
642
|
* Prepares the data for creating user collection documents.
|
|
@@ -588,7 +681,7 @@ export class DataLoader {
|
|
|
588
681
|
// Iterate through each item in the raw data
|
|
589
682
|
for (const item of rawData) {
|
|
590
683
|
// Prepare user data, check for duplicates, and remove user-specific keys
|
|
591
|
-
let
|
|
684
|
+
let { transformedItem, existingId, userData } = this.prepareUserData(item, importDef.attributeMappings, importDef.primaryKeyField, this.getTrueUniqueId(this.getCollectionKey("users")));
|
|
592
685
|
logger.info(`In create user -- transformedItem: ${JSON.stringify(transformedItem, null, 2)}`);
|
|
593
686
|
// Generate a new unique ID for the item or use existing ID
|
|
594
687
|
if (!existingId) {
|
|
@@ -620,15 +713,22 @@ export class DataLoader {
|
|
|
620
713
|
collectionOldIdToNewIdMap?.set(`${oldId}`, `${existingId}`);
|
|
621
714
|
}
|
|
622
715
|
}
|
|
623
|
-
// Merge the final user data into the context
|
|
624
|
-
context = { ...context, ...userData.finalData };
|
|
625
716
|
// Handle merging for currentUserData
|
|
626
717
|
for (let i = 0; i < currentUserData.data.length; i++) {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
718
|
+
const currentUserDataItem = currentUserData.data[i];
|
|
719
|
+
const samePhones = currentUserDataItem.finalData.phone &&
|
|
720
|
+
transformedItem.phone &&
|
|
721
|
+
currentUserDataItem.finalData.phone === transformedItem.phone;
|
|
722
|
+
const sameEmails = currentUserDataItem.finalData.email &&
|
|
723
|
+
transformedItem.email &&
|
|
724
|
+
currentUserDataItem.finalData.email === transformedItem.email;
|
|
725
|
+
if ((currentUserDataItem.finalData.docId === existingId ||
|
|
726
|
+
currentUserDataItem.finalData.userId === existingId) &&
|
|
727
|
+
(samePhones || sameEmails) &&
|
|
728
|
+
currentUserDataItem.finalData &&
|
|
729
|
+
userData.finalData) {
|
|
730
|
+
const userDataMerged = this.mergeObjects(currentUserData.data[i].finalData, userData.finalData);
|
|
731
|
+
currentUserData.data[i].finalData = userDataMerged;
|
|
632
732
|
this.importMap.set(this.getCollectionKey("users"), currentUserData);
|
|
633
733
|
}
|
|
634
734
|
}
|
|
@@ -691,16 +791,23 @@ export class DataLoader {
|
|
|
691
791
|
.set(this.getCollectionKey(collection.name), oldIdToNewIdMapNew)
|
|
692
792
|
.get(this.getCollectionKey(collection.name));
|
|
693
793
|
console.log(`${collection.name} -- collectionOldIdToNewIdMap: ${collectionOldIdToNewIdMap}`);
|
|
794
|
+
const isRegions = collection.name.toLowerCase() === "regions";
|
|
694
795
|
// Iterate through each item in the raw data
|
|
695
796
|
for (const item of rawData) {
|
|
696
797
|
// Generate a new unique ID for the item
|
|
697
798
|
const itemIdNew = this.getTrueUniqueId(this.getCollectionKey(collection.name));
|
|
799
|
+
if (isRegions) {
|
|
800
|
+
logger.info(`Creating region: ${JSON.stringify(item, null, 2)}`);
|
|
801
|
+
}
|
|
698
802
|
// Retrieve the current collection data from the import map
|
|
699
803
|
const currentData = this.importMap.get(this.getCollectionKey(collection.name));
|
|
700
804
|
// Create a context object for the item, including the new ID
|
|
701
805
|
let context = this.createContext(db, collection, item, itemIdNew);
|
|
702
806
|
// Transform the item data based on the attribute mappings
|
|
703
807
|
const transformedData = this.transformData(item, importDef.attributeMappings);
|
|
808
|
+
if (isRegions) {
|
|
809
|
+
logger.info(`Transformed region: ${JSON.stringify(transformedData, null, 2)}`);
|
|
810
|
+
}
|
|
704
811
|
// If a primary key field is defined, handle the ID mapping and check for duplicates
|
|
705
812
|
if (importDef.primaryKeyField) {
|
|
706
813
|
const oldId = item[importDef.primaryKeyField];
|
|
@@ -713,7 +820,7 @@ export class DataLoader {
|
|
|
713
820
|
// Merge the transformed data into the context
|
|
714
821
|
context = { ...context, ...transformedData };
|
|
715
822
|
// Validate the item before proceeding
|
|
716
|
-
const isValid =
|
|
823
|
+
const isValid = this.importDataActions.validateItem(transformedData, importDef.attributeMappings, context);
|
|
717
824
|
if (!isValid) {
|
|
718
825
|
continue;
|
|
719
826
|
}
|
|
@@ -751,7 +858,7 @@ export class DataLoader {
|
|
|
751
858
|
* @param collection - The collection configuration.
|
|
752
859
|
* @param importDef - The import definition containing the attribute mappings and other relevant info.
|
|
753
860
|
*/
|
|
754
|
-
|
|
861
|
+
prepareUpdateData(db, collection, importDef) {
|
|
755
862
|
// Retrieve the current collection data and old-to-new ID map from the import map
|
|
756
863
|
const currentData = this.importMap.get(this.getCollectionKey(collection.name));
|
|
757
864
|
const oldIdToNewIdMap = this.oldIdToNewIdPerCollectionMap.get(this.getCollectionKey(collection.name));
|
|
@@ -773,12 +880,7 @@ export class DataLoader {
|
|
|
773
880
|
let newId;
|
|
774
881
|
let oldId;
|
|
775
882
|
// Determine the new ID for the item based on the primary key field or update mapping
|
|
776
|
-
|
|
777
|
-
oldId = item[importDef.primaryKeyField];
|
|
778
|
-
}
|
|
779
|
-
else if (importDef.updateMapping) {
|
|
780
|
-
oldId = item[importDef.updateMapping.originalIdField];
|
|
781
|
-
}
|
|
883
|
+
oldId = item[importDef.primaryKeyField];
|
|
782
884
|
if (oldId) {
|
|
783
885
|
newId = oldIdToNewIdMap?.get(`${oldId}`);
|
|
784
886
|
if (!newId &&
|
|
@@ -796,24 +898,31 @@ export class DataLoader {
|
|
|
796
898
|
logger.error(`No old ID found (to update another document with) in prepareUpdateData for ${collection.name}, ${JSON.stringify(item, null, 2)}`);
|
|
797
899
|
continue;
|
|
798
900
|
}
|
|
901
|
+
const itemDataToUpdate = this.importMap
|
|
902
|
+
.get(this.getCollectionKey(collection.name))
|
|
903
|
+
?.data.find((data) => `${data.context[importDef.primaryKeyField]}` === `${oldId}`);
|
|
799
904
|
// Log an error and continue to the next item if no new ID is found
|
|
800
|
-
if (!newId) {
|
|
905
|
+
if (!newId && !itemDataToUpdate) {
|
|
801
906
|
logger.error(`No new id found for collection ${collection.name} for updateDef ${JSON.stringify(item, null, 2)} but it says it's supposed to have one...`);
|
|
802
907
|
continue;
|
|
803
908
|
}
|
|
804
|
-
|
|
805
|
-
.
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
909
|
+
else if (itemDataToUpdate) {
|
|
910
|
+
newId = itemDataToUpdate.finalData.docId;
|
|
911
|
+
if (!newId) {
|
|
912
|
+
logger.error(`No new id found for collection ${collection.name} for updateDef ${JSON.stringify(item, null, 2)} but it says it's supposed to have one...`);
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
if (!itemDataToUpdate || !newId) {
|
|
917
|
+
logger.error(`No data or ID (docId) found for collection ${collection.name} for updateDef ${JSON.stringify(item, null, 2)} but it says it's supposed to have one...`);
|
|
809
918
|
continue;
|
|
810
919
|
}
|
|
811
920
|
transformedData = this.mergeObjects(itemDataToUpdate.finalData, transformedData);
|
|
812
921
|
// Create a context object for the item, including the new ID and transformed data
|
|
813
922
|
let context = this.createContext(db, collection, item, newId);
|
|
814
|
-
context =
|
|
923
|
+
context = { ...context, ...transformedData };
|
|
815
924
|
// Validate the item before proceeding
|
|
816
|
-
const isValid =
|
|
925
|
+
const isValid = this.importDataActions.validateItem(item, importDef.attributeMappings, context);
|
|
817
926
|
// Log info and continue to the next item if it's invalid
|
|
818
927
|
if (!isValid) {
|
|
819
928
|
logger.info(`Skipping item: ${JSON.stringify(item, null, 2)} because it's invalid`);
|
|
@@ -832,6 +941,7 @@ export class DataLoader {
|
|
|
832
941
|
itemDataToUpdate.finalData = this.mergeObjects(itemDataToUpdate.finalData, transformedData);
|
|
833
942
|
itemDataToUpdate.context = context;
|
|
834
943
|
itemDataToUpdate.importDef = newImportDef;
|
|
944
|
+
currentData.data.push(itemDataToUpdate);
|
|
835
945
|
}
|
|
836
946
|
else {
|
|
837
947
|
// If no existing item matches, then add the new item
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Databases, Query } from "node-appwrite";
|
|
2
|
+
import { tryAwaitWithRetry } from "../utils/helperFunctions.js";
|
|
2
3
|
export const fetchAllDatabases = async (database) => {
|
|
3
|
-
const databases = await database.list([Query.limit(25)]);
|
|
4
|
+
const databases = await tryAwaitWithRetry(async () => await database.list([Query.limit(25)]));
|
|
4
5
|
const allDatabases = databases.databases;
|
|
5
6
|
let lastDatabaseId = allDatabases[allDatabases.length - 1].$id;
|
|
6
7
|
if (databases.databases.length < 25) {
|
|
@@ -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);
|
|
@@ -11,16 +11,24 @@ export declare class ImportDataActions {
|
|
|
11
11
|
private validityRuleDefinitions;
|
|
12
12
|
private afterImportActionsDefinitions;
|
|
13
13
|
constructor(db: Databases, storage: Storage, config: AppwriteConfig, converterDefinitions: ConverterFunctions, validityRuleDefinitions: ValidationRules, afterImportActionsDefinitions: AfterImportActions);
|
|
14
|
+
/**
|
|
15
|
+
* Runs converter functions on the item based on the provided attribute mappings.
|
|
16
|
+
*
|
|
17
|
+
* @param item - The item to be transformed.
|
|
18
|
+
* @param attributeMappings - The mappings that define how each attribute should be transformed.
|
|
19
|
+
* @returns The transformed item.
|
|
20
|
+
*/
|
|
14
21
|
runConverterFunctions(item: any, attributeMappings: AttributeMappings): any;
|
|
15
22
|
/**
|
|
16
23
|
* Validates a single data item based on defined validation rules.
|
|
17
24
|
* @param item The data item to validate.
|
|
25
|
+
* @param attributeMap The attribute mappings for the data item.
|
|
18
26
|
* @param context The context for resolving templated parameters in validation rules.
|
|
19
27
|
* @returns A promise that resolves to true if the item is valid, false otherwise.
|
|
20
28
|
*/
|
|
21
29
|
validateItem(item: any, attributeMap: AttributeMappings, context: {
|
|
22
30
|
[key: string]: any;
|
|
23
|
-
}):
|
|
31
|
+
}): boolean;
|
|
24
32
|
executeAfterImportActions(item: any, attributeMap: AttributeMappings, context: {
|
|
25
33
|
[key: string]: any;
|
|
26
34
|
}): 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;
|
|
@@ -20,9 +21,19 @@ export class ImportDataActions {
|
|
|
20
21
|
this.validityRuleDefinitions = validityRuleDefinitions;
|
|
21
22
|
this.afterImportActionsDefinitions = afterImportActionsDefinitions;
|
|
22
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Runs converter functions on the item based on the provided attribute mappings.
|
|
26
|
+
*
|
|
27
|
+
* @param item - The item to be transformed.
|
|
28
|
+
* @param attributeMappings - The mappings that define how each attribute should be transformed.
|
|
29
|
+
* @returns The transformed item.
|
|
30
|
+
*/
|
|
23
31
|
runConverterFunctions(item, attributeMappings) {
|
|
24
32
|
const conversionSchema = attributeMappings.reduce((schema, mapping) => {
|
|
25
33
|
schema[mapping.targetKey] = (originalValue) => {
|
|
34
|
+
if (!mapping.converters) {
|
|
35
|
+
return originalValue;
|
|
36
|
+
}
|
|
26
37
|
return mapping.converters?.reduce((value, converterName) => {
|
|
27
38
|
let shouldProcessAsArray = false;
|
|
28
39
|
if ((converterName.includes("[Arr]") ||
|
|
@@ -65,10 +76,11 @@ export class ImportDataActions {
|
|
|
65
76
|
/**
|
|
66
77
|
* Validates a single data item based on defined validation rules.
|
|
67
78
|
* @param item The data item to validate.
|
|
79
|
+
* @param attributeMap The attribute mappings for the data item.
|
|
68
80
|
* @param context The context for resolving templated parameters in validation rules.
|
|
69
81
|
* @returns A promise that resolves to true if the item is valid, false otherwise.
|
|
70
82
|
*/
|
|
71
|
-
|
|
83
|
+
validateItem(item, attributeMap, context) {
|
|
72
84
|
for (const mapping of attributeMap) {
|
|
73
85
|
const { validationActions } = mapping;
|
|
74
86
|
if (!validationActions ||
|
|
@@ -111,7 +123,7 @@ export class ImportDataActions {
|
|
|
111
123
|
const { action, params } = actionDef;
|
|
112
124
|
console.log(`Executing post-import action '${action}' for attribute '${mapping.targetKey}' with params ${params.join(", ")}...`);
|
|
113
125
|
try {
|
|
114
|
-
await this.executeAction(action, params, context, item);
|
|
126
|
+
await tryAwaitWithRetry(async () => await this.executeAction(action, params, context, item));
|
|
115
127
|
}
|
|
116
128
|
catch (error) {
|
|
117
129
|
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}`);
|