appwrite-utils-cli 0.0.32 → 0.0.33

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.
@@ -234,6 +234,10 @@ export class DataLoader {
234
234
  // Determine if this is the users collection
235
235
  let isUsersCollection = this.getCollectionKey(this.config.usersCollectionName) ===
236
236
  this.getCollectionKey(collection.name);
237
+ const collectionDefs = collection.importDefs;
238
+ if (!collectionDefs || !collectionDefs.length) {
239
+ continue;
240
+ }
237
241
  // Process create and update definitions for the collection
238
242
  const createDefs = collection.importDefs.filter((def) => def.type === "create" || !def.type);
239
243
  const updateDefs = collection.importDefs.filter((def) => def.type === "update");
@@ -258,8 +262,8 @@ export class DataLoader {
258
262
  }
259
263
  }
260
264
  console.log("Running update references");
261
- await this.dealWithMergedUsers();
262
- await this.updateOldReferencesForNew();
265
+ this.dealWithMergedUsers();
266
+ this.updateOldReferencesForNew();
263
267
  console.log("Done running update references");
264
268
  }
265
269
  // for (const collection of this.config.collections) {
@@ -272,7 +276,7 @@ export class DataLoader {
272
276
  this.writeMapsToJsonFile();
273
277
  }
274
278
  }
