assai 2.1.0 → 2.1.2

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.
Files changed (25) hide show
  1. package/dist/src/__test/mock_get_collection.d.mts +2 -0
  2. package/dist/src/usecases/mongo/transformers/id/rename_to_dev_id.d.mts +3 -0
  3. package/dist/src/usecases/mongo/transformers/id/rename_to_mongo_id.d.mts +3 -0
  4. package/dist/src/usecases/mongo/transformers/object_id/ids_into_strings.d.mts +5 -0
  5. package/dist/src/usecases/mongo/transformers/object_id/strings_into_id.d.mts +3 -0
  6. package/dist/src/usecases/mongo/transformers/timestamps.d.mts +5 -0
  7. package/package.json +3 -3
  8. package/src/__test/mock_get_collection.mjs +2 -0
  9. package/src/usecases/mongo/operation/aggregate.mjs +2 -7
  10. package/src/usecases/mongo/operation/bulk_write.mjs +8 -8
  11. package/src/usecases/mongo/operation/count.mjs +1 -1
  12. package/src/usecases/mongo/operation/delete_many.mjs +1 -1
  13. package/src/usecases/mongo/operation/delete_one.mjs +1 -1
  14. package/src/usecases/mongo/operation/find.mjs +1 -1
  15. package/src/usecases/mongo/operation/find_one_and_delete.mjs +1 -1
  16. package/src/usecases/mongo/operation/find_one_and_replace.mjs +1 -1
  17. package/src/usecases/mongo/operation/find_one_and_update.mjs +2 -2
  18. package/src/usecases/mongo/operation/update_many.mjs +2 -2
  19. package/src/usecases/mongo/operation/update_one.mjs +2 -2
  20. package/src/usecases/mongo/transformers/id/rename_to_dev_id.mjs +28 -6
  21. package/src/usecases/mongo/transformers/id/rename_to_mongo_id.mjs +28 -6
  22. package/src/usecases/mongo/transformers/input_transformer.mjs +1 -1
  23. package/src/usecases/mongo/transformers/object_id/ids_into_strings.mjs +22 -5
  24. package/src/usecases/mongo/transformers/object_id/strings_into_id.mjs +55 -9
  25. package/src/usecases/mongo/transformers/timestamps.mjs +5 -0
@@ -6,6 +6,8 @@ export type ItestCollection = {
6
6
  tag?: string | ObjectId | undefined;
7
7
  createdAt?: Date | undefined;
8
8
  posts?: any[] | undefined;
9
+ score?: number | undefined;
10
+ group?: string | undefined;
9
11
  address?: object;
10
12
  };
11
13
  import { Collection } from 'mongodb';
@@ -2,5 +2,8 @@
2
2
  * ```
3
3
  * "_id" -> "id"
4
4
  * ```
5
+ * Recursively renames all `_id` keys to `id` in objects and arrays.
6
+ * Does not mutate the original input.
7
+ * @param {*} obj
5
8
  */
6
9
  export function renameToDevId(obj: any): any;
@@ -2,5 +2,8 @@
2
2
  * ```
3
3
  * "id" -> "_id"
4
4
  * ```
5
+ * Recursively renames all `id` keys to `_id` in objects and arrays.
6
+ * Does not mutate the original input.
7
+ * @param {*} obj
5
8
  */
6
9
  export function renameToMongoId(obj: any): any;
@@ -2,6 +2,11 @@
2
2
  * Convert the parameter from `ObjectId` into string whenever possible.
3
3
  *
4
4
  * If an object or array is given, this function will be applied recursively.
5
+ *
6
+ * Uses duck-typing (`_bsontype === 'ObjectId'`) instead of `instanceof` to
7
+ * safely identify ObjectId values across different instances of the `mongodb`
8
+ * / `bson` package (e.g. when the host project and this library resolve to
9
+ * separate copies of the package).
5
10
  * @param {*} obj
6
11
  */
7
12
  export function idsIntoString(obj: any): any;
@@ -6,6 +6,9 @@
6
6
  * - An array;
7
7
  *
8
8
  * If the parameter is a string, then it is converted to `ObjectId` if possible.
9
+ *
10
+ * This function does not mutate the original input. It preserves reference equality for
11
+ * objects and special types (like Date) that don't need transformation.
9
12
  * @param {*} obj
10
13
  */
11
14
  export function stringsIntoId(obj: any): any;
