appwrite-utils-cli 0.0.65 → 0.0.67

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 CHANGED
@@ -132,6 +132,7 @@ This setup ensures that developers have robust tools at their fingertips to mana
132
132
 
133
133
  ### Changelog
134
134
 
135
+ - 0.0.67: Fixed `updates` in `importDef`'s update mappings overwriting postImportActions from the original
135
136
  - 0.0.57: Fixed `dataLoader`'s `idMapping`'s giving me issues
136
137
  - 0.0.55: Added `documentExists` check to batch creation functionality to try to prevent duplicates
137
138
  - 0.0.54: Various fixes in here
@@ -319,6 +319,7 @@ export declare const getMigrationCollectionSchemas: () => {
319
319
  fieldToSet?: string | undefined;
320
320
  targetFieldToMatch?: string | undefined;
321
321
  }[] | undefined;
322
+ createUsers?: boolean | null | undefined;
322
323
  updateMapping?: {
323
324
  targetField: string;
324
325
  originalIdField: string;
@@ -573,6 +574,7 @@ export declare const getMigrationCollectionSchemas: () => {
573
574
  fieldToSet?: string | undefined;
574
575
  targetFieldToMatch?: string | undefined;
575
576
  }[] | undefined;
577
+ createUsers?: boolean | null | undefined;
576
578
  updateMapping?: {
577
579
  targetField: string;
578
580
  originalIdField: string;
@@ -380,6 +380,7 @@ export declare const CollectionImportDataSchema: z.ZodObject<{
380
380
  fieldToSet?: string | undefined;
381
381
  targetFieldToMatch?: string | undefined;
382
382
  }>, "many">>;
383
+ createUsers: z.ZodOptional<z.ZodNullable<z.ZodDefault<z.ZodBoolean>>>;
383
384
  updateMapping: z.ZodOptional<z.ZodObject<{
384
385
  originalIdField: z.ZodString;
385
386
  targetField: z.ZodString;
@@ -494,6 +495,7 @@ export declare const CollectionImportDataSchema: z.ZodObject<{
494
495
  fieldToSet?: string | undefined;
495
496
  targetFieldToMatch?: string | undefined;
496
497
  }[] | undefined;
498
+ createUsers?: boolean | null | undefined;
497
499
  updateMapping?: {
498
500
  targetField: string;
499
501
  originalIdField: string;
@@ -529,6 +531,7 @@ export declare const CollectionImportDataSchema: z.ZodObject<{
529
531
  fieldToSet?: string | undefined;
530
532
  targetFieldToMatch?: string | undefined;
531
533
  }[] | undefined;
534
+ createUsers?: boolean | null | undefined;
532
535
  updateMapping?: {
533
536
  targetField: string;
534
537
  originalIdField: string;
@@ -623,16 +626,6 @@ export declare const CollectionImportDataSchema: z.ZodObject<{
623
626
  relatedCollection: string;
624
627
  relationType: "oneToMany" | "manyToOne" | "oneToOne" | "manyToMany";
625
628
  twoWay: boolean;
626
- /**
627
- * Generates attribute mappings with post-import actions based on the provided attribute mappings.
628
- * This method checks each mapping for a fileData attribute and adds a post-import action to create a file
629
- * and update the field with the file's ID if necessary.
630
- *
631
- * @param attributeMappings - The attribute mappings from the import definition.
632
- * @param context - The context object containing information about the database, collection, and document.
633
- * @param item - The item being imported, used for resolving template paths in fileData mappings.
634
- * @returns The attribute mappings updated with any necessary post-import actions.
635
- */
636
629
  twoWayKey: string;
637
630
  onDelete: "setNull" | "cascade" | "restrict";
638
631
  side: "parent" | "child";
@@ -689,6 +682,7 @@ export declare const CollectionImportDataSchema: z.ZodObject<{
689
682
  fieldToSet?: string | undefined;
690
683
  targetFieldToMatch?: string | undefined;
691
684
  }[] | undefined;
685
+ createUsers?: boolean | null | undefined;
692
686
  updateMapping?: {
693
687
  targetField: string;
694
688
  originalIdField: string;
@@ -845,6 +839,7 @@ export declare const CollectionImportDataSchema: z.ZodObject<{
845
839
  fieldToSet?: string | undefined;
846
840
  targetFieldToMatch?: string | undefined;
847
841
  }[] | undefined;
842
+ createUsers?: boolean | null | undefined;
848
843
  updateMapping?: {
849
844
  targetField: string;
850
845
  originalIdField: string;
@@ -880,6 +875,7 @@ export declare const CollectionImportDataSchema: z.ZodObject<{
880
875
  fieldToSet?: string | undefined;
881
876
  targetFieldToMatch?: string | undefined;
882
877
  }>, "many">>;
878
+ createUsers: z.ZodOptional<z.ZodNullable<z.ZodDefault<z.ZodBoolean>>>;
883
879
  updateMapping: z.ZodOptional<z.ZodObject<{
884
880
  originalIdField: z.ZodString;
885
881
  targetField: z.ZodString;
@@ -994,6 +990,7 @@ export declare const CollectionImportDataSchema: z.ZodObject<{
994
990
  fieldToSet?: string | undefined;
995
991
  targetFieldToMatch?: string | undefined;
996
992
  }[] | undefined;
993
+ createUsers?: boolean | null | undefined;
997
994
  updateMapping?: {
998
995
  targetField: string;
999
996
  originalIdField: string;
@@ -1029,6 +1026,7 @@ export declare const CollectionImportDataSchema: z.ZodObject<{
1029
1026
  fieldToSet?: string | undefined;
1030
1027
  targetFieldToMatch?: string | undefined;
1031
1028
  }[] | undefined;
1029
+ createUsers?: boolean | null | undefined;
1032
1030
  updateMapping?: {
1033
1031
  targetField: string;
1034
1032
  originalIdField: string;
@@ -1069,6 +1067,7 @@ export declare const CollectionImportDataSchema: z.ZodObject<{
1069
1067
  fieldToSet?: string | undefined;
1070
1068
  targetFieldToMatch?: string | undefined;
1071
1069
  }[] | undefined;
1070
+ createUsers?: boolean | null | undefined;
1072
1071
  updateMapping?: {
1073
1072
  targetField: string;
1074
1073
  originalIdField: string;
@@ -1109,6 +1108,7 @@ export declare const CollectionImportDataSchema: z.ZodObject<{
1109
1108
  fieldToSet?: string | undefined;
1110
1109
  targetFieldToMatch?: string | undefined;
1111
1110
  }[] | undefined;
1111
+ createUsers?: boolean | null | undefined;
1112
1112
  updateMapping?: {
1113
1113
  targetField: string;
1114
1114
  originalIdField: string;
@@ -1151,6 +1151,7 @@ export declare const CollectionImportDataSchema: z.ZodObject<{
1151
1151
  fieldToSet?: string | undefined;
1152
1152
  targetFieldToMatch?: string | undefined;
1153
1153
  }[] | undefined;
1154
+ createUsers?: boolean | null | undefined;
1154
1155
  updateMapping?: {
1155
1156
  targetField: string;
1156
1157
  originalIdField: string;
@@ -1245,16 +1246,6 @@ export declare const CollectionImportDataSchema: z.ZodObject<{
1245
1246
  relatedCollection: string;
1246
1247
  relationType: "oneToMany" | "manyToOne" | "oneToOne" | "manyToMany";
1247
1248
  twoWay: boolean;
1248
- /**
1249
- * Generates attribute mappings with post-import actions based on the provided attribute mappings.
1250
- * This method checks each mapping for a fileData attribute and adds a post-import action to create a file
1251
- * and update the field with the file's ID if necessary.
1252
- *
1253
- * @param attributeMappings - The attribute mappings from the import definition.
1254
- * @param context - The context object containing information about the database, collection, and document.
1255
- * @param item - The item being imported, used for resolving template paths in fileData mappings.
1256
- * @returns The attribute mappings updated with any necessary post-import actions.
1257
- */
1258
1249
  twoWayKey: string;
1259
1250
  onDelete: "setNull" | "cascade" | "restrict";
1260
1251
  side: "parent" | "child";
@@ -1311,6 +1302,7 @@ export declare const CollectionImportDataSchema: z.ZodObject<{
1311
1302
  fieldToSet?: string | undefined;
1312
1303
  targetFieldToMatch?: string | undefined;
1313
1304
  }[] | undefined;
1305
+ createUsers?: boolean | null | undefined;
1314
1306
  updateMapping?: {
1315
1307
  targetField: string;
1316
1308
  originalIdField: string;
@@ -1357,6 +1349,7 @@ export declare const CollectionImportDataSchema: z.ZodObject<{
1357
1349
  fieldToSet?: string | undefined;
1358
1350
  targetFieldToMatch?: string | undefined;
1359
1351
  }[] | undefined;
1352
+ createUsers?: boolean | null | undefined;
1360
1353
  updateMapping?: {
1361
1354
  targetField: string;
1362
1355
  originalIdField: string;
@@ -1510,6 +1503,7 @@ export declare const CollectionImportDataSchema: z.ZodObject<{
1510
1503
  fieldToSet?: string | undefined;
1511
1504
  targetFieldToMatch?: string | undefined;
1512
1505
  }[] | undefined;
1506
+ createUsers?: boolean | null | undefined;
1513
1507
  updateMapping?: {
1514
1508
  targetField: string;
1515
1509
  originalIdField: string;
@@ -1561,6 +1555,7 @@ export declare class DataLoader {
1561
1555
  fieldToSet?: string | undefined;
1562
1556
  targetFieldToMatch?: string | undefined;
1563
1557
  }[] | undefined;
1558
+ createUsers?: boolean | null | undefined;
1564
1559
  updateMapping?: {
1565
1560
  targetField: string;
1566
1561
  originalIdField: string;
@@ -1655,16 +1650,6 @@ export declare class DataLoader {
1655
1650
  relatedCollection: string;
1656
1651
  relationType: "oneToMany" | "manyToOne" | "oneToOne" | "manyToMany";
1657
1652
  twoWay: boolean;
1658
- /**
1659
- * Generates attribute mappings with post-import actions based on the provided attribute mappings.
1660
- * This method checks each mapping for a fileData attribute and adds a post-import action to create a file
1661
- * and update the field with the file's ID if necessary.
1662
- *
1663
- * @param attributeMappings - The attribute mappings from the import definition.
1664
- * @param context - The context object containing information about the database, collection, and document.
1665
- * @param item - The item being imported, used for resolving template paths in fileData mappings.
1666
- * @returns The attribute mappings updated with any necessary post-import actions.
1667
- */
1668
1653
  twoWayKey: string;
1669
1654
  onDelete: "setNull" | "cascade" | "restrict";
1670
1655
  side: "parent" | "child";
@@ -1721,6 +1706,7 @@ export declare class DataLoader {
1721
1706
  fieldToSet?: string | undefined;
1722
1707
  targetFieldToMatch?: string | undefined;
1723
1708
  }[] | undefined;
1709
+ createUsers?: boolean | null | undefined;
1724
1710
  updateMapping?: {
1725
1711
  targetField: string;
1726
1712
  originalIdField: string;
@@ -1737,6 +1723,7 @@ export declare class DataLoader {
1737
1723
  private mergedUserMap;
1738
1724
  private emailToUserIdMap;
1739
1725
  private phoneToUserIdMap;
1726
+ private userIdSet;
1740
1727
  userExistsMap: Map<string, boolean>;
1741
1728
  private shouldWriteFile;
1742
1729
  constructor(appwriteFolderPath: string, importDataActions: ImportDataActions, database: Databases, config: AppwriteConfig, shouldWriteFile?: boolean);
@@ -45,6 +45,7 @@ export class DataLoader {
45
45
  // Maps to hold email and phone to user ID mappings for unique-ness in User Accounts
46
46
  emailToUserIdMap = new Map();
47
47
  phoneToUserIdMap = new Map();
48
+ userIdSet = new Set();
48
49
  userExistsMap = new Map();
49
50
  shouldWriteFile = false;
50
51
  // Constructor to initialize the DataLoader with necessary configurations
@@ -173,6 +174,7 @@ export class DataLoader {
173
174
  let newId = ID.unique();
174
175
  let condition = this.checkMapValuesForId(newId, collectionName) ||
175
176
  this.userExistsMap.has(newId) ||
177
+ this.userIdSet.has(newId) ||
176
178
  this.importMap
177
179
  .get(this.getCollectionKey("users"))
178
180
  ?.data.some((user) => user.finalData.docId === newId || user.finalData.userId === newId);
@@ -181,6 +183,7 @@ export class DataLoader {
181
183
  condition =
182
184
  this.checkMapValuesForId(newId, collectionName) ||
183
185
  this.userExistsMap.has(newId) ||
186
+ this.userIdSet.has(newId) ||
184
187
  this.importMap
185
188
  .get(this.getCollectionKey("users"))
186
189
  ?.data.some((user) => user.finalData.docId === newId || user.finalData.userId === newId);
@@ -260,12 +263,13 @@ export class DataLoader {
260
263
  // Iterate over the users and setup our maps ahead of time for email and phone
261
264
  for (const user of allUsers) {
262
265
  if (user.email) {
263
- this.emailToUserIdMap.set(user.email, user.$id);
266
+ this.emailToUserIdMap.set(user.email.toLowerCase(), user.$id);
264
267
  }
265
268
  if (user.phone) {
266
269
  this.phoneToUserIdMap.set(user.phone, user.$id);
267
270
  }
268
271
  this.userExistsMap.set(user.$id, true);
272
+ this.userIdSet.add(user.$id);
269
273
  let importData = this.importMap.get(this.getCollectionKey("users"));
270
274
  if (!importData) {
271
275
  importData = {
@@ -275,11 +279,13 @@ export class DataLoader {
275
279
  importData.data.push({
276
280
  finalData: {
277
281
  ...user,
282
+ email: user.email?.toLowerCase(),
278
283
  userId: user.$id,
279
284
  docId: user.$id,
280
285
  },
281
286
  context: {
282
287
  ...user,
288
+ email: user.email?.toLowerCase(),
283
289
  userId: user.$id,
284
290
  docId: user.$id,
285
291
  },
@@ -320,7 +326,7 @@ export class DataLoader {
320
326
  const createDefs = collection.importDefs.filter((def) => def.type === "create" || !def.type);
321
327
  const updateDefs = collection.importDefs.filter((def) => def.type === "update");
322
328
  for (const createDef of createDefs) {
323
- if (!isUsersCollection) {
329
+ if (!isUsersCollection || !createDef.createUsers) {
324
330
  await this.prepareCreateData(db, collection, createDef);
325
331
  }
326
332
  else {
@@ -464,7 +470,9 @@ export class DataLoader {
464
470
  const currentDataFiltered = getCurrentDataFiltered(collectionData.data[i]);
465
471
  // Log and skip if no matching data found
466
472
  if (!foundData.length) {
467
- console.log(`No data found for collection ${collectionConfig.name}:\nTarget collection: ${targetCollectionKey}\nValue to match: ${valueToMatch}\nField to set: ${fieldToSetKey}\nTarget field to match: ${targetFieldKey}\nTarget field value: ${idMapping.targetField}`);
473
+ // console.log(
474
+ // `No data found for collection ${collectionConfig.name}: - Target collection: ${targetCollectionKey} - Value to match: ${valueToMatch} - Field to set: ${fieldToSetKey} - Target field to match: ${targetFieldKey} - Target field value: ${idMapping.targetField}`
475
+ // );
468
476
  logger.info(`No data found for collection: ${targetCollectionKey} with value: ${valueToMatch} for field: ${fieldToSetKey} -- idMapping: ${JSON.stringify(idMapping, null, 2)}`);
469
477
  continue;
470
478
  }
@@ -581,14 +589,18 @@ export class DataLoader {
581
589
  * @returns The transformed item with user-specific keys removed.
582
590
  */
583
591
  prepareUserData(item, attributeMappings, primaryKeyField, newId) {
584
- if (this.userExistsMap.has(newId) ||
592
+ if (this.userIdSet.has(newId) ||
593
+ this.userExistsMap.has(newId) ||
585
594
  Array.from(this.emailToUserIdMap.values()).includes(newId) ||
586
595
  Array.from(this.phoneToUserIdMap.values()).includes(newId)) {
587
596
  newId = this.getTrueUniqueId(this.getCollectionKey("users"));
588
597
  }
589
598
  let transformedItem = this.transformData(item, attributeMappings);
590
- const userData = AuthUserCreateSchema.safeParse(transformedItem);
591
- if (!userData.success || !(userData.data.email && userData.data.phone)) {
599
+ let userData = AuthUserCreateSchema.safeParse(transformedItem);
600
+ if (userData.data?.email) {
601
+ userData.data.email = userData.data.email.toLowerCase();
602
+ }
603
+ if (!userData.success || !(userData.data.email || userData.data.phone)) {
592
604
  logger.error(`Invalid user data: ${JSON.stringify(userData.error?.errors, undefined, 2)} or missing email/phone`);
593
605
  const userKeys = ["email", "phone", "name", "labels", "prefs"];
594
606
  userKeys.forEach((key) => {
@@ -605,7 +617,7 @@ export class DataLoader {
605
617
  },
606
618
  };
607
619
  }
608
- const email = userData.data.email;
620
+ const email = userData.data.email?.toLowerCase();
609
621
  const phone = userData.data.phone;
610
622
  let existingId;
611
623
  // Check for duplicate email and phone
@@ -653,7 +665,12 @@ export class DataLoader {
653
665
  if (userFound) {
654
666
  userFound.finalData.userId = existingId;
655
667
  userFound.finalData.docId = existingId;
656
- this.userExistsMap.set(existingId, true);
668
+ this.userIdSet.add(existingId);
669
+ transformedItem = {
670
+ ...transformedItem,
671
+ userId: existingId,
672
+ docId: existingId,
673
+ };
657
674
  }
658
675
  const userKeys = ["email", "phone", "name", "labels", "prefs"];
659
676
  userKeys.forEach((key) => {
@@ -688,6 +705,7 @@ export class DataLoader {
688
705
  this.importMap.set(this.getCollectionKey("users"), {
689
706
  data: [...(usersMap?.data || []), userDataToAdd],
690
707
  });
708
+ this.userIdSet.add(existingId);
691
709
  return {
692
710
  transformedItem,
693
711
  existingId,
@@ -962,17 +980,14 @@ export class DataLoader {
962
980
  item[importDef.updateMapping.originalIdField] ||
963
981
  transformedData[importDef.updateMapping.originalIdField];
964
982
  if (oldId) {
965
- itemDataToUpdate = currentData?.data.find(({ context, rawData, finalData }) => {
983
+ itemDataToUpdate = currentData?.data.find(({ context, finalData }) => {
966
984
  const targetField = importDef.updateMapping.targetField ??
967
985
  importDef.updateMapping.originalIdField;
968
986
  return (`${context[targetField]}` === `${oldId}` ||
969
- `${rawData[targetField]}` === `${oldId}` ||
970
987
  `${finalData[targetField]}` === `${oldId}`);
971
988
  });
972
989
  if (itemDataToUpdate) {
973
- newId =
974
- itemDataToUpdate.finalData.docId ||
975
- itemDataToUpdate.context.docId;
990
+ newId = itemDataToUpdate.context.docId;
976
991
  }
977
992
  }
978
993
  }
@@ -1020,10 +1035,10 @@ export class DataLoader {
1020
1035
  }
1021
1036
  transformedData = this.mergeObjects(itemDataToUpdate.finalData, transformedData);
1022
1037
  // Create a context object for the item, including the new ID and transformed data
1023
- let context = this.createContext(db, collection, item, newId);
1038
+ let context = itemDataToUpdate.context;
1024
1039
  context = this.mergeObjects(context, transformedData);
1025
1040
  // Validate the item before proceeding
1026
- const isValid = await this.importDataActions.validateItem(item, importDef.attributeMappings, context);
1041
+ const isValid = this.importDataActions.validateItem(item, importDef.attributeMappings, context);
1027
1042
  if (!isValid) {
1028
1043
  logger.info(`Skipping item: ${JSON.stringify(item, null, 2)} because it's invalid`);
1029
1044
  continue;
@@ -1038,7 +1053,21 @@ export class DataLoader {
1038
1053
  if (itemDataToUpdate) {
1039
1054
  itemDataToUpdate.finalData = this.mergeObjects(itemDataToUpdate.finalData, transformedData);
1040
1055
  itemDataToUpdate.context = context;
1041
- itemDataToUpdate.importDef = newImportDef;
1056
+ // Merge existing importDef with new importDef, focusing only on postImportActions
1057
+ itemDataToUpdate.importDef = {
1058
+ ...itemDataToUpdate.importDef,
1059
+ attributeMappings: itemDataToUpdate.importDef?.attributeMappings.map((attrMapping, index) => ({
1060
+ ...attrMapping,
1061
+ postImportActions: [
1062
+ ...(attrMapping.postImportActions || []),
1063
+ ...(newImportDef.attributeMappings[index]
1064
+ ?.postImportActions || []),
1065
+ ],
1066
+ })) || [],
1067
+ };
1068
+ if (collection.name.toLowerCase() === "councils") {
1069
+ console.log(`Mappings in update councils: ${JSON.stringify(itemDataToUpdate.importDef.attributeMappings, null, 2)}`);
1070
+ }
1042
1071
  }
1043
1072
  else {
1044
1073
  currentData.data.push({
@@ -58,10 +58,10 @@ export class UsersController {
58
58
  Query.cursorAfter(lastDocumentId),
59
59
  ]));
60
60
  allUsers.push(...moreUsers.users);
61
- lastDocumentId = moreUsers.users[moreUsers.users.length - 1].$id;
62
61
  if (moreUsers.users.length < 200) {
63
62
  break;
64
63
  }
64
+ lastDocumentId = moreUsers.users[moreUsers.users.length - 1].$id;
65
65
  }
66
66
  }
67
67
  else {
@@ -99,9 +99,9 @@ export class UsersController {
99
99
  }
100
100
  }
101
101
  if (e instanceof Error) {
102
- logger.error("FAILED CREATING USER: ", e.message);
102
+ logger.error("FAILED CREATING USER: ", e.message, item);
103
103
  }
104
- console.log("FAILED CREATING USER: ", e);
104
+ console.log("FAILED CREATING USER: ", e, item);
105
105
  throw e;
106
106
  }
107
107
  }
package/package.json CHANGED
@@ -1,54 +1,55 @@
1
- {
2
- "name": "appwrite-utils-cli",
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.65",
5
- "main": "src/main.ts",
6
- "type": "module",
7
- "repository": {
8
- "type": "git",
9
- "url": "https://github.com/zachhandley/AppwriteUtils"
10
- },
11
- "author": "Zach Handley <zach@blackleafdigital.com> (https://zachhandley.com)",
12
- "keywords": [
13
- "appwrite",
14
- "cli",
15
- "utils",
16
- "migrations",
17
- "data",
18
- "database",
19
- "import",
20
- "migration",
21
- "utility"
22
- ],
23
- "bin": {
24
- "appwrite-init": "./dist/init.js",
25
- "appwrite-migrate": "./dist/main.js"
26
- },
27
- "scripts": {
28
- "build": "bun run tsc",
29
- "init": "tsx --no-cache src/init.ts",
30
- "migrate": "tsx --no-cache src/main.ts",
31
- "deploy": "bun run build && npm publish --access public",
32
- "postinstall": "echo 'This package is intended for CLI use only and should not be added as a dependency in other projects.'"
33
- },
34
- "dependencies": {
35
- "@types/inquirer": "^9.0.7",
36
- "appwrite-utils": "^0.3.2",
37
- "commander": "^12.0.0",
38
- "inquirer": "^9.2.20",
39
- "js-yaml": "^4.1.0",
40
- "lodash": "^4.17.21",
41
- "luxon": "^3.4.4",
42
- "nanostores": "^0.10.3",
43
- "node-appwrite": "^12.0.1",
44
- "tsx": "^4.9.3",
45
- "winston": "^3.13.0",
46
- "zod": "^3.22.4"
47
- },
48
- "devDependencies": {
49
- "@types/js-yaml": "^4.0.9",
50
- "@types/lodash": "^4.17.0",
51
- "@types/luxon": "^3.4.2",
52
- "typescript": "^5.0.0"
53
- }
54
- }
1
+ {
2
+ "name": "appwrite-utils-cli",
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.67",
5
+ "main": "src/main.ts",
6
+ "type": "module",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/zachhandley/AppwriteUtils"
10
+ },
11
+ "author": "Zach Handley <zach@blackleafdigital.com> (https://zachhandley.com)",
12
+ "keywords": [
13
+ "appwrite",
14
+ "cli",
15
+ "utils",
16
+ "migrations",
17
+ "data",
18
+ "database",
19
+ "import",
20
+ "migration",
21
+ "utility"
22
+ ],
23
+ "bin": {
24
+ "appwrite-init": "./dist/init.js",
25
+ "appwrite-migrate": "./dist/main.js"
26
+ },
27
+ "scripts": {
28
+ "build": "bun run tsc",
29
+ "init": "tsx --no-cache src/init.ts",
30
+ "migrate": "tsx --no-cache src/main.ts",
31
+ "deploy": "bun run build && npm publish --access public",
32
+ "postinstall": "echo 'This package is intended for CLI use only and should not be added as a dependency in other projects.'"
33
+ },
34
+ "dependencies": {
35
+ "@types/inquirer": "^9.0.7",
36
+ "appwrite-utils": "^0.3.3",
37
+ "commander": "^12.0.0",
38
+ "inquirer": "^9.2.20",
39
+ "js-yaml": "^4.1.0",
40
+ "lodash": "^4.17.21",
41
+ "luxon": "^3.4.4",
42
+ "nanostores": "^0.10.3",
43
+ "node-appwrite": "^12.0.1",
44
+ "tsx": "^4.9.3",
45
+ "ulid": "^2.3.0",
46
+ "winston": "^3.13.0",
47
+ "zod": "^3.22.4"
48
+ },
49
+ "devDependencies": {
50
+ "@types/js-yaml": "^4.0.9",
51
+ "@types/lodash": "^4.17.0",
52
+ "@types/luxon": "^3.4.2",
53
+ "typescript": "^5.0.0"
54
+ }
55
+ }
@@ -64,6 +64,7 @@ export class DataLoader {
64
64
  // Maps to hold email and phone to user ID mappings for unique-ness in User Accounts
65
65
  private emailToUserIdMap = new Map<string, string>();
66
66
  private phoneToUserIdMap = new Map<string, string>();
67
+ private userIdSet = new Set<string>();
67
68
  userExistsMap = new Map<string, boolean>();
68
69
  private shouldWriteFile = false;
69
70
 
@@ -212,6 +213,7 @@ export class DataLoader {
212
213
  let condition =
213
214
  this.checkMapValuesForId(newId, collectionName) ||
214
215
  this.userExistsMap.has(newId) ||
216
+ this.userIdSet.has(newId) ||
215
217
  this.importMap
216
218
  .get(this.getCollectionKey("users"))
217
219
  ?.data.some(
@@ -223,6 +225,7 @@ export class DataLoader {
223
225
  condition =
224
226
  this.checkMapValuesForId(newId, collectionName) ||
225
227
  this.userExistsMap.has(newId) ||
228
+ this.userIdSet.has(newId) ||
226
229
  this.importMap
227
230
  .get(this.getCollectionKey("users"))
228
231
  ?.data.some(
@@ -330,12 +333,13 @@ export class DataLoader {
330
333
  // Iterate over the users and setup our maps ahead of time for email and phone
331
334
  for (const user of allUsers) {
332
335
  if (user.email) {
333
- this.emailToUserIdMap.set(user.email, user.$id);
336
+ this.emailToUserIdMap.set(user.email.toLowerCase(), user.$id);
334
337
  }
335
338
  if (user.phone) {
336
339
  this.phoneToUserIdMap.set(user.phone, user.$id);
337
340
  }
338
341
  this.userExistsMap.set(user.$id, true);
342
+ this.userIdSet.add(user.$id);
339
343
  let importData = this.importMap.get(this.getCollectionKey("users"));
340
344
  if (!importData) {
341
345
  importData = {
@@ -345,11 +349,13 @@ export class DataLoader {
345
349
  importData.data.push({
346
350
  finalData: {
347
351
  ...user,
352
+ email: user.email?.toLowerCase(),
348
353
  userId: user.$id,
349
354
  docId: user.$id,
350
355
  },
351
356
  context: {
352
357
  ...user,
358
+ email: user.email?.toLowerCase(),
353
359
  userId: user.$id,
354
360
  docId: user.$id,
355
361
  },
@@ -398,7 +404,7 @@ export class DataLoader {
398
404
  (def: ImportDef) => def.type === "update"
399
405
  );
400
406
  for (const createDef of createDefs) {
401
- if (!isUsersCollection) {
407
+ if (!isUsersCollection || !createDef.createUsers) {
402
408
  await this.prepareCreateData(db, collection, createDef);
403
409
  } else {
404
410
  // Special handling for users collection if needed
@@ -592,9 +598,9 @@ export class DataLoader {
592
598
  );
593
599
  // Log and skip if no matching data found
594
600
  if (!foundData.length) {
595
- console.log(
596
- `No data found for collection ${collectionConfig.name}:\nTarget collection: ${targetCollectionKey}\nValue to match: ${valueToMatch}\nField to set: ${fieldToSetKey}\nTarget field to match: ${targetFieldKey}\nTarget field value: ${idMapping.targetField}`
597
- );
601
+ // console.log(
602
+ // `No data found for collection ${collectionConfig.name}: - Target collection: ${targetCollectionKey} - Value to match: ${valueToMatch} - Field to set: ${fieldToSetKey} - Target field to match: ${targetFieldKey} - Target field value: ${idMapping.targetField}`
603
+ // );
598
604
  logger.info(
599
605
  `No data found for collection: ${targetCollectionKey} with value: ${valueToMatch} for field: ${fieldToSetKey} -- idMapping: ${JSON.stringify(
600
606
  idMapping,
@@ -761,6 +767,7 @@ export class DataLoader {
761
767
  };
762
768
  } {
763
769
  if (
770
+ this.userIdSet.has(newId) ||
764
771
  this.userExistsMap.has(newId) ||
765
772
  Array.from(this.emailToUserIdMap.values()).includes(newId) ||
766
773
  Array.from(this.phoneToUserIdMap.values()).includes(newId)
@@ -768,8 +775,11 @@ export class DataLoader {
768
775
  newId = this.getTrueUniqueId(this.getCollectionKey("users"));
769
776
  }
770
777
  let transformedItem = this.transformData(item, attributeMappings);
771
- const userData = AuthUserCreateSchema.safeParse(transformedItem);
772
- if (!userData.success || !(userData.data.email && userData.data.phone)) {
778
+ let userData = AuthUserCreateSchema.safeParse(transformedItem);
779
+ if (userData.data?.email) {
780
+ userData.data.email = userData.data.email.toLowerCase();
781
+ }
782
+ if (!userData.success || !(userData.data.email || userData.data.phone)) {
773
783
  logger.error(
774
784
  `Invalid user data: ${JSON.stringify(
775
785
  userData.error?.errors,
@@ -793,7 +803,7 @@ export class DataLoader {
793
803
  },
794
804
  };
795
805
  }
796
- const email = userData.data.email;
806
+ const email = userData.data.email?.toLowerCase();
797
807
  const phone = userData.data.phone;
798
808
  let existingId: string | undefined;
799
809
 
@@ -836,7 +846,12 @@ export class DataLoader {
836
846
  if (userFound) {
837
847
  userFound.finalData.userId = existingId;
838
848
  userFound.finalData.docId = existingId;
839
- this.userExistsMap.set(existingId, true);
849
+ this.userIdSet.add(existingId);
850
+ transformedItem = {
851
+ ...transformedItem,
852
+ userId: existingId,
853
+ docId: existingId,
854
+ };
840
855
  }
841
856
 
842
857
  const userKeys = ["email", "phone", "name", "labels", "prefs"];
@@ -873,6 +888,7 @@ export class DataLoader {
873
888
  this.importMap.set(this.getCollectionKey("users"), {
874
889
  data: [...(usersMap?.data || []), userDataToAdd],
875
890
  });
891
+ this.userIdSet.add(existingId);
876
892
 
877
893
  return {
878
894
  transformedItem,
@@ -1281,23 +1297,20 @@ export class DataLoader {
1281
1297
  transformedData[importDef.updateMapping.originalIdField];
1282
1298
  if (oldId) {
1283
1299
  itemDataToUpdate = currentData?.data.find(
1284
- ({ context, rawData, finalData }) => {
1300
+ ({ context, finalData }) => {
1285
1301
  const targetField =
1286
1302
  importDef.updateMapping!.targetField ??
1287
1303
  importDef.updateMapping!.originalIdField;
1288
1304
 
1289
1305
  return (
1290
1306
  `${context[targetField]}` === `${oldId}` ||
1291
- `${rawData[targetField]}` === `${oldId}` ||
1292
1307
  `${finalData[targetField]}` === `${oldId}`
1293
1308
  );
1294
1309
  }
1295
1310
  );
1296
1311
 
1297
1312
  if (itemDataToUpdate) {
1298
- newId =
1299
- itemDataToUpdate.finalData.docId ||
1300
- itemDataToUpdate.context.docId;
1313
+ newId = itemDataToUpdate.context.docId;
1301
1314
  }
1302
1315
  }
1303
1316
  }
@@ -1391,11 +1404,11 @@ export class DataLoader {
1391
1404
  );
1392
1405
 
1393
1406
  // Create a context object for the item, including the new ID and transformed data
1394
- let context = this.createContext(db, collection, item, newId);
1407
+ let context = itemDataToUpdate.context;
1395
1408
  context = this.mergeObjects(context, transformedData);
1396
1409
 
1397
1410
  // Validate the item before proceeding
1398
- const isValid = await this.importDataActions.validateItem(
1411
+ const isValid = this.importDataActions.validateItem(
1399
1412
  item,
1400
1413
  importDef.attributeMappings,
1401
1414
  context
@@ -1427,7 +1440,31 @@ export class DataLoader {
1427
1440
  transformedData
1428
1441
  );
1429
1442
  itemDataToUpdate.context = context;
1430
- itemDataToUpdate.importDef = newImportDef;
1443
+ // Merge existing importDef with new importDef, focusing only on postImportActions
1444
+ itemDataToUpdate.importDef = {
1445
+ ...itemDataToUpdate.importDef,
1446
+ attributeMappings:
1447
+ itemDataToUpdate.importDef?.attributeMappings.map(
1448
+ (attrMapping, index) => ({
1449
+ ...attrMapping,
1450
+ postImportActions: [
1451
+ ...(attrMapping.postImportActions || []),
1452
+ ...(newImportDef.attributeMappings[index]
1453
+ ?.postImportActions || []),
1454
+ ],
1455
+ })
1456
+ ) || [],
1457
+ } as ImportDef;
1458
+
1459
+ if (collection.name.toLowerCase() === "councils") {
1460
+ console.log(
1461
+ `Mappings in update councils: ${JSON.stringify(
1462
+ itemDataToUpdate.importDef.attributeMappings,
1463
+ null,
1464
+ 2
1465
+ )}`
1466
+ );
1467
+ }
1431
1468
  } else {
1432
1469
  currentData!.data.push({
1433
1470
  rawData: item,
@@ -89,10 +89,10 @@ export class UsersController {
89
89
  ])
90
90
  );
91
91
  allUsers.push(...moreUsers.users);
92
- lastDocumentId = moreUsers.users[moreUsers.users.length - 1].$id;
93
92
  if (moreUsers.users.length < 200) {
94
93
  break;
95
94
  }
95
+ lastDocumentId = moreUsers.users[moreUsers.users.length - 1].$id;
96
96
  }
97
97
  } else {
98
98
  allUsers.push(...users.users);
@@ -141,9 +141,9 @@ export class UsersController {
141
141
  }
142
142
  }
143
143
  if (e instanceof Error) {
144
- logger.error("FAILED CREATING USER: ", e.message);
144
+ logger.error("FAILED CREATING USER: ", e.message, item);
145
145
  }
146
- console.log("FAILED CREATING USER: ", e);
146
+ console.log("FAILED CREATING USER: ", e, item);
147
147
  throw e;
148
148
  }
149
149
  }