275
- async dealWithMergedUsers() {
279
+ dealWithMergedUsers() {
276
280
  const usersCollectionKey = this.getCollectionKey(this.config.usersCollectionName);
277
281
  const usersCollectionPrimaryKeyFields = new Set();
278
282
  if (!this.config.collections) {
@@ -281,7 +285,11 @@ export class DataLoader {
281
285
  // Collect primary key fields from the users collection definitions
282
286
  this.config.collections.forEach((collection) => {
283
287
  if (this.getCollectionKey(collection.name) === usersCollectionKey) {
284
- collection.importDefs.forEach((importDef) => {
288
+ const collectionImportDefs = collection.importDefs;
289
+ if (!collectionImportDefs || !collectionImportDefs.length) {
290
+ return;
291
+ }
292
+ collectionImportDefs.forEach((importDef) => {
285
293
  if (importDef.primaryKeyField) {
286
294
  usersCollectionPrimaryKeyFields.add(importDef.primaryKeyField);
287
295
  }
@@ -293,19 +301,23 @@ export class DataLoader {
293
301
  const collectionData = this.importMap.get(this.getCollectionKey(collection.name));
294
302
  if (!collectionData || !collectionData.data)
295
303
  return;
296
- collection.importDefs.forEach((importDef) => {
304
+ const collectionImportDefs = collection.importDefs;
305
+ if (!collectionImportDefs || !collectionImportDefs.length) {
306
+ return;
307
+ }
308
+ collectionImportDefs.forEach((importDef) => {
297
309
  importDef.idMappings?.forEach((idMapping) => {
298
310
  if (this.getCollectionKey(idMapping.targetCollection) ===
299
311
  usersCollectionKey) {
300
- if (usersCollectionPrimaryKeyFields.has(idMapping.targetField)) {
312
+ const targetFieldKey = idMapping.targetFieldToMatch || idMapping.targetField;
313
+ if (usersCollectionPrimaryKeyFields.has(targetFieldKey)) {
301
314
  // Process each item in the collection
302
315
  collectionData.data.forEach((item) => {
303
316
  const oldId = item.context[idMapping.sourceField];
304
317
  const newId = this.mergedUserMap.get(oldId);
305
318
  if (newId) {
306
319
  // Update context to use new user ID
307
- item.context[idMapping.fieldToSet || idMapping.targetField] =
308
- newId;
320
+ item.context[idMapping.fieldToSet || targetFieldKey] = newId;
309
321
  }
310
322
  });
311
323
  }
@@ -333,123 +345,82 @@ export class DataLoader {
333
345
  for (const idMapping of importDef.idMappings) {
334
346
  const targetCollectionKey = this.getCollectionKey(idMapping.targetCollection);
335
347
  const fieldToSetKey = idMapping.fieldToSet || idMapping.sourceField;
348
+ const targetFieldKey = idMapping.targetFieldToMatch || idMapping.targetField;
336
349
  const valueToMatch = collectionData.data[i].context[idMapping.sourceField];
350
+ // Skip if value to match is missing or empty
337
351
  if (!valueToMatch || _.isEmpty(valueToMatch))
338
352
  continue;
353
+ const isFieldToSetArray = collectionConfig.attributes.find((attribute) => attribute.key === fieldToSetKey)?.array;
339
354
  const targetCollectionData = this.importMap.get(targetCollectionKey);
340
355
  if (!targetCollectionData || !targetCollectionData.data)
341
356
  continue;
342
- const foundData = targetCollectionData.data.filter((data) => {
343
- const targetValue = data.context[idMapping.targetField];
357
+ // Find matching data in the target collection
358
+ const foundData = targetCollectionData.data.filter(({ context }) => {
359
+ const targetValue = context[targetFieldKey];
344
360
  const isMatch = `${targetValue}` === `${valueToMatch}`;
345
- // Debugging output to understand what's being compared
346
- logger.warn(`Comparing target: ${targetValue} with match: ${valueToMatch} - Result: ${isMatch}`);
347
- return isMatch;
361
+ // Ensure the targetValue is defined and not null
362
+ return (isMatch &&
363
+ targetValue !== undefined &&
364
+ targetValue !== null);
348
365
  });
366
+ // Log and skip if no matching data found
349
367
  if (!foundData.length) {
350
- console.log(`No data found for collection: ${targetCollectionKey} with value: ${valueToMatch} for field: ${fieldToSetKey}`);
368
+ 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}`);
351
369
  logger.error(`No data found for collection: ${targetCollectionKey} with value: ${valueToMatch} for field: ${fieldToSetKey} -- idMapping: ${JSON.stringify(idMapping, null, 2)}`);
352
370
  continue;
353
371
  }
354
372
  needsUpdate = true;
355
- // Properly handle arrays and non-arrays
356
- if (Array.isArray(collectionData.data[i].finalData[fieldToSetKey])) {
357
- collectionData.data[i].finalData[fieldToSetKey] =
358
- foundData.map((data) => data.finalData);
373
+ const getCurrentDataFiltered = (currentData) => {
374
+ if (Array.isArray(currentData.finalData[fieldToSetKey])) {
375
+ return currentData.finalData[fieldToSetKey].filter((data) => `${data}` !== `${valueToMatch}`);
376
+ }
377
+ return currentData.finalData[fieldToSetKey];
378
+ };
379
+ // Get the current data to be updated
380
+ const currentDataFiltered = getCurrentDataFiltered(collectionData.data[i]);
381
+ // Extract the new data to set
382
+ const newData = foundData.map((data) => data.context[idMapping.targetField]);
383
+ // Handle cases where current data is an array
384
+ if (isFieldToSetArray) {
385
+ if (!currentDataFiltered) {
386
+ // Set new data if current data is undefined
387
+ collectionData.data[i].finalData[fieldToSetKey] =
388
+ Array.isArray(newData) ? newData : [newData];
389
+ }
390
+ else {
391
+ // Merge arrays if new data is non-empty array and filter for uniqueness
392
+ collectionData.data[i].finalData[fieldToSetKey] = [
393
+ ...new Set([...currentDataFiltered, ...newData].filter((value) => `${value}` !== `${valueToMatch}`)),
394
+ ];
395
+ }
359
396
  }
360
397
  else {
361
- collectionData.data[i].finalData[fieldToSetKey] =
362
- foundData[0].finalData;
363
- }
364
- }
365
- }
366
- }
367
- }
368
- }
369
- if (needsUpdate) {
370
- this.importMap.set(collectionKey, collectionData);
371
- }
372
- }
373
- }
374
- async updateReferencesInRelatedCollections() {
375
- if (!this.config.collections) {
376
- return;
377
- }
378
- // Iterate over each collection configuration
379
- for (const collectionConfig of this.config.collections) {
380
- const collectionKey = this.getCollectionKey(collectionConfig.name);
381
- const collectionData = this.importMap.get(collectionKey);
382
- if (!collectionData || !collectionData.data)
383
- continue;
384
- console.log(`Updating references for collection: ${collectionConfig.name}`);
385
- // Iterate over each data item in the current collection
386
- for (const item of collectionData.data) {
387
- let needsUpdate = false;
388
- // Check if the current collection has import definitions with idMappings
389
- if (collectionConfig.importDefs) {
390
- for (const importDef of collectionConfig.importDefs) {
391
- if (importDef.idMappings) {
392
- // Iterate over each idMapping defined for the current import definition
393
- for (const idMapping of importDef.idMappings) {
394
- const oldIds = Array.isArray(item.context[idMapping.sourceField])
395
- ? item.context[idMapping.sourceField]
396
- : [item.context[idMapping.sourceField]];
397
- const resolvedNewIds = [];
398
- oldIds.forEach((oldId) => {
399
- // Attempt to find a new ID for the old ID
400
- let newIdForOldId = this.findNewIdForOldId(oldId, idMapping, importDef);
401
- if (newIdForOldId &&
402
- !resolvedNewIds.includes(newIdForOldId)) {
403
- resolvedNewIds.push(newIdForOldId);
398
+ if (!currentDataFiltered) {
399
+ // Set new data if current data is undefined
400
+ collectionData.data[i].finalData[fieldToSetKey] =
401
+ Array.isArray(newData) ? newData : [newData];
404
402
  }
405
- else {
406
- logger.error(`No new ID found for old ID ${oldId} in collection ${collectionConfig.name}`);
403
+ else if (Array.isArray(newData) && newData.length > 0) {
404
+ // Convert current data to array and merge if new data is non-empty array, then filter for uniqueness
405
+ collectionData.data[i].finalData[fieldToSetKey] = [
406
+ ...new Set([currentDataFiltered, ...newData].filter((value) => `${value}` !== `${valueToMatch}`)),
407
+ ];
408
+ }
409
+ else if (!Array.isArray(newData) && newData !== undefined) {
410
+ // Simply update the field if new data is not an array and defined
411
+ collectionData.data[i].finalData[fieldToSetKey] = newData;
407
412
  }
408
- });
409
- if (resolvedNewIds.length) {
410
- const targetField = idMapping.fieldToSet || idMapping.targetField;
411
- const isArray = collectionConfig.attributes.some((attribute) => attribute.key === targetField && attribute.array);
412
- // Set the target field based on whether it's an array or single value
413
- item.finalData[targetField] = isArray
414
- ? resolvedNewIds
415
- : resolvedNewIds[0];
416
- needsUpdate = true;
417
413
  }
418
414
  }
419
415
  }
420
416
  }
421
417
  }
422
- // Update the importMap if changes were made to the item
423
- if (needsUpdate) {
424
- this.importMap.set(collectionKey, collectionData);
425
- logger.info(`Updated item: ${JSON.stringify(item.finalData, undefined, 2)}`);
426
- }
427
418
  }
428
- }
429
- }
430
- findNewIdForOldId(oldId, idMapping, importDef) {
431
- // First, check if this ID mapping is related to the users collection.
432
- const targetCollectionKey = this.getCollectionKey(idMapping.targetCollection);
433
- const isUsersCollection = targetCollectionKey ===
434
- this.getCollectionKey(this.config.usersCollectionName);
435
- // If handling users, check the mergedUserMap for any existing new ID.
436
- if (isUsersCollection) {
437
- for (const [newUserId, oldIds] of this.mergedUserMap.entries()) {
438
- if (oldIds.includes(oldId)) {
439
- return newUserId;
440
- }
441
- }
442
- }
443
- // If not a user or no merged ID found, check the regular ID mapping from old to new.
444
- const targetCollectionData = this.importMap.get(targetCollectionKey);
445
- if (targetCollectionData) {
446
- const foundEntry = targetCollectionData.data.find((entry) => entry.context[importDef.primaryKeyField] === oldId);
447
- if (foundEntry) {
448
- return foundEntry.context.docId; // Assuming `docId` stores the new ID after import
419
+ // Update the import map if any changes were made
420
+ if (needsUpdate) {
421
+ this.importMap.set(collectionKey, collectionData);
449
422
  }
450
423
  }
451
- logger.error(`No corresponding new ID found for ${oldId} in ${targetCollectionKey}`);
452
- return null; // Return null if no new ID is found
453
424
  }
454
425
  writeMapsToJsonFile() {
455
426
  const outputDir = path.resolve(process.cwd());
@@ -601,12 +572,12 @@ export class DataLoader {
601
572
  if (!existingId) {
602
573
  // No existing user ID, generate a new unique ID
603
574
  existingId = this.getTrueUniqueId(this.getCollectionKey("users"));
604
- transformedItem.userId = existingId; // Assign the new ID to the transformed data's userId field
575
+ transformedItem.docId = existingId; // Assign the new ID to the transformed data's docId field
605
576
  }
606
577
  // Create a context object for the item, including the new ID
607
578
  let context = this.createContext(db, collection, item, existingId);
608
579
  // Merge the transformed data into the context
609
- context = { ...context, ...transformedItem };
580
+ context = { ...context, ...transformedItem, ...userData.finalData };
610
581
  // If a primary key field is defined, handle the ID mapping and check for duplicates
611
582
  if (importDef.primaryKeyField) {
612
583
  const oldId = item[importDef.primaryKeyField];
@@ -17,25 +17,25 @@ export class SchemaGenerator {
17
17
  const collections = this.config.collections;
18
18
  delete this.config.collections;
19
19
  const configPath = path.join(this.appwriteFolderPath, "appwriteConfig.ts");
20
- const configContent = `import { type AppwriteConfig } from "appwrite-utils";
21
-
22
- const appwriteConfig: AppwriteConfig = {
23
- appwriteEndpoint: "${this.config.appwriteEndpoint}",
24
- appwriteProject: "${this.config.appwriteProject}",
25
- appwriteKey: "${this.config.appwriteKey}",
26
- enableDevDatabase: ${this.config.enableDevDatabase},
27
- enableBackups: ${this.config.enableBackups},
28
- backupInterval: ${this.config.backupInterval},
29
- backupRetention: ${this.config.backupRetention},
30
- enableBackupCleanup: ${this.config.enableBackupCleanup},
31
- enableMockData: ${this.config.enableMockData},
32
- enableWipeOtherDatabases: ${this.config.enableWipeOtherDatabases},
33
- documentBucketId: "${this.config.documentBucketId}",
34
- usersCollectionName: "${this.config.usersCollectionName}",
35
- databases: ${JSON.stringify(this.config.databases)}
36
- };
37
-
38
- export default appwriteConfig;
20
+ const configContent = `import { type AppwriteConfig } from "appwrite-utils";
21
+
22
+ const appwriteConfig: AppwriteConfig = {
23
+ appwriteEndpoint: "${this.config.appwriteEndpoint}",
24
+ appwriteProject: "${this.config.appwriteProject}",
25
+ appwriteKey: "${this.config.appwriteKey}",
26
+ enableDevDatabase: ${this.config.enableDevDatabase},
27
+ enableBackups: ${this.config.enableBackups},
28
+ backupInterval: ${this.config.backupInterval},
29
+ backupRetention: ${this.config.backupRetention},
30
+ enableBackupCleanup: ${this.config.enableBackupCleanup},
31
+ enableMockData: ${this.config.enableMockData},
32
+ enableWipeOtherDatabases: ${this.config.enableWipeOtherDatabases},
33
+ documentBucketId: "${this.config.documentBucketId}",
34
+ usersCollectionName: "${this.config.usersCollectionName}",
35
+ databases: ${JSON.stringify(this.config.databases)}
36
+ };
37
+
38
+ export default appwriteConfig;
39
39
  `;
40
40
  fs.writeFileSync(configPath, configContent, { encoding: "utf-8" });
41
41
  const collectionsFolderPath = path.join(this.appwriteFolderPath, "collections");
@@ -45,19 +45,19 @@ export class SchemaGenerator {
45
45
  collections?.forEach((collection) => {
46
46
  const { databaseId, ...collectionWithoutDbId } = collection; // Destructure to exclude databaseId
47
47
  const collectionFilePath = path.join(collectionsFolderPath, `${collection.name}.ts`);
48
- const collectionContent = `import { type CollectionCreate } from "appwrite-utils";
49
-
50
- const ${collection.name}Config: Partial<CollectionCreate> = {
51
- name: "${collection.name}",
52
- $id: "${collection.$id}",
53
- enabled: ${collection.enabled},
54
- documentSecurity: ${collection.documentSecurity},
55
- $permissions: [
48
+ const collectionContent = `import { type CollectionCreate } from "appwrite-utils";
49
+
50
+ const ${collection.name}Config: Partial<CollectionCreate> = {
51
+ name: "${collection.name}",
52
+ $id: "${collection.$id}",
53
+ enabled: ${collection.enabled},
54
+ documentSecurity: ${collection.documentSecurity},
55
+ $permissions: [
56
56
  ${collection.$permissions
57
57
  .map((permission) => `{ permission: "${permission.permission}", target: "${permission.target}" }`)
58
- .join(",\n ")}
59
- ],
60
- attributes: [
58
+ .join(",\n ")}
59
+ ],
60
+ attributes: [
61
61
  ${collection.attributes
62
62
  .map((attr) => {
63
63
  return `{ ${Object.entries(attr)
@@ -85,9 +85,9 @@ export class SchemaGenerator {
85
85
  })
86
86
  .join(", ")} }`;
87
87
  })
88
- .join(",\n ")}
89
- ],
90
- indexes: [
88
+ .join(",\n ")}
89
+ ],
90
+ indexes: [
91
91
  ${(collection.indexes?.map((index) => {
92
92
  // Map each attribute to ensure it is properly quoted
93
93
  const formattedAttributes = index.attributes.map((attr) => `"${attr}"`).join(", ") ?? "";
@@ -95,11 +95,11 @@ export class SchemaGenerator {
95
95
  ?.filter((order) => order !== null)
96
96
  .map((order) => `"${order}"`)
97
97
  .join(", ") ?? ""}] }`;
98
- }) ?? []).join(",\n ")}
99
- ]
100
- };
101
-
102
- export default ${collection.name}Config;
98
+ }) ?? []).join(",\n ")}
99
+ ]
100
+ };
101
+
102
+ export default ${collection.name}Config;
103
103
  `;
104
104
  fs.writeFileSync(collectionFilePath, collectionContent, {
105
105
  encoding: "utf-8",
@@ -69,6 +69,9 @@ export class UsersController {
69
69
  async getAllUsers() {
70
70
  const allUsers = [];
71
71
  const users = await this.users.list([Query.limit(200)]);
72
+ if (users.users.length === 0) {
73
+ return [];
74
+ }
72
75
  if (users.users.length === 200) {
73
76
  let lastDocumentId = users.users[users.users.length - 1].$id;
74
77
  allUsers.push(...users.users);
package/package.json CHANGED
@@ -1,54 +1,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.32",
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.2.4",
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.33",
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": "node --max-old-space-size=16384 ./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.2.5",
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
+ }