@@ -1,4 +1,9 @@
1
1
  /**
2
+ * Transforms a document by adding or updating timestamp fields (createdAt and updatedAt).
3
+ * If createdAt is not set, it can be generated as a new Date or extracted from the document's ObjectId.
4
+ * If updatedAt is not set, it is generated as a new Date.
5
+ * The document is modified in place.
6
+ *
2
7
  * @template {import('../../../types.js').MongoDocument} T
3
8
  * @param {import('mongodb').WithId<T> | import('../../../types.js').MongoDocument} doc
4
9
  * @param {import('../../../factories/create_mongo_collection.mjs').IcreateCollectionOptions<T>} options
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assai",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "repository": {
5
5
  "url": "https://github.com/TimeLord2010/assai",
6
6
  "type": "git"
@@ -28,8 +28,8 @@
28
28
  ],
29
29
  "author": "Vinícius Gabriel",
30
30
  "license": "ISC",
31
- "dependencies": {
32
- "mongodb": "^7.0.0"
31
+ "peerDependencies": {
32
+ "mongodb": ">=4.0.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@faker-js/faker": "^8.4.1",
@@ -17,5 +17,7 @@ export async function mockGetCollection(collectionName = 'test') {
17
17
  * @property {string | ObjectId} [tag]
18
18
  * @property {Date} [createdAt]
19
19
  * @property {object[]} [posts]
20
+ * @property {number} [score]
21
+ * @property {string} [group]
20
22
  * @property {object} [address]
21
23
  */
@@ -12,15 +12,10 @@ import { outputTransformer } from '../transformers/output_transformer.mjs'
12
12
  * @returns {Promise<T[]>}
13
13
  */
14
14
  export async function aggregate({ getCollection, pipeline, options, collectionOptions }) {
15
- for (const stage of pipeline) {
16
- if (stage.$match != null) {
17
- stage.$match = renameToMongoId(stage.$match)
18
- }
19
- }
20
- pipeline = stringsIntoId(pipeline)
15
+ const finalPipeline = stringsIntoId(renameToMongoId(pipeline))
21
16
 
22
17
  const col = await getCollection()
23
- const docs = await col.aggregate(pipeline, options).toArray()
18
+ const docs = await col.aggregate(finalPipeline, options).toArray()
24
19
  // @ts-ignore
25
20
  return docs.map((doc) => outputTransformer({ document: doc, collectionOptions }))
26
21
  }
@@ -21,22 +21,22 @@ export async function bulkWrite({ getCollection, operations, options, collection
21
21
  })
22
22
  } else if (o.updateOne != null) {
23
23
  o.updateOne.filter = renameToMongoId(o.updateOne.filter)
24
- stringsIntoId(o.updateOne.filter)
25
- stringsIntoId(o.updateOne.update)
24
+ o.updateOne.filter = stringsIntoId(o.updateOne.filter)
25
+ o.updateOne.update = stringsIntoId(o.updateOne.update)
26
26
  } else if (o.updateMany != null) {
27
27
  o.updateMany.filter = renameToMongoId(o.updateMany.filter)
28
- stringsIntoId(o.updateMany.filter)
29
- stringsIntoId(o.updateMany.update)
28
+ o.updateMany.filter = stringsIntoId(o.updateMany.filter)
29
+ o.updateMany.update = stringsIntoId(o.updateMany.update)
30
30
  } else if (o.deleteOne != null) {
31
31
  o.deleteOne.filter = renameToMongoId(o.deleteOne.filter)
32
- stringsIntoId(o.deleteOne.filter)
32
+ o.deleteOne.filter = stringsIntoId(o.deleteOne.filter)
33
33
  } else if (o.deleteMany != null) {
34
34
  o.deleteMany.filter = renameToMongoId(o.deleteMany.filter)
35
- stringsIntoId(o.deleteMany.filter)
35
+ o.deleteMany.filter = stringsIntoId(o.deleteMany.filter)
36
36
  } else if (o.replaceOne != null) {
37
37
  o.replaceOne.filter = renameToMongoId(o.replaceOne.filter)
38
- stringsIntoId(o.replaceOne.filter)
39
- stringsIntoId(o.replaceOne.replacement)
38
+ o.replaceOne.filter = stringsIntoId(o.replaceOne.filter)
39
+ o.replaceOne.replacement = stringsIntoId(o.replaceOne.replacement)
40
40
  }
41
41
  }
42
42
 
@@ -9,7 +9,7 @@ import { renameToMongoId, stringsIntoId } from '../transformers/index.mjs'
9
9
  */
