appwrite-utils-cli 0.0.32 → 0.0.34

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.
@@ -124,7 +124,7 @@ export class DataLoader {
124
124
  result[key] = updateValue;
125
125
  }
126
126
  // If the update value is nullish, keep the original value unless it doesn't exist
127
- else if (sourceValue === undefined) {
127
+ else if (sourceValue === undefined || sourceValue === null) {
128
128
  result[key] = updateValue;
129
129
  }
130
130
  });
@@ -298,6 +298,10 @@ export class DataLoader {
298
298
  let isUsersCollection =
299
299
  this.getCollectionKey(this.config.usersCollectionName) ===
300
300
  this.getCollectionKey(collection.name);
301
+ const collectionDefs = collection.importDefs;
302
+ if (!collectionDefs || !collectionDefs.length) {
303
+ continue;
304
+ }
301
305
  // Process create and update definitions for the collection
302
306
  const createDefs = collection.importDefs.filter(
303
307
  (def: ImportDef) => def.type === "create" || !def.type
@@ -331,8 +335,8 @@ export class DataLoader {
331
335
  }
332
336
  }
333
337
  console.log("Running update references");
334
- await this.dealWithMergedUsers();
335
- await this.updateOldReferencesForNew();
338
+ this.dealWithMergedUsers();
339
+ this.updateOldReferencesForNew();
336
340
  console.log("Done running update references");
337
341
  }
338
342
  // for (const collection of this.config.collections) {
@@ -346,7 +350,7 @@ export class DataLoader {
346
350
  }
347
351
  }
348
352
 
