appwrite-utils-cli 0.0.261 → 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 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 : JSON.stringify(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: Record<string, any>, target: Record<string, any>): Record<string, any>;
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, target) {
73
- Object.keys(target).forEach((key) => {
74
- const targetValue = target[key];
75
- if (source.hasOwnProperty(key) &&
76
- targetValue !== null &&
77
- targetValue !== undefined &&
78
- targetValue !== "") {
79
- source[key] = targetValue;
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 source;
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 oldId = item.context[idMapping.sourceField];
268
- if (oldId) {
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
- // Your existing logic to resolve newIdForOldId here...
290
+ // Handling users merged into a new ID
291
+ newIdForOldId = this.findNewIdForOldId(oldId, idMapping);
271
292
  if (newIdForOldId) {
272
- // Determine the field to update, handling arrays appropriately
273
- const fieldToUpdate = idMapping.fieldToSet || idMapping.targetField;
274
- if (Array.isArray(item.finalData[fieldToUpdate])) {
275
- // If the target field is an array, append the new ID
276
- item.finalData[fieldToUpdate].push(newIdForOldId);
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
- // Otherwise, directly set the new ID
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
- // async updateReferencesInRelatedCollections() {
302
- // // Process each collection defined in the config
303
- // for (const collection of this.config.collections) {
304
- // // Ensure we handle only those definitions with idMappings and a primaryKeyField defined
305
- // for (const importDef of collection.importDefs) {
306
- // if (!importDef.idMappings || !importDef.primaryKeyField) continue;
307
- // // Load data for this particular import definition
308
- // const collectionData = this.importMap.get(
309
- // this.getCollectionKey(collection.name)
310
- // )?.data;
311
- // if (!collectionData) continue;
312
- // // Iterate over each item in the collection data
313
- // for (const item of collectionData) {
314
- // let needsUpdate = false;
315
- // // Go through each idMapping defined for the collection
316
- // for (const mapping of importDef.idMappings) {
317
- // const oldReferenceId =
318
- // item[mapping.targetField as keyof typeof item]; // Get the current reference ID from the item
319
- // const referenceCollectionMap =
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 {
@@ -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): Promise<any>;
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
- async runConverterFunctions(item, attributeMappings) {
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: z
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.261",
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 : JSON.stringify(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
- source: Record<string, any>,
101
- target: Record<string, any>
102
- ): Record<string, any> {
103
- Object.keys(target).forEach((key) => {
104
- const targetValue = target[key];
105
- if (
106
- source.hasOwnProperty(key) &&
107
- targetValue !== null &&
108
- targetValue !== undefined &&
109
- targetValue !== ""
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
- source[key] = targetValue;
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
- return source;
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 oldId = item.context[idMapping.sourceField];
346
- if (oldId) {
347
- let newIdForOldId: string | undefined;
348
- // Your existing logic to resolve newIdForOldId here...
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
- // Determine the field to update, handling arrays appropriately
352
- const fieldToUpdate =
380
+ const targetField =
353
381
  idMapping.fieldToSet || idMapping.targetField;
354
- if (Array.isArray(item.finalData[fieldToUpdate])) {
355
- // If the target field is an array, append the new ID
356
- item.finalData[fieldToUpdate].push(newIdForOldId);
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
- // Otherwise, directly set the new ID
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
- // async updateReferencesInRelatedCollections() {
386
- // // Process each collection defined in the config
387
- // for (const collection of this.config.collections) {
388
- // // Ensure we handle only those definitions with idMappings and a primaryKeyField defined
389
- // for (const importDef of collection.importDefs) {
390
- // if (!importDef.idMappings || !importDef.primaryKeyField) continue;
391
-
392
- // // Load data for this particular import definition
393
- // const collectionData = this.importMap.get(
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
- // // Go through each idMapping defined for the collection
403
- // for (const mapping of importDef.idMappings) {
404
- // const oldReferenceId =
405
- // item[mapping.targetField as keyof typeof item]; // Get the current reference ID from the item
406
- // const referenceCollectionMap =
407
- // this.oldIdToNewIdPerCollectionMap.get(
408
- // this.getCollectionKey(mapping.targetCollection)
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
- // if (
412
- // referenceCollectionMap &&
413
- // referenceCollectionMap.has(oldReferenceId)
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
- // // Save changes if any reference was updated
430
- // if (needsUpdate) {
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);
@@ -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
- async runConverterFunctions(item: any, attributeMappings: AttributeMappings) {
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) => {
@@ -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: z
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
+ };