10
10
  export async function count({ getCollection, query }) {
11
11
  query = renameToMongoId(query)
12
- stringsIntoId(query)
12
+ query = stringsIntoId(query)
13
13
 
14
14
  const col = await getCollection()
15
15
  const count = await col.countDocuments(query)
@@ -9,7 +9,7 @@ import { renameToMongoId, stringsIntoId } from '../transformers/index.mjs'
9
9
  */
10
10
  export async function deleteMany({ query, getCollection }) {
11
11
  query = renameToMongoId(query)
12
- stringsIntoId(query)
12
+ query = stringsIntoId(query)
13
13
  const col = await getCollection()
14
14
  const r = await col.deleteMany(query)
15
15
  return r.deletedCount
@@ -10,7 +10,7 @@ import { renameToMongoId, stringsIntoId } from '../transformers/index.mjs'
10
10
  */
11
11
  export async function deleteOne({ query, getCollection }) {
12
12
  query = renameToMongoId(query)
13
- stringsIntoId(query)
13
+ query = stringsIntoId(query)
14
14
  const col = await getCollection()
15
15
  const r = await col.deleteOne(query)
16
16
  return r.deletedCount > 0
@@ -20,7 +20,7 @@ export async function find({ getCollection, query, options, collectionOptions })
20
20
  query = renameToMongoId(query)
21
21
  options = renameFindOptions(options)
22
22
 
23
- stringsIntoId(query)
23
+ query = stringsIntoId(query)
24
24
 
25
25
  const col = await getCollection()
26
26
 
@@ -13,7 +13,7 @@ import { outputTransformer } from '../transformers/output_transformer.mjs'
13
13
  */
14
14
  export async function findOneAndDelete({ query, options, collectionOptions, getCollection }) {
15
15
  query = renameToMongoId(query)
16
- stringsIntoId(query)
16
+ query = stringsIntoId(query)
17
17
  const col = await getCollection()
18
18
 
19
19
  const doc = await col.findOneAndDelete(query, options ?? {})
@@ -14,7 +14,7 @@ import { outputTransformer } from '../transformers/output_transformer.mjs'
14
14
  */
15
15
  export async function findOneAndReplace({ query, replacement, options, collectionOptions, getCollection }) {
16
16
  query = renameToMongoId(query)
17
- stringsIntoId(query)
17
+ query = stringsIntoId(query)
18
18
  const col = await getCollection()
19
19
 
20
20
  const doc = await col.findOneAndReplace(query, replacement, options ?? {})
@@ -14,8 +14,8 @@ import { outputTransformer } from '../transformers/output_transformer.mjs'
14
14
  */
15
15
  export async function findOneAndUpdate({ query, update, options, collectionOptions, getCollection }) {
16
16
  query = renameToMongoId(query)
17
- stringsIntoId(query)
18
- stringsIntoId(update)
17
+ query = stringsIntoId(query)
18
+ update = stringsIntoId(update)
19
19
  const col = await getCollection()
20
20
 
21
21
  const { timestamps } = collectionOptions ?? {}
@@ -12,8 +12,8 @@ import { renameToMongoId, stringsIntoId } from '../transformers/index.mjs'
12
12
  */
13
13
  export async function updateMany({ query, update, options, collectionOptions, getCollection }) {
14
14
  query = renameToMongoId(query)
15
- stringsIntoId(query)
16
- stringsIntoId(update)
15
+ query = stringsIntoId(query)
16
+ update = stringsIntoId(update)
17
17
 
18
18
  const { timestamps } = collectionOptions ?? {}
19
19
  const { updatedAt } = timestamps ?? {
@@ -13,8 +13,8 @@ import { renameToMongoId, stringsIntoId } from '../transformers/index.mjs'
13
13
  */
14
14
  export async function updateOne({ query, update, options, collectionOptions, getCollection }) {
15
15
  query = renameToMongoId(query)
16
- stringsIntoId(query)
17
- stringsIntoId(update)
16
+ query = stringsIntoId(query)
17
+ update = stringsIntoId(update)
18
18
  const col = await getCollection()
19
19
 
20
20
  const { timestamps } = collectionOptions ?? {}
@@ -2,13 +2,35 @@
2
2
  * ```
3
3
  * "_id" -> "id"
4
4
  * ```
5
+ * Recursively renames all `_id` keys to `id` in objects and arrays.
6
+ * Does not mutate the original input.
7
+ * @param {*} obj
5
8
  */
6
9
  export function renameToDevId(obj) {
7
- if (!obj) return obj
8
- let { _id, ...rest } = obj
9
- if (!_id) return obj
10
- return {
11
- id: _id,
12
- ...rest
10
+ if (obj == null) return obj
11
+ if (typeof obj !== 'object') return obj
12
+ if (obj instanceof Date) return obj
13
+
14
+ if (Array.isArray(obj)) {
15
+ let hasChanges = false
16
+ const transformed = new Array(obj.length)
17
+ for (let i = 0; i < obj.length; i++) {
18
+ const item = obj[i]
19
+ const transformedItem = renameToDevId(item)
20
+ transformed[i] = transformedItem
21
+ if (transformedItem !== item) hasChanges = true
22
+ }
23
+ return hasChanges ? transformed : obj
13
24
  }
25
+
26
+ let hasChanges = false
27
+ /** @type {Record<string, any>} */
28
+ const transformed = {}
29
+ for (const [key, value] of Object.entries(obj)) {
30
+ const newKey = key === '_id' ? 'id' : key
31
+ const newValue = renameToDevId(value)
32
+ transformed[newKey] = newValue
33
+ if (newKey !== key || newValue !== value) hasChanges = true
34
+ }
35
+ return hasChanges ? transformed : obj
14
36
  }
@@ -2,13 +2,35 @@
2
2
  * ```
3
3
  * "id" -> "_id"
4
4
  * ```
5
+ * Recursively renames all `id` keys to `_id` in objects and arrays.
6
+ * Does not mutate the original input.
7
+ * @param {*} obj
5
8
  */
6
9
  export function renameToMongoId(obj) {
7
- if (!obj) return obj
8
- let { id, ...rest } = obj
9
- if (!id) return obj
10
- return {
11
- _id: id,
12
- ...rest
10
+ if (obj == null) return obj
11
+ if (typeof obj !== 'object') return obj
12
+ if (obj instanceof Date) return obj
13
+
14
+ if (Array.isArray(obj)) {
15
+ let hasChanges = false
16
+ const transformed = new Array(obj.length)
17
+ for (let i = 0; i < obj.length; i++) {
18
+ const item = obj[i]
19
+ const transformedItem = renameToMongoId(item)
20
+ transformed[i] = transformedItem
21
+ if (transformedItem !== item) hasChanges = true
22
+ }
23
+ return hasChanges ? transformed : obj
13
24
  }
25
+
26
+ let hasChanges = false
27
+ /** @type {Record<string, any>} */
28
+ const transformed = {}
29
+ for (const [key, value] of Object.entries(obj)) {
30
+ const newKey = key === 'id' ? '_id' : key
31
+ const newValue = renameToMongoId(value)
32
+ transformed[newKey] = newValue
33
+ if (newKey !== key || newValue !== value) hasChanges = true
34
+ }
35
+ return hasChanges ? transformed : obj
14
36
  }
@@ -11,7 +11,7 @@ export function inputTransformer({
11
11
  document, collectionOptions,
12
12
  }) {
13
13
  document = renameToMongoId(document)
14
- stringsIntoId(document)
14
+ document = stringsIntoId(document)
15
15
 
16
16
  const { timestamps } = collectionOptions ?? {}
17
17
  const { createdAt, updatedAt } = timestamps ?? {
@@ -1,15 +1,18 @@
1
- import { ObjectId } from 'mongodb'
2
-
3
1
  /**
4
2
  * Convert the parameter from `ObjectId` into string whenever possible.
5
3
  *
6
4
  * If an object or array is given, this function will be applied recursively.
5
+ *
6
+ * Uses duck-typing (`_bsontype === 'ObjectId'`) instead of `instanceof` to
7
+ * safely identify ObjectId values across different instances of the `mongodb`
8
+ * / `bson` package (e.g. when the host project and this library resolve to
9
+ * separate copies of the package).
7
10
  * @param {*} obj
8
11
  */
9
12
  export function idsIntoString(obj) {
10
13
  if (obj == null) return obj
11
14
  if (typeof obj != 'object') return obj
12
- if (obj instanceof ObjectId) return obj.toHexString()
15
+ if (isObjectId(obj)) return obj.toHexString()
13
16
  if (Array.isArray(obj)) {
14
17
  for (let i = 0; i < obj.length; i++) {
15
18
  const item = obj[i]
@@ -18,7 +21,7 @@ export function idsIntoString(obj) {
18
21
  return obj
19
22
  }
20
23
  for (const [key, value] of Object.entries(obj)) {
21
- if (value instanceof ObjectId) {
24
+ if (isObjectId(value)) {
22
25
  obj[key] = value.toHexString()
23
26
  continue
24
27
  }
@@ -27,4 +30,18 @@ export function idsIntoString(obj) {
27
30
  }
28
31
  }
29
32
  return obj
30
- }
33
+ }
34
+
35
+ /**
36
+ * Duck-type check for BSON ObjectId values.
37
+ *
38
+ * Using `_bsontype` instead of `instanceof ObjectId` avoids false negatives
39
+ * that occur when the host project and this package resolve to different copies
40
+ * of the `mongodb` / `bson` module, which makes `instanceof` return `false`
41
+ * even for legitimate ObjectId instances.
42
+ * @param {*} value
43
+ * @returns {boolean}
44
+ */
45
+ function isObjectId(value) {
46
+ return value != null && typeof value == 'object' && value._bsontype === 'ObjectId'
47
+ }
@@ -8,31 +8,77 @@ import { ObjectId } from 'mongodb'
8
8
  * - An array;
9
9
  *
10
10
  * If the parameter is a string, then it is converted to `ObjectId` if possible.
11
+ *
12
+ * This function does not mutate the original input. It preserves reference equality for
13
+ * objects and special types (like Date) that don't need transformation.
11
14
  * @param {*} obj
12
15
  */
13
16
  export function stringsIntoId(obj) {
14
17
  if (obj == null) return obj
15
18
  if (isObjectIdString(obj)) return new ObjectId(obj)
16
19
  if (typeof obj != 'object') return obj
20
+
21
+ // Check if this object/array needs any transformation
22
+ const transformed = _transformRecursive(obj)
23
+ return transformed
24
+ }
25
+
26
+ /**
27
+ * Recursively transforms ObjectId hex strings to ObjectId instances.
28
+ * Returns the original reference if no changes are needed, or a new object if changes are made.
29
+ * @param {*} obj
30
+ * @returns {*}
31
+ */
32
+ function _transformRecursive(obj) {
33
+ if (obj == null || typeof obj != 'object') return obj
34
+
35
+ // Don't clone or transform special objects like Date
36
+ if (obj instanceof Date) return obj
37
+
17
38
  if (Array.isArray(obj)) {
18
- // We clone the array to prevent the original array reference to change.
19
- obj = [...obj]
39
+ let hasChanges = false
40
+ const transformed = new Array(obj.length)
20
41
 
21
42
  for (let i = 0; i < obj.length; i++) {
22
43
  const item = obj[i]
23
- obj[i] = stringsIntoId(item)
44
+ if (isObjectIdString(item)) {
45
+ transformed[i] = new ObjectId(item)
46
+ hasChanges = true
47
+ } else if (item != null && typeof item == 'object') {
48
+ const transformedItem = _transformRecursive(item)
49
+ transformed[i] = transformedItem
50
+ if (transformedItem !== item) {
51
+ hasChanges = true
52
+ }
53
+ } else {
54
+ transformed[i] = item
55
+ }
24
56
  }
25
- return obj
57
+
58
+ return hasChanges ? transformed : obj
26
59
  }
60
+
61
+ // Handle plain objects
62
+ let hasChanges = false
63
+ /** @type {Record<string, any>} */
64
+ const transformed = {}
65
+
27
66
  for (const [key, value] of Object.entries(obj)) {
28
67
  if (isObjectIdString(value)) {
29
- obj[key] = ObjectId.createFromHexString(value)
30
- }
31
- if (value != null && typeof value == 'object') {
32
- obj[key] = stringsIntoId(value)
68
+ transformed[key] = ObjectId.createFromHexString(value)
69
+ hasChanges = true
70
+ } else if (value != null && typeof value == 'object') {
71
+ const transformedValue = _transformRecursive(value)
72
+ transformed[key] = transformedValue
73
+ if (transformedValue !== value) {
74
+ hasChanges = true
75
+ }
76
+ } else {
77
+ transformed[key] = value
33
78
  }
34
79
  }
35
- return obj
80
+
81
+ return hasChanges ? transformed : obj
36
82
  }
37
83
 
38
84
  /**
@@ -1,6 +1,11 @@
1
1
  import { ObjectId } from 'mongodb'
2
2
 
3
3
  /**
4
+ * Transforms a document by adding or updating timestamp fields (createdAt and updatedAt).
5
+ * If createdAt is not set, it can be generated as a new Date or extracted from the document's ObjectId.
6
+ * If updatedAt is not set, it is generated as a new Date.
7
+ * The document is modified in place.
8
+ *
4
9
  * @template {import('../../../types.js').MongoDocument} T
5
10
  * @param {import('mongodb').WithId<T> | import('../../../types.js').MongoDocument} doc
6
11
  * @param {import('../../../factories/create_mongo_collection.mjs').IcreateCollectionOptions<T>} options