349
- async dealWithMergedUsers() {
353
+ dealWithMergedUsers() {
350
354
  const usersCollectionKey = this.getCollectionKey(
351
355
  this.config.usersCollectionName
352
356
  );
@@ -357,7 +361,11 @@ export class DataLoader {
357
361
  // Collect primary key fields from the users collection definitions
358
362
  this.config.collections.forEach((collection) => {
359
363
  if (this.getCollectionKey(collection.name) === usersCollectionKey) {
360
- collection.importDefs.forEach((importDef) => {
364
+ const collectionImportDefs = collection.importDefs;
365
+ if (!collectionImportDefs || !collectionImportDefs.length) {
366
+ return;
367
+ }
368
+ collectionImportDefs.forEach((importDef) => {
361
369
  if (importDef.primaryKeyField) {
362
370
  usersCollectionPrimaryKeyFields.add(importDef.primaryKeyField);
363
371
  }
@@ -371,23 +379,29 @@ export class DataLoader {
371
379
  this.getCollectionKey(collection.name)
372
380
  );
373
381
  if (!collectionData || !collectionData.data) return;
374
-
375
- collection.importDefs.forEach((importDef) => {
382
+ const collectionImportDefs = collection.importDefs;
383
+ if (!collectionImportDefs || !collectionImportDefs.length) {
384
+ return;
385
+ }
386
+ collectionImportDefs.forEach((importDef) => {
376
387
  importDef.idMappings?.forEach((idMapping) => {
377
388
  if (
378
389
  this.getCollectionKey(idMapping.targetCollection) ===
379
390
  usersCollectionKey
380
391
  ) {
381
- if (usersCollectionPrimaryKeyFields.has(idMapping.targetField)) {
392
+ const targetFieldKey =
393
+ idMapping.targetFieldToMatch || idMapping.targetField;
394
+ if (usersCollectionPrimaryKeyFields.has(targetFieldKey)) {
382
395
  // Process each item in the collection
383
396
  collectionData.data.forEach((item) => {
384
397
  const oldId = item.context[idMapping.sourceField];
385
- const newId = this.mergedUserMap.get(oldId);
398
+ const newId = this.mergedUserMap.get(`${oldId}`);
386
399
 
387
400
  if (newId) {
388
401
  // Update context to use new user ID
389
- item.context[idMapping.fieldToSet || idMapping.targetField] =
390
- newId;
402
+ item.finalData[
403
+ idMapping.fieldToSet || idMapping.sourceField
404
+ ] = newId;
391
405
  }
392
406
  });
393
407
  }
@@ -401,6 +415,7 @@ export class DataLoader {
401
415
  if (!this.config.collections) {
402
416
  return;
403
417
  }
418
+
404
419
  for (const collectionConfig of this.config.collections) {
405
420
  const collectionKey = this.getCollectionKey(collectionConfig.name);
406
421
  const collectionData = this.importMap.get(collectionKey);
@@ -424,29 +439,41 @@ export class DataLoader {
424
439
  );
425
440
  const fieldToSetKey =
426
441
  idMapping.fieldToSet || idMapping.sourceField;
442
+ const targetFieldKey =
443
+ idMapping.targetFieldToMatch || idMapping.targetField;
427
444
  const valueToMatch =
428
445
  collectionData.data[i].context[idMapping.sourceField];
429
446
 
447
+ // Skip if value to match is missing or empty
430
448
  if (!valueToMatch || _.isEmpty(valueToMatch)) continue;
431
449
 
450
+ const isFieldToSetArray = collectionConfig.attributes.find(
451
+ (attribute) => attribute.key === fieldToSetKey
452
+ )?.array;
453
+
432
454
  const targetCollectionData =
433
455
  this.importMap.get(targetCollectionKey);
434
456
  if (!targetCollectionData || !targetCollectionData.data)
435
457
  continue;
436
458
 
437
- const foundData = targetCollectionData.data.filter((data) => {
438
- const targetValue = data.context[idMapping.targetField];
439
- const isMatch = `${targetValue}` === `${valueToMatch}`;
440
- // Debugging output to understand what's being compared
441
- logger.warn(
442
- `Comparing target: ${targetValue} with match: ${valueToMatch} - Result: ${isMatch}`
443
- );
444
- return isMatch;
445
- });
459
+ // Find matching data in the target collection
460
+ const foundData = targetCollectionData.data.filter(
461
+ ({ context }) => {
462
+ const targetValue = context[targetFieldKey];
463
+ const isMatch = `${targetValue}` === `${valueToMatch}`;
464
+ // Ensure the targetValue is defined and not null
465
+ return (
466
+ isMatch &&
467
+ targetValue !== undefined &&
468
+ targetValue !== null
469
+ );
470
+ }
471
+ );
446
472
 
473
+ // Log and skip if no matching data found
447
474
  if (!foundData.length) {
448
475
  console.log(
449
- `No data found for collection: ${targetCollectionKey} with value: ${valueToMatch} for field: ${fieldToSetKey}`
476
+ `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}`
450
477
  );
451
478
  logger.error(
452
479
  `No data found for collection: ${targetCollectionKey} with value: ${valueToMatch} for field: ${fieldToSetKey} -- idMapping: ${JSON.stringify(
@@ -460,143 +487,72 @@ export class DataLoader {
460
487
 
461
488
  needsUpdate = true;
462
489
 
463
- // Properly handle arrays and non-arrays
464
- if (
465
- Array.isArray(collectionData.data[i].finalData[fieldToSetKey])
466
- ) {
467
- collectionData.data[i].finalData[fieldToSetKey] =
468
- foundData.map((data) => data.finalData);
469
- } else {
470
- collectionData.data[i].finalData[fieldToSetKey] =
471
- foundData[0].finalData;
472
- }
473
- }
474
- }
475
- }
476
- }
477
- }
478
-
479
- if (needsUpdate) {
480
- this.importMap.set(collectionKey, collectionData);
481
- }
482
- }
483
- }
484
-
485
- async updateReferencesInRelatedCollections() {
486
- if (!this.config.collections) {
487
- return;
488
- }
489
- // Iterate over each collection configuration
490
- for (const collectionConfig of this.config.collections) {
491
- const collectionKey = this.getCollectionKey(collectionConfig.name);
492
- const collectionData = this.importMap.get(collectionKey);
493
-
494
- if (!collectionData || !collectionData.data) continue;
495
-
496
- console.log(
497
- `Updating references for collection: ${collectionConfig.name}`
498
- );
490
+ const getCurrentDataFiltered = (currentData: any) => {
491
+ if (Array.isArray(currentData.finalData[fieldToSetKey])) {
492
+ return currentData.finalData[fieldToSetKey].filter(
493
+ (data: any) => `${data}` !== `${valueToMatch}`
494
+ );
495
+ }
496
+ return currentData.finalData[fieldToSetKey];
497
+ };
499
498
 
500
- // Iterate over each data item in the current collection
501
- for (const item of collectionData.data) {
502
- let needsUpdate = false;
499
+ // Get the current data to be updated
500
+ const currentDataFiltered = getCurrentDataFiltered(
501
+ collectionData.data[i]
502
+ );
503
503
 
504
- // Check if the current collection has import definitions with idMappings
505
- if (collectionConfig.importDefs) {
506
- for (const importDef of collectionConfig.importDefs) {
507
- if (importDef.idMappings) {
508
- // Iterate over each idMapping defined for the current import definition
509
- for (const idMapping of importDef.idMappings) {
510
- const oldIds = Array.isArray(
511
- item.context[idMapping.sourceField]
512
- )
513
- ? item.context[idMapping.sourceField]
514
- : [item.context[idMapping.sourceField]];
515
- const resolvedNewIds: string[] = [];
516
-
517
- oldIds.forEach((oldId: any) => {
518
- // Attempt to find a new ID for the old ID
519
- let newIdForOldId = this.findNewIdForOldId(
520
- oldId,
521
- idMapping,
522
- importDef
523
- );
504
+ // Extract the new data to set
505
+ const newData = foundData.map(
506
+ (data) => data.context[idMapping.targetField]
507
+ );
524
508
 
525
- if (
526
- newIdForOldId &&
527
- !resolvedNewIds.includes(newIdForOldId)
528
- ) {
529
- resolvedNewIds.push(newIdForOldId);
509
+ // Handle cases where current data is an array
510
+ if (isFieldToSetArray) {
511
+ if (!currentDataFiltered) {
512
+ // Set new data if current data is undefined
513
+ collectionData.data[i].finalData[fieldToSetKey] =
514
+ Array.isArray(newData) ? newData : [newData];
530
515
  } else {
531
- logger.error(
532
- `No new ID found for old ID ${oldId} in collection ${collectionConfig.name}`
533
- );
516
+ // Merge arrays if new data is non-empty array and filter for uniqueness
517
+ collectionData.data[i].finalData[fieldToSetKey] = [
518
+ ...new Set(
519
+ [...currentDataFiltered, ...newData].filter(
520
+ (value: any) => `${value}` !== `${valueToMatch}`
521
+ )
522
+ ),
523
+ ];
524
+ }
525
+ } else {
526
+ if (!currentDataFiltered) {
527
+ // Set new data if current data is undefined
528
+ collectionData.data[i].finalData[fieldToSetKey] =
529
+ Array.isArray(newData) ? newData[0] : newData;
530
+ } else if (Array.isArray(newData) && newData.length > 0) {
531
+ // Convert current data to array and merge if new data is non-empty array, then filter for uniqueness
532
+ // and take the first value, because it's an array and the attribute is not an array
533
+ collectionData.data[i].finalData[fieldToSetKey] = [
534
+ ...new Set(
535
+ [currentDataFiltered, ...newData].filter(
536
+ (value: any) => `${value}` !== `${valueToMatch}`
537
+ )
538
+ ),
539
+ ].slice(0, 1)[0];
540
+ } else if (!Array.isArray(newData) && newData !== undefined) {
541
+ // Simply update the field if new data is not an array and defined
542
+ collectionData.data[i].finalData[fieldToSetKey] = newData;
534
543
  }
535
- });
536
-
537
- if (resolvedNewIds.length) {
538
- const targetField =
539
- idMapping.fieldToSet || idMapping.targetField;
540
- const isArray = collectionConfig.attributes.some(
541
- (attribute) =>
542
- attribute.key === targetField && attribute.array
543
- );
544
-
545
- // Set the target field based on whether it's an array or single value
546
- item.finalData[targetField] = isArray
547
- ? resolvedNewIds
548
- : resolvedNewIds[0];
549
- needsUpdate = true;
550
544
  }
551
545
  }
552
546
  }
553
547
  }
554
548
  }
555
-
556
- // Update the importMap if changes were made to the item
557
- if (needsUpdate) {
558
- this.importMap.set(collectionKey, collectionData);
559
- logger.info(
560
- `Updated item: ${JSON.stringify(item.finalData, undefined, 2)}`
561
- );
562
- }
563
- }
564
- }
565
- }
566
-
567
- findNewIdForOldId(oldId: string, idMapping: IdMapping, importDef: ImportDef) {
568
- // First, check if this ID mapping is related to the users collection.
569
- const targetCollectionKey = this.getCollectionKey(
570
- idMapping.targetCollection
571
- );
572
- const isUsersCollection =
573
- targetCollectionKey ===
574
- this.getCollectionKey(this.config.usersCollectionName);
575
-
576
- // If handling users, check the mergedUserMap for any existing new ID.
577
- if (isUsersCollection) {
578
- for (const [newUserId, oldIds] of this.mergedUserMap.entries()) {
579
- if (oldIds.includes(oldId)) {
580
- return newUserId;
581
- }
582
549
  }
583
- }
584
550
 
585
- // If not a user or no merged ID found, check the regular ID mapping from old to new.
586
- const targetCollectionData = this.importMap.get(targetCollectionKey);
587
- if (targetCollectionData) {
588
- const foundEntry = targetCollectionData.data.find(
589
- (entry) => entry.context[importDef.primaryKeyField] === oldId
590
- );
591
- if (foundEntry) {
592
- return foundEntry.context.docId; // Assuming `docId` stores the new ID after import
551
+ // Update the import map if any changes were made
552
+ if (needsUpdate) {
553
+ this.importMap.set(collectionKey, collectionData);
593
554
  }
594
555
  }
595
-
596
- logger.error(
597
- `No corresponding new ID found for ${oldId} in ${targetCollectionKey}`
598
- );
599
- return null; // Return null if no new ID is found
600
556
  }
601
557
 
602
558
  private writeMapsToJsonFile() {
@@ -662,7 +618,6 @@ export class DataLoader {
662
618
  primaryKeyField: string,
663
619
  newId: string
664
620
  ): Promise<any> {
665
- // Transform the item data based on the attribute mappings
666
621
  let transformedItem = this.transformData(item, attributeMappings);
667
622
  const userData = AuthUserCreateSchema.safeParse(transformedItem);
668
623
  if (!userData.success) {
@@ -679,48 +634,64 @@ export class DataLoader {
679
634
  const phone = userData.data.phone;
680
635
  let existingId: string | undefined;
681
636
 
682
- // Check for duplicate email and add to emailToUserIdMap if not found
683
- if (email && email.length > 0) {
684
- if (this.emailToUserIdMap.has(email)) {
685
- existingId = this.emailToUserIdMap.get(email);
686
- } else {
687
- this.emailToUserIdMap.set(email, newId);
688
- }
637
+ // Check for duplicate email and phone
638
+ if (email && this.emailToUserIdMap.has(email)) {
639
+ existingId = this.emailToUserIdMap.get(email);
640
+ } else if (phone && this.phoneToUserIdMap.has(phone)) {
641
+ existingId = this.phoneToUserIdMap.get(phone);
642
+ } else {
643
+ if (email) this.emailToUserIdMap.set(email, newId);
644
+ if (phone) this.phoneToUserIdMap.set(phone, newId);
689
645
  }
690
646
 
691
- // Check for duplicate phone and add to phoneToUserIdMap if not found
692
- if (phone && phone.length > 0) {
693
- if (this.phoneToUserIdMap.has(phone)) {
694
- existingId = this.phoneToUserIdMap.get(phone);
695
- } else {
696
- this.phoneToUserIdMap.set(phone, newId);
697
- }
698
- }
699
- if (!existingId) {
700
- existingId = newId;
701
- }
702
-
703
- // If existingId is found, add to mergedUserMap
704
647
  if (existingId) {
705
648
  userData.data.userId = existingId;
706
649
  const mergedUsers = this.mergedUserMap.get(existingId) || [];
707
650
  mergedUsers.push(`${item[primaryKeyField]}`);
708
651
  this.mergedUserMap.set(existingId, mergedUsers);
652
+ const userFound = this.importMap
653
+ .get(this.getCollectionKey("users"))
654
+ ?.data.find((userDataExisting) => {
655
+ let userIdToMatch: string | undefined;
656
+ if (userDataExisting?.finalData?.userId) {
657
+ userIdToMatch = userDataExisting?.finalData?.userId;
658
+ } else if (userDataExisting?.finalData?.docId) {
659
+ userIdToMatch = userDataExisting?.finalData?.docId;
660
+ } else if (userDataExisting?.context?.userId) {
661
+ userIdToMatch = userDataExisting.context.userId;
662
+ } else if (userDataExisting?.context?.docId) {
663
+ userIdToMatch = userDataExisting.context.docId;
664
+ }
665
+ return userIdToMatch === existingId;
666
+ });
667
+ if (userFound) {
668
+ userFound.finalData.userId = existingId;
669
+ }
670
+ return [
671
+ transformedItem,
672
+ existingId,
673
+ {
674
+ rawData: userFound?.rawData,
675
+ finalData: userFound?.finalData,
676
+ },
677
+ ];
678
+ } else {
679
+ existingId = newId;
680
+ userData.data.userId = existingId;
709
681
  }
710
682
 
711
- // Remove user-specific keys from the transformed item
712
683
  const userKeys = ["email", "phone", "name", "labels", "prefs"];
713
684
  userKeys.forEach((key) => {
714
685
  if (transformedItem.hasOwnProperty(key)) {
715
686
  delete transformedItem[key];
716
687
  }
717
688
  });
689
+
718
690
  const usersMap = this.importMap.get(this.getCollectionKey("users"));
719
691
  const userDataToAdd = {
720
692
  rawData: item,
721
693
  finalData: userData.data,
722
694
  };
723
- // Directly update the importMap with the new user data, without pushing to usersMap.data first
724
695
  this.importMap.set(this.getCollectionKey("users"), {
725
696
  data: [...(usersMap?.data || []), userDataToAdd],
726
697
  });
@@ -808,14 +779,14 @@ export class DataLoader {
808
779
  if (!existingId) {
809
780
  // No existing user ID, generate a new unique ID
810
781
  existingId = this.getTrueUniqueId(this.getCollectionKey("users"));
811
- transformedItem.userId = existingId; // Assign the new ID to the transformed data's userId field
782
+ transformedItem.docId = existingId; // Assign the new ID to the transformed data's docId field
812
783
  }
813
784
 
814
785
  // Create a context object for the item, including the new ID
815
786
  let context = this.createContext(db, collection, item, existingId);
816
787
 
817
788
  // Merge the transformed data into the context
818
- context = { ...context, ...transformedItem };
789
+ context = { ...context, ...transformedItem, ...userData.finalData };
819
790
 
820
791
  // If a primary key field is defined, handle the ID mapping and check for duplicates
821
792
  if (importDef.primaryKeyField) {
@@ -137,53 +137,62 @@ export class ImportController {
137
137
  const usersData = usersDataMap?.data;
138
138
  const usersController = new UsersController(this.config, this.database);
139
139
  if (usersData) {
140
- console.log("Found users data");
141
- const userBatchesAll = createBatches(usersData);
142
- console.log(`${userBatchesAll.length} user batches`);
143
- for (let i = 0; i < userBatchesAll.length; i++) {
144
- const userBatches = userBatchesAll[i];
145
- console.log(
146
- `Processing user batch ${i + 1} of ${userBatchesAll.length}`
147
- );
148
- const userBatchPromises = userBatches
149
- .map((userBatch) => {
150
- if (userBatch.finalData && userBatch.finalData.length > 0) {
151
- const userId = userBatch.finalData.userId;
152
- if (dataLoader.userExistsMap.has(userId)) {
153
- // We only are storing the existing user ID's as true, so we need to check for that
154
- if (!(dataLoader.userExistsMap.get(userId) === true)) {
155
- const userId =
156
- userBatch.finalData.userId ||
157
- userBatch.context.userId ||
158
- userBatch.context.docId;
159
- if (!userBatch.finalData.userId) {
160
- userBatch.finalData.userId = userId;
161
- }
162
- return usersController
163
- .createUserAndReturn(userBatch.finalData)
164
- .then(() => console.log("Created user"))
165
- .catch((error) => {
166
- logger.error(
167
- "Error creating user:",
168
- error,
169
- "\nUser data is ",
170
- userBatch.finalData
171
- );
172
- });
173
- } else {
174
- console.log("Skipped existing user: ", userId);
175
- return Promise.resolve();
176
- }
177
- }
140
+ console.log("Found users data", usersData.length);
141
+ const userDataBatches = createBatches(usersData);
142
+ for (const batch of userDataBatches) {
143
+ console.log("Importing users batch", batch.length);
144
+ const userBatchPromises = batch
145
+ .filter((item) => {
146
+ let itemId: string | undefined;
147
+ if (item.finalData.userId) {
148
+ itemId = item.finalData.userId;
149
+ } else if (item.finalData.docId) {
150
+ itemId = item.finalData.docId;
151
+ }
152
+ if (!itemId) {
153
+ return false;
178
154
  }
155
+ return (
156
+ item &&
157
+ item.finalData &&
158
+ !dataLoader.userExistsMap.has(itemId)
159
+ );
179
160
  })
180
- .flat();
181
- // Wait for all promises in the current user batch to resolve
182
- await Promise.allSettled(userBatchPromises);
183
- console.log(
184
- `Completed user batch ${i + 1} of ${userBatchesAll.length}`
185
- );
161
+ .map((item) => {
162
+ return usersController.createUserAndReturn(item.finalData);
163
+ });
164
+ await Promise.all(userBatchPromises);
165
+ for (const item of batch) {
166
+ if (item && item.finalData) {
167
+ dataLoader.userExistsMap.set(
168
+ item.finalData.userId ||
169
+ item.finalData.docId ||
170
+ item.context.userId ||
171
+ item.context.docId,
172
+ true
173
+ );
174
+ }
175
+ }
176
+ console.log("Finished importing users batch", batch.length);
186
177
  }
178
+ // for (let i = 0; i < usersData.length; i++) {
179
+ // const user = usersData[i];
180
+ // if (user.finalData) {
181
+ // const userId =
182
+ // user.finalData.userId ||
183
+ // user.context.userId ||
184
+ // user.context.docId;
185
+ // if (!dataLoader.userExistsMap.has(userId)) {
186
+ // if (!user.finalData.userId) {
187
+ // user.finalData.userId = userId;
188
+ // }
189
+ // await usersController.createUserAndReturn(user.finalData);
190
+ // dataLoader.userExistsMap.set(userId, true);
191
+ // } else {
192
+ // console.log("Skipped existing user: ", userId);
193
+ // }
194
+ // }
195
+ // }
187
196
  console.log("Finished importing users");
188
197
  }
189
198
  }
@@ -216,10 +225,10 @@ export class ImportController {
216
225
  console.log(`Processing batch ${i + 1} of ${dataSplit.length}`);
217
226
  const batchPromises = batches.map((item) => {
218
227
  const id =
219
- item.context.docId ||
220
- item.context.userId ||
221
228
  item.finalData.docId ||
222
- item.finalData.userId;
229
+ item.finalData.userId ||
230
+ item.context.docId ||
231
+ item.context.userId;
223
232
  if (item.finalData.hasOwnProperty("userId")) {
224
233
  delete item.finalData.userId;
225
234
  }
@@ -229,22 +238,12 @@ export class ImportController {
229
238
  if (!item.finalData) {
230
239
  return Promise.resolve();
231
240
  }
232
- return this.database
233
- .createDocument(db.$id, collection.$id, id, item.finalData)
234
- .then(() => {
235
- processedItems++;
236
- console.log("Created item");
237
- })
238
- .catch((error) => {
239
- console.error(
240
- `Error creating item in ${collection.name}:`,
241
- error,
242
- "\nItem data is ",
243
- item.finalData
244
- );
245
- throw error;
246
- // Optionally, log the failed item for retry or review
247
- });
241
+ return this.database.createDocument(
242
+ db.$id,
243
+ collection.$id,
244
+ id,
245
+ item.finalData
246
+ );
248
247
  });
249
248
  // Wait for all promises in the current batch to resolve
250
249
  await Promise.allSettled(batchPromises);