appwrite-utils-cli 0.0.260 → 0.0.262
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/dist/main.js +0 -0
- package/dist/migrations/converters.js +1 -1
- package/dist/migrations/dataLoader.d.ts +11 -3
- package/dist/migrations/dataLoader.js +76 -81
- package/dist/migrations/importController.js +3 -0
- package/dist/migrations/importDataActions.d.ts +1 -1
- package/dist/migrations/importDataActions.js +1 -1
- package/dist/migrations/schema.d.ts +18 -0
- package/dist/migrations/schema.js +14 -14
- package/dist/setup.js +0 -0
- package/dist/utils/helperFunctions.d.ts +3 -0
- package/dist/utils/helperFunctions.js +8 -0
- package/package.json +1 -1
- package/src/migrations/converters.ts +2 -2
- package/src/migrations/dataLoader.ts +102 -92
- package/src/migrations/importController.ts +3 -0
- package/src/migrations/importDataActions.ts +1 -1
- package/src/migrations/schema.ts +25 -22
- package/src/utils/helperFunctions.ts +17 -0
package/dist/main.js
CHANGED
File without changes
|
@@ -10,7 +10,7 @@ export const converterFunctions = {
|
|
10
10
|
anyToString(value) {
|
11
11
|
if (value == null)
|
12
12
|
return null;
|
13
|
-
return typeof value === "string" ? value :
|
13
|
+
return typeof value === "string" ? value : `${value}`;
|
14
14
|
},
|
15
15
|
/**
|
16
16
|
* Converts any value to a number. Returns null for non-numeric values, null, or undefined.
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import type { ImportDataActions } from "./importDataActions.js";
|
2
|
-
import { type AppwriteConfig, type AttributeMappings, type ConfigCollection, type ConfigDatabase, type ImportDef } from "./schema.js";
|
2
|
+
import { type AppwriteConfig, type AttributeMappings, type ConfigCollection, type ConfigDatabase, type IdMapping, type ImportDef } from "./schema.js";
|
3
3
|
import { z } from "zod";
|
4
4
|
import { type Databases } from "node-appwrite";
|
5
5
|
export declare const CollectionImportDataSchema: z.ZodObject<{
|
@@ -232,7 +232,14 @@ export declare const CollectionImportDataSchema: z.ZodObject<{
|
|
232
232
|
elements?: string[] | undefined;
|
233
233
|
xdefault?: string | null | undefined;
|
234
234
|
}>]>, z.ZodObject<{
|
235
|
-
key: z.ZodString;
|
235
|
+
key: z.ZodString; /**
|
236
|
+
* Transforms the given item based on the provided attribute mappings.
|
237
|
+
* This method applies conversion rules to the item's attributes as defined in the attribute mappings.
|
238
|
+
*
|
239
|
+
* @param item - The item to be transformed.
|
240
|
+
* @param attributeMappings - The mappings that define how each attribute should be transformed.
|
241
|
+
* @returns The transformed item.
|
242
|
+
*/
|
236
243
|
type: z.ZodDefault<z.ZodLiteral<"relationship">>;
|
237
244
|
error: z.ZodDefault<z.ZodString>;
|
238
245
|
required: z.ZodDefault<z.ZodBoolean>;
|
@@ -1572,7 +1579,7 @@ export declare class DataLoader {
|
|
1572
1579
|
* @param target - The target object with values to update the source object.
|
1573
1580
|
* @returns The updated source object.
|
1574
1581
|
*/
|
1575
|
-
mergeObjects(source:
|
1582
|
+
mergeObjects(source: any, update: any): any;
|
1576
1583
|
loadData(importDef: ImportDef): Promise<any[]>;
|
1577
1584
|
checkMapValuesForId(newId: string, collectionName: string): string | false;
|
1578
1585
|
getTrueUniqueId(collectionName: string): string;
|
@@ -1590,6 +1597,7 @@ export declare class DataLoader {
|
|
1590
1597
|
getAllUsers(): Promise<import("node-appwrite").Models.User<import("node-appwrite").Models.Preferences>[]>;
|
1591
1598
|
start(dbId: string): Promise<void>;
|
1592
1599
|
updateReferencesInRelatedCollections(): Promise<void>;
|
1600
|
+
findNewIdForOldId(oldId: string, idMapping: IdMapping): string | undefined;
|
1593
1601
|
private writeMapsToJsonFile;
|
1594
1602
|
/**
|
1595
1603
|
* Prepares user data by checking for duplicates based on email or phone, adding to a duplicate map if found,
|
@@ -10,6 +10,7 @@ import { findOrCreateOperation, updateOperation } from "./migrationHelper.js";
|
|
10
10
|
import { AuthUserCreateSchema } from "../schemas/authUser.js";
|
11
11
|
import _ from "lodash";
|
12
12
|
import { UsersController } from "./users.js";
|
13
|
+
import { finalizeByAttributeMap } from "../utils/helperFunctions.js";
|
13
14
|
// Define a schema for the structure of collection import data using Zod for validation
|
14
15
|
export const CollectionImportDataSchema = z.object({
|
15
16
|
// Optional collection creation schema
|
@@ -69,17 +70,33 @@ export class DataLoader {
|
|
69
70
|
* @param target - The target object with values to update the source object.
|
70
71
|
* @returns The updated source object.
|
71
72
|
*/
|
72
|
-
mergeObjects(source,
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
73
|
+
mergeObjects(source, update) {
|
74
|
+
// Create a new object to hold the merged result
|
75
|
+
const result = { ...source };
|
76
|
+
Object.keys(update).forEach((key) => {
|
77
|
+
const sourceValue = source[key];
|
78
|
+
const updateValue = update[key];
|
79
|
+
// If the update value is an array, concatenate and remove duplicates
|
80
|
+
if (Array.isArray(updateValue)) {
|
81
|
+
const sourceArray = Array.isArray(sourceValue) ? sourceValue : [];
|
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) {
|
96
|
+
result[key] = updateValue;
|
80
97
|
}
|
81
98
|
});
|
82
|
-
return
|
99
|
+
return result;
|
83
100
|
}
|
84
101
|
// Method to load data from a file specified in the import definition
|
85
102
|
async loadData(importDef) {
|
@@ -255,6 +272,7 @@ export class DataLoader {
|
|
255
272
|
const collectionData = this.importMap.get(collectionKey);
|
256
273
|
if (!collectionData || !collectionData.data)
|
257
274
|
continue;
|
275
|
+
console.log(`Updating references for collection: ${collectionConfig.name}`);
|
258
276
|
// Iterate over each data item in the current collection
|
259
277
|
for (const item of collectionData.data) {
|
260
278
|
let needsUpdate = false;
|
@@ -264,91 +282,62 @@ export class DataLoader {
|
|
264
282
|
if (importDef.idMappings) {
|
265
283
|
// Iterate over each idMapping defined for the current import definition
|
266
284
|
for (const idMapping of importDef.idMappings) {
|
267
|
-
const
|
268
|
-
|
285
|
+
const oldIds = Array.isArray(item.context[idMapping.sourceField])
|
286
|
+
? item.context[idMapping.sourceField]
|
287
|
+
: [item.context[idMapping.sourceField]];
|
288
|
+
oldIds.forEach((oldId) => {
|
269
289
|
let newIdForOldId;
|
270
|
-
//
|
290
|
+
// Handling users merged into a new ID
|
291
|
+
newIdForOldId = this.findNewIdForOldId(oldId, idMapping);
|
271
292
|
if (newIdForOldId) {
|
272
|
-
|
273
|
-
const
|
274
|
-
|
275
|
-
|
276
|
-
item.finalData[
|
293
|
+
const targetField = idMapping.fieldToSet || idMapping.targetField;
|
294
|
+
const isArray = collectionConfig.attributes.some((attribute) => attribute.key === targetField && attribute.array);
|
295
|
+
// Properly update the target field based on whether it should be an array
|
296
|
+
if (isArray) {
|
297
|
+
if (!Array.isArray(item.finalData[targetField])) {
|
298
|
+
item.finalData[targetField] = [newIdForOldId];
|
299
|
+
}
|
300
|
+
else if (!item.finalData[targetField].includes(newIdForOldId)) {
|
301
|
+
item.finalData[targetField].push(newIdForOldId);
|
302
|
+
}
|
277
303
|
}
|
278
304
|
else {
|
279
|
-
|
280
|
-
item.finalData[fieldToUpdate] = newIdForOldId;
|
305
|
+
item.finalData[targetField] = newIdForOldId;
|
281
306
|
}
|
282
|
-
console.log(`Updated ${oldId} to ${newIdForOldId}`);
|
283
307
|
needsUpdate = true;
|
284
308
|
}
|
285
|
-
}
|
309
|
+
});
|
286
310
|
}
|
287
311
|
}
|
288
312
|
}
|
289
313
|
}
|
314
|
+
// Update the importMap if changes were made to the item
|
290
315
|
if (needsUpdate) {
|
291
|
-
// Re-transform the item's finalData using its attribute mappings
|
292
|
-
const importDef = item.importDef; // Assuming importDef is available in the item
|
293
|
-
if (importDef && importDef.attributeMappings) {
|
294
|
-
item.finalData = await this.transformData(item.finalData, importDef.attributeMappings);
|
295
|
-
}
|
296
316
|
this.importMap.set(collectionKey, collectionData);
|
317
|
+
logger.info(`Updated item: ${JSON.stringify(item.finalData, undefined, 2)}`);
|
297
318
|
}
|
298
319
|
}
|
299
320
|
}
|
300
321
|
}
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
// this.oldIdToNewIdPerCollectionMap.get(
|
321
|
-
// this.getCollectionKey(mapping.targetCollection)
|
322
|
-
// );
|
323
|
-
// if (
|
324
|
-
// referenceCollectionMap &&
|
325
|
-
// referenceCollectionMap.has(oldReferenceId)
|
326
|
-
// ) {
|
327
|
-
// // Update the target field with the new reference ID from the mapped collection
|
328
|
-
// item[mapping.sourceField as keyof typeof item] =
|
329
|
-
// referenceCollectionMap.get(oldReferenceId);
|
330
|
-
// needsUpdate = true;
|
331
|
-
// console.log(
|
332
|
-
// `Updated item with ${mapping.sourceField} = ${JSON.stringify(
|
333
|
-
// item,
|
334
|
-
// null,
|
335
|
-
// undefined
|
336
|
-
// )}.`
|
337
|
-
// );
|
338
|
-
// }
|
339
|
-
// }
|
340
|
-
// // Save changes if any reference was updated
|
341
|
-
// if (needsUpdate) {
|
342
|
-
// await this.saveUpdatedItem(
|
343
|
-
// collection.name,
|
344
|
-
// item,
|
345
|
-
// importDef.primaryKeyField
|
346
|
-
// );
|
347
|
-
// }
|
348
|
-
// }
|
349
|
-
// }
|
350
|
-
// }
|
351
|
-
// }
|
322
|
+
findNewIdForOldId(oldId, idMapping) {
|
323
|
+
// Check merged users first for any corresponding new ID
|
324
|
+
let newIdForOldId;
|
325
|
+
for (const [newUserId, oldIds] of this.mergedUserMap.entries()) {
|
326
|
+
if (oldIds.includes(oldId)) {
|
327
|
+
newIdForOldId = newUserId;
|
328
|
+
break;
|
329
|
+
}
|
330
|
+
}
|
331
|
+
// If no new ID found in merged users, check the old-to-new ID map for the target collection
|
332
|
+
if (!newIdForOldId) {
|
333
|
+
const targetCollectionKey = this.getCollectionKey(idMapping.targetCollection);
|
334
|
+
const targetOldIdToNewIdMap = this.oldIdToNewIdPerCollectionMap.get(targetCollectionKey);
|
335
|
+
if (targetOldIdToNewIdMap && targetOldIdToNewIdMap.has(oldId)) {
|
336
|
+
newIdForOldId = targetOldIdToNewIdMap.get(oldId);
|
337
|
+
}
|
338
|
+
}
|
339
|
+
return newIdForOldId;
|
340
|
+
}
|
352
341
|
writeMapsToJsonFile() {
|
353
342
|
const outputDir = path.resolve(process.cwd());
|
354
343
|
const outputFile = path.join(outputDir, "dataLoaderOutput.json");
|
@@ -401,7 +390,6 @@ export class DataLoader {
|
|
401
390
|
// Check for duplicate email and add to emailToUserIdMap if not found
|
402
391
|
if (email && email.length > 0) {
|
403
392
|
if (this.emailToUserIdMap.has(email)) {
|
404
|
-
logger.error(`Duplicate email found or user exists already: ${email}`);
|
405
393
|
existingId = this.emailToUserIdMap.get(email);
|
406
394
|
}
|
407
395
|
else {
|
@@ -411,7 +399,6 @@ export class DataLoader {
|
|
411
399
|
// Check for duplicate phone and add to phoneToUserIdMap if not found
|
412
400
|
if (phone && phone.length > 0) {
|
413
401
|
if (this.phoneToUserIdMap.has(phone)) {
|
414
|
-
logger.error(`Duplicate phone found: ${phone}`);
|
415
402
|
existingId = this.phoneToUserIdMap.get(phone);
|
416
403
|
}
|
417
404
|
else {
|
@@ -728,15 +715,23 @@ export class DataLoader {
|
|
728
715
|
attributeMappings: mappingsWithActions,
|
729
716
|
};
|
730
717
|
// Add the item with its context and final data to the current collection data
|
731
|
-
if (
|
718
|
+
if (itemDataToUpdate) {
|
719
|
+
// Update the existing item's finalData and context in place
|
720
|
+
itemDataToUpdate.finalData = this.mergeObjects(itemDataToUpdate.finalData, transformedData);
|
721
|
+
itemDataToUpdate.context = context;
|
722
|
+
itemDataToUpdate.importDef = newImportDef;
|
723
|
+
}
|
724
|
+
else {
|
725
|
+
// If no existing item matches, then add the new item
|
732
726
|
currentData.data.push({
|
733
727
|
rawData: item,
|
734
728
|
context: context,
|
735
729
|
importDef: newImportDef,
|
736
730
|
finalData: transformedData,
|
737
731
|
});
|
738
|
-
this.importMap.set(this.getCollectionKey(collection.name), currentData);
|
739
732
|
}
|
733
|
+
// Since we're modifying currentData in place, we ensure no duplicates are added
|
734
|
+
this.importMap.set(this.getCollectionKey(collection.name), currentData);
|
740
735
|
}
|
741
736
|
}
|
742
737
|
updateReferencesBasedOnAttributeMappings() {
|
@@ -159,6 +159,9 @@ export class ImportController {
|
|
159
159
|
if (item.finalData.hasOwnProperty("docId")) {
|
160
160
|
delete item.finalData.docId;
|
161
161
|
}
|
162
|
+
if (!item.finalData) {
|
163
|
+
return Promise.resolve();
|
164
|
+
}
|
162
165
|
return this.database
|
163
166
|
.createDocument(db.$id, collection.$id, id, item.finalData)
|
164
167
|
.then(() => {
|
@@ -12,7 +12,7 @@ export declare class ImportDataActions {
|
|
12
12
|
private validityRuleDefinitions;
|
13
13
|
private afterImportActionsDefinitions;
|
14
14
|
constructor(db: Databases, storage: Storage, config: AppwriteConfig, converterDefinitions: ConverterFunctions, validityRuleDefinitions: ValidationRules, afterImportActionsDefinitions: AfterImportActions);
|
15
|
-
runConverterFunctions(item: any, attributeMappings: AttributeMappings):
|
15
|
+
runConverterFunctions(item: any, attributeMappings: AttributeMappings): any;
|
16
16
|
/**
|
17
17
|
* Validates a single data item based on defined validation rules.
|
18
18
|
* @param item The data item to validate.
|
@@ -18,7 +18,7 @@ export class ImportDataActions {
|
|
18
18
|
this.validityRuleDefinitions = validityRuleDefinitions;
|
19
19
|
this.afterImportActionsDefinitions = afterImportActionsDefinitions;
|
20
20
|
}
|
21
|
-
|
21
|
+
runConverterFunctions(item, attributeMappings) {
|
22
22
|
const conversionSchema = attributeMappings.reduce((schema, mapping) => {
|
23
23
|
schema[mapping.targetKey] = (originalValue) => {
|
24
24
|
return mapping.converters.reduce((value, converterName) => {
|
@@ -746,6 +746,22 @@ export declare const AttributeMappingsSchema: z.ZodArray<z.ZodObject<{
|
|
746
746
|
action: string;
|
747
747
|
}[] | undefined;
|
748
748
|
}>, "many">;
|
749
|
+
export declare const idMappingSchema: z.ZodArray<z.ZodObject<{
|
750
|
+
sourceField: z.ZodString;
|
751
|
+
fieldToSet: z.ZodOptional<z.ZodString>;
|
752
|
+
targetField: z.ZodString;
|
753
|
+
targetCollection: z.ZodString;
|
754
|
+
}, "strip", z.ZodTypeAny, {
|
755
|
+
targetField: string;
|
756
|
+
sourceField: string;
|
757
|
+
targetCollection: string;
|
758
|
+
fieldToSet?: string | undefined;
|
759
|
+
}, {
|
760
|
+
targetField: string;
|
761
|
+
sourceField: string;
|
762
|
+
targetCollection: string;
|
763
|
+
fieldToSet?: string | undefined;
|
764
|
+
}>, "many">;
|
749
765
|
export declare const importDefSchema: z.ZodObject<{
|
750
766
|
type: z.ZodOptional<z.ZodDefault<z.ZodEnum<["create", "update"]>>>;
|
751
767
|
filePath: z.ZodString;
|
@@ -3692,6 +3708,8 @@ export type ConfigDatabases = AppwriteConfig["databases"];
|
|
3692
3708
|
export type ConfigDatabase = ConfigDatabases[number];
|
3693
3709
|
export type ImportDefs = z.infer<typeof importDefSchemas>;
|
3694
3710
|
export type ImportDef = z.infer<typeof importDefSchema>;
|
3711
|
+
export type IdMappings = z.infer<typeof idMappingSchema>;
|
3712
|
+
export type IdMapping = IdMappings[number];
|
3695
3713
|
export type AttributeMappings = z.infer<typeof AttributeMappingsSchema>;
|
3696
3714
|
export type AttributeMapping = AttributeMappings[number];
|
3697
3715
|
export {};
|
@@ -366,6 +366,19 @@ export const AttributeMappingsSchema = z.array(z.object({
|
|
366
366
|
.describe("The after import actions and parameter placeholders (they'll be replaced with the actual data) to use for the import")
|
367
367
|
.default([]),
|
368
368
|
}));
|
369
|
+
export const idMappingSchema = z.array(z.object({
|
370
|
+
sourceField: z
|
371
|
+
.string()
|
372
|
+
.describe("The key of the data in the import data to match in the current data"),
|
373
|
+
fieldToSet: z
|
374
|
+
.string()
|
375
|
+
.optional()
|
376
|
+
.describe("The field to set in the target collection, if different from sourceField"),
|
377
|
+
targetField: z
|
378
|
+
.string()
|
379
|
+
.describe("The field in the target collection to match with sourceField that will then be updated"),
|
380
|
+
targetCollection: z.string().describe("The collection to search"),
|
381
|
+
}));
|
369
382
|
export const importDefSchema = z
|
370
383
|
.object({
|
371
384
|
type: z
|
@@ -382,20 +395,7 @@ export const importDefSchema = z
|
|
382
395
|
.string()
|
383
396
|
.default("id")
|
384
397
|
.describe("The field in the import data representing the primary key for this import data (if any)"),
|
385
|
-
idMappings:
|
386
|
-
.array(z.object({
|
387
|
-
sourceField: z
|
388
|
-
.string()
|
389
|
-
.describe("The key of the data in the import data to match in the current data"),
|
390
|
-
fieldToSet: z
|
391
|
-
.string()
|
392
|
-
.optional()
|
393
|
-
.describe("The field to set in the target collection, if different from sourceField"),
|
394
|
-
targetField: z
|
395
|
-
.string()
|
396
|
-
.describe("The field in the target collection to match with sourceField that will then be updated"),
|
397
|
-
targetCollection: z.string().describe("The collection to search"),
|
398
|
-
}))
|
398
|
+
idMappings: idMappingSchema
|
399
399
|
.optional()
|
400
400
|
.describe("The id mappings for the attribute to map ID's to"),
|
401
401
|
updateMapping: z
|
package/dist/setup.js
CHANGED
File without changes
|
@@ -1,4 +1,6 @@
|
|
1
1
|
import type { Models } from "node-appwrite";
|
2
|
+
import type { CollectionImportData } from "src/migrations/dataLoader.js";
|
3
|
+
import type { ConfigCollection } from "src/migrations/schema.js";
|
2
4
|
export declare const toPascalCase: (str: string) => string;
|
3
5
|
export declare const toCamelCase: (str: string) => string;
|
4
6
|
export declare const ensureDirectoryExistence: (filePath: string) => true | undefined;
|
@@ -32,3 +34,4 @@ export declare const getFileViewUrl: (endpoint: string, projectId: string, bucke
|
|
32
34
|
* @return {string} The complete download URL for the file.
|
33
35
|
*/
|
34
36
|
export declare const getFileDownloadUrl: (endpoint: string, projectId: string, bucketId: string, fileId: string, jwt?: Models.Jwt) => string;
|
37
|
+
export declare const finalizeByAttributeMap: (appwriteFolderPath: string, collection: ConfigCollection, item: CollectionImportData["data"][number]) => Promise<any>;
|
@@ -70,3 +70,11 @@ export const getFileViewUrl = (endpoint, projectId, bucketId, fileId, jwt) => {
|
|
70
70
|
export const getFileDownloadUrl = (endpoint, projectId, bucketId, fileId, jwt) => {
|
71
71
|
return `${endpoint}/storage/buckets/${bucketId}/files/${fileId}/download?project=${projectId}${jwt ? `&jwt=${jwt.jwt}` : ""}`;
|
72
72
|
};
|
73
|
+
export const finalizeByAttributeMap = async (appwriteFolderPath, collection, item) => {
|
74
|
+
const schemaFolderPath = path.join(appwriteFolderPath, "schemas");
|
75
|
+
const zodSchema = await import(`${schemaFolderPath}/${toCamelCase(collection.name)}.ts`);
|
76
|
+
return zodSchema.parse({
|
77
|
+
...item.context,
|
78
|
+
...item.finalData,
|
79
|
+
});
|
80
|
+
};
|
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.262",
|
5
5
|
"main": "src/main.ts",
|
6
6
|
"type": "module",
|
7
7
|
"repository": {
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import { DateTime } from "luxon";
|
2
2
|
import _ from "lodash";
|
3
|
-
import type { AppwriteConfig } from "./schema.js";
|
3
|
+
import type { AppwriteConfig, ConfigCollection } from "./schema.js";
|
4
4
|
|
5
5
|
const { cloneDeep, isObject } = _;
|
6
6
|
|
@@ -16,7 +16,7 @@ export const converterFunctions = {
|
|
16
16
|
*/
|
17
17
|
anyToString(value: any): string | null {
|
18
18
|
if (value == null) return null;
|
19
|
-
return typeof value === "string" ? value :
|
19
|
+
return typeof value === "string" ? value : `${value}`;
|
20
20
|
},
|
21
21
|
|
22
22
|
/**
|
@@ -7,6 +7,7 @@ import {
|
|
7
7
|
type AttributeMappings,
|
8
8
|
type ConfigCollection,
|
9
9
|
type ConfigDatabase,
|
10
|
+
type IdMapping,
|
10
11
|
type ImportDef,
|
11
12
|
type ImportDefs,
|
12
13
|
type RelationshipAttribute,
|
@@ -22,6 +23,7 @@ import { findOrCreateOperation, updateOperation } from "./migrationHelper.js";
|
|
22
23
|
import { AuthUserCreateSchema } from "../schemas/authUser.js";
|
23
24
|
import _ from "lodash";
|
24
25
|
import { UsersController } from "./users.js";
|
26
|
+
import { finalizeByAttributeMap } from "../utils/helperFunctions.js";
|
25
27
|
// Define a schema for the structure of collection import data using Zod for validation
|
26
28
|
export const CollectionImportDataSchema = z.object({
|
27
29
|
// Optional collection creation schema
|
@@ -96,22 +98,38 @@ export class DataLoader {
|
|
96
98
|
* @param target - The target object with values to update the source object.
|
97
99
|
* @returns The updated source object.
|
98
100
|
*/
|
99
|
-
mergeObjects(
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
Object.keys(
|
104
|
-
const
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
101
|
+
mergeObjects(source: any, update: any): any {
|
102
|
+
// Create a new object to hold the merged result
|
103
|
+
const result = { ...source };
|
104
|
+
|
105
|
+
Object.keys(update).forEach((key) => {
|
106
|
+
const sourceValue = source[key];
|
107
|
+
const updateValue = update[key];
|
108
|
+
|
109
|
+
// If the update value is an array, concatenate and remove duplicates
|
110
|
+
if (Array.isArray(updateValue)) {
|
111
|
+
const sourceArray = Array.isArray(sourceValue) ? sourceValue : [];
|
112
|
+
result[key] = [...new Set([...sourceArray, ...updateValue])];
|
113
|
+
}
|
114
|
+
// If the update value is an object, recursively merge
|
115
|
+
else if (
|
116
|
+
updateValue !== null &&
|
117
|
+
typeof updateValue === "object" &&
|
118
|
+
!(updateValue instanceof Date)
|
110
119
|
) {
|
111
|
-
|
120
|
+
result[key] = this.mergeObjects(sourceValue, updateValue);
|
121
|
+
}
|
122
|
+
// If the update value is not nullish, overwrite the source value
|
123
|
+
else if (updateValue !== null && updateValue !== undefined) {
|
124
|
+
result[key] = updateValue;
|
125
|
+
}
|
126
|
+
// If the update value is nullish, keep the original value unless it doesn't exist
|
127
|
+
else if (sourceValue === undefined) {
|
128
|
+
result[key] = updateValue;
|
112
129
|
}
|
113
130
|
});
|
114
|
-
|
131
|
+
|
132
|
+
return result;
|
115
133
|
}
|
116
134
|
|
117
135
|
// Method to load data from a file specified in the import definition
|
@@ -332,6 +350,10 @@ export class DataLoader {
|
|
332
350
|
|
333
351
|
if (!collectionData || !collectionData.data) continue;
|
334
352
|
|
353
|
+
console.log(
|
354
|
+
`Updating references for collection: ${collectionConfig.name}`
|
355
|
+
);
|
356
|
+
|
335
357
|
// Iterate over each data item in the current collection
|
336
358
|
for (const item of collectionData.data) {
|
337
359
|
let needsUpdate = false;
|
@@ -342,102 +364,82 @@ export class DataLoader {
|
|
342
364
|
if (importDef.idMappings) {
|
343
365
|
// Iterate over each idMapping defined for the current import definition
|
344
366
|
for (const idMapping of importDef.idMappings) {
|
345
|
-
const
|
346
|
-
|
347
|
-
|
348
|
-
|
367
|
+
const oldIds = Array.isArray(
|
368
|
+
item.context[idMapping.sourceField]
|
369
|
+
)
|
370
|
+
? item.context[idMapping.sourceField]
|
371
|
+
: [item.context[idMapping.sourceField]];
|
372
|
+
|
373
|
+
oldIds.forEach((oldId: any) => {
|
374
|
+
let newIdForOldId;
|
375
|
+
|
376
|
+
// Handling users merged into a new ID
|
377
|
+
newIdForOldId = this.findNewIdForOldId(oldId, idMapping);
|
349
378
|
|
350
379
|
if (newIdForOldId) {
|
351
|
-
|
352
|
-
const fieldToUpdate =
|
380
|
+
const targetField =
|
353
381
|
idMapping.fieldToSet || idMapping.targetField;
|
354
|
-
|
355
|
-
|
356
|
-
|
382
|
+
const isArray = collectionConfig.attributes.some(
|
383
|
+
(attribute) =>
|
384
|
+
attribute.key === targetField && attribute.array
|
385
|
+
);
|
386
|
+
|
387
|
+
// Properly update the target field based on whether it should be an array
|
388
|
+
if (isArray) {
|
389
|
+
if (!Array.isArray(item.finalData[targetField])) {
|
390
|
+
item.finalData[targetField] = [newIdForOldId];
|
391
|
+
} else if (
|
392
|
+
!item.finalData[targetField].includes(newIdForOldId)
|
393
|
+
) {
|
394
|
+
item.finalData[targetField].push(newIdForOldId);
|
395
|
+
}
|
357
396
|
} else {
|
358
|
-
|
359
|
-
item.finalData[fieldToUpdate] = newIdForOldId;
|
397
|
+
item.finalData[targetField] = newIdForOldId;
|
360
398
|
}
|
361
|
-
console.log(`Updated ${oldId} to ${newIdForOldId}`);
|
362
399
|
needsUpdate = true;
|
363
400
|
}
|
364
|
-
}
|
401
|
+
});
|
365
402
|
}
|
366
403
|
}
|
367
404
|
}
|
368
405
|
}
|
369
406
|
|
407
|
+
// Update the importMap if changes were made to the item
|
370
408
|
if (needsUpdate) {
|
371
|
-
// Re-transform the item's finalData using its attribute mappings
|
372
|
-
const importDef = item.importDef; // Assuming importDef is available in the item
|
373
|
-
if (importDef && importDef.attributeMappings) {
|
374
|
-
item.finalData = await this.transformData(
|
375
|
-
item.finalData,
|
376
|
-
importDef.attributeMappings
|
377
|
-
);
|
378
|
-
}
|
379
409
|
this.importMap.set(collectionKey, collectionData);
|
410
|
+
logger.info(
|
411
|
+
`Updated item: ${JSON.stringify(item.finalData, undefined, 2)}`
|
412
|
+
);
|
380
413
|
}
|
381
414
|
}
|
382
415
|
}
|
383
416
|
}
|
384
417
|
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
// this.getCollectionKey(collection.name)
|
395
|
-
// )?.data;
|
396
|
-
// if (!collectionData) continue;
|
397
|
-
|
398
|
-
// // Iterate over each item in the collection data
|
399
|
-
// for (const item of collectionData) {
|
400
|
-
// let needsUpdate = false;
|
418
|
+
findNewIdForOldId(oldId: string, idMapping: IdMapping) {
|
419
|
+
// Check merged users first for any corresponding new ID
|
420
|
+
let newIdForOldId;
|
421
|
+
for (const [newUserId, oldIds] of this.mergedUserMap.entries()) {
|
422
|
+
if (oldIds.includes(oldId)) {
|
423
|
+
newIdForOldId = newUserId;
|
424
|
+
break;
|
425
|
+
}
|
426
|
+
}
|
401
427
|
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
// );
|
428
|
+
// If no new ID found in merged users, check the old-to-new ID map for the target collection
|
429
|
+
if (!newIdForOldId) {
|
430
|
+
const targetCollectionKey = this.getCollectionKey(
|
431
|
+
idMapping.targetCollection
|
432
|
+
);
|
433
|
+
const targetOldIdToNewIdMap =
|
434
|
+
this.oldIdToNewIdPerCollectionMap.get(targetCollectionKey);
|
410
435
|
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
// // Update the target field with the new reference ID from the mapped collection
|
416
|
-
// item[mapping.sourceField as keyof typeof item] =
|
417
|
-
// referenceCollectionMap.get(oldReferenceId);
|
418
|
-
// needsUpdate = true;
|
419
|
-
// console.log(
|
420
|
-
// `Updated item with ${mapping.sourceField} = ${JSON.stringify(
|
421
|
-
// item,
|
422
|
-
// null,
|
423
|
-
// undefined
|
424
|
-
// )}.`
|
425
|
-
// );
|
426
|
-
// }
|
427
|
-
// }
|
436
|
+
if (targetOldIdToNewIdMap && targetOldIdToNewIdMap.has(oldId)) {
|
437
|
+
newIdForOldId = targetOldIdToNewIdMap.get(oldId);
|
438
|
+
}
|
439
|
+
}
|
428
440
|
|
429
|
-
|
430
|
-
|
431
|
-
// await this.saveUpdatedItem(
|
432
|
-
// collection.name,
|
433
|
-
// item,
|
434
|
-
// importDef.primaryKeyField
|
435
|
-
// );
|
436
|
-
// }
|
437
|
-
// }
|
438
|
-
// }
|
439
|
-
// }
|
440
|
-
// }
|
441
|
+
return newIdForOldId;
|
442
|
+
}
|
441
443
|
|
442
444
|
private writeMapsToJsonFile() {
|
443
445
|
const outputDir = path.resolve(process.cwd());
|
@@ -514,7 +516,6 @@ export class DataLoader {
|
|
514
516
|
// Check for duplicate email and add to emailToUserIdMap if not found
|
515
517
|
if (email && email.length > 0) {
|
516
518
|
if (this.emailToUserIdMap.has(email)) {
|
517
|
-
logger.error(`Duplicate email found or user exists already: ${email}`);
|
518
519
|
existingId = this.emailToUserIdMap.get(email);
|
519
520
|
} else {
|
520
521
|
this.emailToUserIdMap.set(email, newId);
|
@@ -524,7 +525,6 @@ export class DataLoader {
|
|
524
525
|
// Check for duplicate phone and add to phoneToUserIdMap if not found
|
525
526
|
if (phone && phone.length > 0) {
|
526
527
|
if (this.phoneToUserIdMap.has(phone)) {
|
527
|
-
logger.error(`Duplicate phone found: ${phone}`);
|
528
528
|
existingId = this.phoneToUserIdMap.get(phone);
|
529
529
|
} else {
|
530
530
|
this.phoneToUserIdMap.set(phone, newId);
|
@@ -999,15 +999,25 @@ export class DataLoader {
|
|
999
999
|
attributeMappings: mappingsWithActions,
|
1000
1000
|
};
|
1001
1001
|
// Add the item with its context and final data to the current collection data
|
1002
|
-
if (
|
1003
|
-
|
1002
|
+
if (itemDataToUpdate) {
|
1003
|
+
// Update the existing item's finalData and context in place
|
1004
|
+
itemDataToUpdate.finalData = this.mergeObjects(
|
1005
|
+
itemDataToUpdate.finalData,
|
1006
|
+
transformedData
|
1007
|
+
);
|
1008
|
+
itemDataToUpdate.context = context;
|
1009
|
+
itemDataToUpdate.importDef = newImportDef;
|
1010
|
+
} else {
|
1011
|
+
// If no existing item matches, then add the new item
|
1012
|
+
currentData!.data.push({
|
1004
1013
|
rawData: item,
|
1005
1014
|
context: context,
|
1006
1015
|
importDef: newImportDef,
|
1007
1016
|
finalData: transformedData,
|
1008
1017
|
});
|
1009
|
-
this.importMap.set(this.getCollectionKey(collection.name), currentData);
|
1010
1018
|
}
|
1019
|
+
// Since we're modifying currentData in place, we ensure no duplicates are added
|
1020
|
+
this.importMap.set(this.getCollectionKey(collection.name), currentData!);
|
1011
1021
|
}
|
1012
1022
|
}
|
1013
1023
|
|
@@ -236,6 +236,9 @@ export class ImportController {
|
|
236
236
|
if (item.finalData.hasOwnProperty("docId")) {
|
237
237
|
delete item.finalData.docId;
|
238
238
|
}
|
239
|
+
if (!item.finalData) {
|
240
|
+
return Promise.resolve();
|
241
|
+
}
|
239
242
|
return this.database
|
240
243
|
.createDocument(db.$id, collection.$id, id, item.finalData)
|
241
244
|
.then(() => {
|
@@ -45,7 +45,7 @@ export class ImportDataActions {
|
|
45
45
|
this.afterImportActionsDefinitions = afterImportActionsDefinitions;
|
46
46
|
}
|
47
47
|
|
48
|
-
|
48
|
+
runConverterFunctions(item: any, attributeMappings: AttributeMappings) {
|
49
49
|
const conversionSchema = attributeMappings.reduce((schema, mapping) => {
|
50
50
|
schema[mapping.targetKey] = (originalValue: any) => {
|
51
51
|
return mapping.converters.reduce((value, converterName) => {
|
package/src/migrations/schema.ts
CHANGED
@@ -435,6 +435,28 @@ export const AttributeMappingsSchema = z.array(
|
|
435
435
|
})
|
436
436
|
);
|
437
437
|
|
438
|
+
export const idMappingSchema = z.array(
|
439
|
+
z.object({
|
440
|
+
sourceField: z
|
441
|
+
.string()
|
442
|
+
.describe(
|
443
|
+
"The key of the data in the import data to match in the current data"
|
444
|
+
),
|
445
|
+
fieldToSet: z
|
446
|
+
.string()
|
447
|
+
.optional()
|
448
|
+
.describe(
|
449
|
+
"The field to set in the target collection, if different from sourceField"
|
450
|
+
),
|
451
|
+
targetField: z
|
452
|
+
.string()
|
453
|
+
.describe(
|
454
|
+
"The field in the target collection to match with sourceField that will then be updated"
|
455
|
+
),
|
456
|
+
targetCollection: z.string().describe("The collection to search"),
|
457
|
+
})
|
458
|
+
);
|
459
|
+
|
438
460
|
export const importDefSchema = z
|
439
461
|
.object({
|
440
462
|
type: z
|
@@ -457,28 +479,7 @@ export const importDefSchema = z
|
|
457
479
|
.describe(
|
458
480
|
"The field in the import data representing the primary key for this import data (if any)"
|
459
481
|
),
|
460
|
-
idMappings:
|
461
|
-
.array(
|
462
|
-
z.object({
|
463
|
-
sourceField: z
|
464
|
-
.string()
|
465
|
-
.describe(
|
466
|
-
"The key of the data in the import data to match in the current data"
|
467
|
-
),
|
468
|
-
fieldToSet: z
|
469
|
-
.string()
|
470
|
-
.optional()
|
471
|
-
.describe(
|
472
|
-
"The field to set in the target collection, if different from sourceField"
|
473
|
-
),
|
474
|
-
targetField: z
|
475
|
-
.string()
|
476
|
-
.describe(
|
477
|
-
"The field in the target collection to match with sourceField that will then be updated"
|
478
|
-
),
|
479
|
-
targetCollection: z.string().describe("The collection to search"),
|
480
|
-
})
|
481
|
-
)
|
482
|
+
idMappings: idMappingSchema
|
482
483
|
.optional()
|
483
484
|
.describe("The id mappings for the attribute to map ID's to"),
|
484
485
|
updateMapping: z
|
@@ -622,5 +623,7 @@ export type ConfigDatabases = AppwriteConfig["databases"];
|
|
622
623
|
export type ConfigDatabase = ConfigDatabases[number];
|
623
624
|
export type ImportDefs = z.infer<typeof importDefSchemas>;
|
624
625
|
export type ImportDef = z.infer<typeof importDefSchema>;
|
626
|
+
export type IdMappings = z.infer<typeof idMappingSchema>;
|
627
|
+
export type IdMapping = IdMappings[number];
|
625
628
|
export type AttributeMappings = z.infer<typeof AttributeMappingsSchema>;
|
626
629
|
export type AttributeMapping = AttributeMappings[number];
|
@@ -2,6 +2,8 @@ import type { AppwriteConfig } from "../types.js";
|
|
2
2
|
import type { Models, Storage } from "node-appwrite";
|
3
3
|
import fs from "node:fs";
|
4
4
|
import path from "node:path";
|
5
|
+
import type { CollectionImportData } from "src/migrations/dataLoader.js";
|
6
|
+
import type { ConfigCollection } from "src/migrations/schema.js";
|
5
7
|
|
6
8
|
export const toPascalCase = (str: string): string => {
|
7
9
|
return (
|
@@ -109,3 +111,18 @@ export const getFileDownloadUrl = (
|
|
109
111
|
jwt ? `&jwt=${jwt.jwt}` : ""
|
110
112
|
}`;
|
111
113
|
};
|
114
|
+
|
115
|
+
export const finalizeByAttributeMap = async (
|
116
|
+
appwriteFolderPath: string,
|
117
|
+
collection: ConfigCollection,
|
118
|
+
item: CollectionImportData["data"][number]
|
119
|
+
) => {
|
120
|
+
const schemaFolderPath = path.join(appwriteFolderPath, "schemas");
|
121
|
+
const zodSchema = await import(
|
122
|
+
`${schemaFolderPath}/${toCamelCase(collection.name)}.ts`
|
123
|
+
);
|
124
|
+
return zodSchema.parse({
|
125
|
+
...item.context,
|
126
|
+
...item.finalData,
|
127
|
+
});
|
128
|
+
};
|