adapt-authoring-mongodb 1.1.4 → 2.0.0

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.
@@ -0,0 +1,15 @@
1
+ name: Tests
2
+ on: push
3
+ jobs:
4
+ default:
5
+ runs-on: ubuntu-latest
6
+ permissions:
7
+ contents: read
8
+ steps:
9
+ - uses: actions/checkout@v4
10
+ - uses: actions/setup-node@v4
11
+ with:
12
+ node-version: 'lts/*'
13
+ cache: 'npm'
14
+ - run: npm ci
15
+ - run: npm test
package/index.js CHANGED
@@ -3,4 +3,5 @@
3
3
  * @namespace mongodb
4
4
  */
5
5
  export { default as ObjectIdUtils } from './lib/ObjectIdUtils.js'
6
+ export { create, isObjectId, isValid, parse, parseIds } from './lib/utils.js'
6
7
  export { default } from './lib/MongoDBModule.js'
@@ -1,6 +1,7 @@
1
1
  import { AbstractModule } from 'adapt-authoring-core'
2
2
  import { MongoClient } from 'mongodb'
3
3
  import ObjectIdUtils from './ObjectIdUtils.js'
4
+ import { parseIds } from './utils/parseIds.js'
4
5
  /**
5
6
  * Represents a single MongoDB server instance
6
7
  * @memberof mongodb
@@ -98,7 +99,7 @@ class MongoDBModule extends AbstractModule {
98
99
  * @see https://mongodb.github.io/node-mongodb-native/4.2/classes/Collection.html#insertOne
99
100
  */
100
101
  async insert (collectionName, data, options) {
101
- this.ObjectId.parseIds(data)
102
+ parseIds(data)
102
103
  this.parseOptions(options)
103
104
  // MongoDB doesn't like the explicit setting of _id
104
105
  delete data._id
@@ -122,7 +123,7 @@ class MongoDBModule extends AbstractModule {
122
123
  * @see https://mongodb.github.io/node-mongodb-native/4.2/classes/Collection.html#find
123
124
  */
124
125
  async find (collectionName, query, options) {
125
- this.ObjectId.parseIds(query)
126
+ parseIds(query)
126
127
  this.parseOptions(options)
127
128
  try {
128
129
  const cursor = this.getCollection(collectionName).find(query, options)
@@ -145,8 +146,8 @@ class MongoDBModule extends AbstractModule {
145
146
  async update (collectionName, query, data, options) {
146
147
  const opts = Object.assign({ includeResultMetadata: false, returnDocument: 'after' }, options)
147
148
  this.parseOptions(opts)
148
- this.ObjectId.parseIds(query)
149
- this.ObjectId.parseIds(data)
149
+ parseIds(query)
150
+ parseIds(data)
150
151
  // MongoDB doesn't like the explicit setting of _id
151
152
  delete data._id
152
153
  if (data.$set) delete data.$set._id
@@ -168,8 +169,8 @@ class MongoDBModule extends AbstractModule {
168
169
  */
169
170
  async updateMany (collectionName, query, data, options) {
170
171
  this.parseOptions(options)
171
- this.ObjectId.parseIds(query)
172
- this.ObjectId.parseIds(data)
172
+ parseIds(query)
173
+ parseIds(data)
173
174
  // MongoDB doesn't like the explicit setting of _id
174
175
  delete data._id
175
176
  if (data.$set) delete data.$set._id
@@ -193,8 +194,8 @@ class MongoDBModule extends AbstractModule {
193
194
  */
194
195
  async replace (collectionName, query, data, options) {
195
196
  const opts = Object.assign({ includeResultMetadata: false, returnDocument: 'after' }, options)
196
- this.ObjectId.parseIds(query)
197
- this.ObjectId.parseIds(data)
197
+ parseIds(query)
198
+ parseIds(data)
198
199
  this.parseOptions(options)
199
200
  // MongoDB doesn't like the explicit setting of _id
200
201
  delete data._id
@@ -216,7 +217,7 @@ class MongoDBModule extends AbstractModule {
216
217
  * @see https://mongodb.github.io/node-mongodb-native/4.2/classes/Collection.html#deleteOne
217
218
  */
218
219
  async delete (collectionName, query, options) {
219
- this.ObjectId.parseIds(query)
220
+ parseIds(query)
220
221
  this.parseOptions(options)
221
222
  try {
222
223
  await this.getCollection(collectionName).deleteOne(query, options)
@@ -235,7 +236,7 @@ class MongoDBModule extends AbstractModule {
235
236
  * @see https://mongodb.github.io/node-mongodb-native/4.2/classes/Collection.html#deleteMany
236
237
  */
237
238
  async deleteMany (collectionName, query, options) {
238
- this.ObjectId.parseIds(query)
239
+ parseIds(query)
239
240
  this.parseOptions(options)
240
241
  try {
241
242
  await this.getCollection(collectionName).deleteMany(query, options)
@@ -258,14 +259,6 @@ class MongoDBModule extends AbstractModule {
258
259
  else if (error.code === 11000) e = this.app.errors.MONGO_DUPL_INDEX
259
260
  return e.setData({ collectionName, action, error: error.message })
260
261
  }
261
-
262
- /**
263
- * ObjectId utility functions
264
- * @type {ObjectIdUtils}
265
- */
266
- get ObjectId () {
267
- return ObjectIdUtils
268
- }
269
262
  }
270
263
 
271
264
  export default MongoDBModule
@@ -1,7 +1,9 @@
1
- import { App, Utils } from 'adapt-authoring-core'
2
- import { ObjectId } from 'mongodb'
1
+ import { App } from 'adapt-authoring-core'
2
+ import { isValid } from './utils/isValid.js'
3
+ import { parse } from './utils/parse.js'
4
+
3
5
  /**
4
- * Utility functions for dealing with MongoDB ObjectIds
6
+ * JSON schema integration for MongoDB ObjectIds
5
7
  * @memberof mongodb
6
8
  */
7
9
  class ObjectIdUtils {
@@ -17,11 +19,11 @@ class ObjectIdUtils {
17
19
  schemaType: 'boolean',
18
20
  compile: () => {
19
21
  return (value, { parentData, parentDataProperty }) => {
20
- if (!ObjectIdUtils.isValid(value)) {
22
+ if (!isValid(value)) {
21
23
  return false
22
24
  }
23
25
  try {
24
- parentData[parentDataProperty] = ObjectIdUtils.parse(value)
26
+ parentData[parentDataProperty] = parse(value)
25
27
  } catch (e) {
26
28
  return false
27
29
  }
@@ -30,73 +32,6 @@ class ObjectIdUtils {
30
32
  }
31
33
  })
32
34
  }
33
-
34
- /**
35
- * Creates a new ObjectId instance
36
- * @return {external:MongoDBObjectId}
37
- */
38
- static create () {
39
- return new ObjectId()
40
- }
41
-
42
- /**
43
- * Checks a string is a valid ObjectId
44
- * @param {String} s String to check
45
- * @return {Boolean}
46
- */
47
- static isValid (s) {
48
- try {
49
- return ObjectIdUtils.parse(s).equals(s)
50
- } catch (e) {
51
- return false
52
- }
53
- }
54
-
55
- /**
56
- * Converts a string to an ObjectId
57
- * @param {String} s The string to convert
58
- * @return {external:MongoDBObjectId} The converted ID
59
- * @throws {Error}
60
- */
61
- static parse (s) {
62
- if (ObjectIdUtils.isObjectId(s)) {
63
- return s
64
- }
65
- if (!ObjectId.isValid(s)) {
66
- throw App.instance.errors.INVALID_OBJECTID.setData({ value: s })
67
- }
68
- return new ObjectId(s)
69
- }
70
-
71
- /**
72
- * Checks an input object for any strings which pass the parse check and convert matches to ObjectId instances
73
- * @param {Object} o Object to be checked
74
- */
75
- static parseIds (o) {
76
- if (o === undefined) {
77
- return
78
- }
79
- Object.entries(o).forEach(([k, v]) => {
80
- if (Utils.isObject(v)) {
81
- this.parseIds(v)
82
- } else if (Array.isArray(v)) {
83
- v.forEach((v2, i) => {
84
- try {
85
- if (typeof v2 === 'string') v[i] = this.parse(v2)
86
- } catch (e) {} // ignore failures
87
- this.parseIds(v2)
88
- })
89
- } else if (typeof v === 'string' && this.isValid(v)) {
90
- try {
91
- o[k] = this.parse(v)
92
- } catch (e) {} // ignore failures
93
- }
94
- })
95
- }
96
-
97
- static isObjectId (data) {
98
- return data instanceof ObjectId
99
- }
100
35
  }
101
36
 
102
37
  export default ObjectIdUtils
@@ -0,0 +1,10 @@
1
+ import { ObjectId } from 'mongodb'
2
+
3
+ /**
4
+ * Creates a new ObjectId instance
5
+ * @return {external:MongoDBObjectId}
6
+ * @memberof mongodb
7
+ */
8
+ export function create () {
9
+ return new ObjectId()
10
+ }
@@ -0,0 +1,11 @@
1
+ import { ObjectId } from 'mongodb'
2
+
3
+ /**
4
+ * Checks if a value is a MongoDB ObjectId instance
5
+ * @param {*} data The value to check
6
+ * @return {Boolean}
7
+ * @memberof mongodb
8
+ */
9
+ export function isObjectId (data) {
10
+ return data instanceof ObjectId
11
+ }
@@ -0,0 +1,15 @@
1
+ import { parse } from './parse.js'
2
+
3
+ /**
4
+ * Checks a string is a valid ObjectId
5
+ * @param {String} s String to check
6
+ * @return {Boolean}
7
+ * @memberof mongodb
8
+ */
9
+ export function isValid (s) {
10
+ try {
11
+ return parse(s).equals(s)
12
+ } catch (e) {
13
+ return false
14
+ }
15
+ }
@@ -0,0 +1,20 @@
1
+ import { App } from 'adapt-authoring-core'
2
+ import { ObjectId } from 'mongodb'
3
+ import { isObjectId } from './isObjectId.js'
4
+
5
+ /**
6
+ * Converts a string to an ObjectId
7
+ * @param {String} s The string to convert
8
+ * @return {external:MongoDBObjectId} The converted ID
9
+ * @throws {Error}
10
+ * @memberof mongodb
11
+ */
12
+ export function parse (s) {
13
+ if (isObjectId(s)) {
14
+ return s
15
+ }
16
+ if (!ObjectId.isValid(s)) {
17
+ throw App.instance.errors.INVALID_OBJECTID.setData({ value: s })
18
+ }
19
+ return new ObjectId(s)
20
+ }
@@ -0,0 +1,30 @@
1
+ import { isObject } from 'adapt-authoring-core'
2
+ import { isValid } from './isValid.js'
3
+ import { parse } from './parse.js'
4
+
5
+ /**
6
+ * Checks an input object for any strings which pass the parse check and convert matches to ObjectId instances
7
+ * @param {Object} o Object to be checked
8
+ * @memberof mongodb
9
+ */
10
+ export function parseIds (o) {
11
+ if (o === undefined) {
12
+ return
13
+ }
14
+ Object.entries(o).forEach(([k, v]) => {
15
+ if (isObject(v)) {
16
+ parseIds(v)
17
+ } else if (Array.isArray(v)) {
18
+ v.forEach((v2, i) => {
19
+ try {
20
+ if (typeof v2 === 'string') v[i] = parse(v2)
21
+ } catch (e) {} // ignore failures
22
+ parseIds(v2)
23
+ })
24
+ } else if (typeof v === 'string' && isValid(v)) {
25
+ try {
26
+ o[k] = parse(v)
27
+ } catch (e) {} // ignore failures
28
+ }
29
+ })
30
+ }
package/lib/utils.js ADDED
@@ -0,0 +1,5 @@
1
+ export { create } from './utils/create.js'
2
+ export { isObjectId } from './utils/isObjectId.js'
3
+ export { isValid } from './utils/isValid.js'
4
+ export { parse } from './utils/parse.js'
5
+ export { parseIds } from './utils/parseIds.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adapt-authoring-mongodb",
3
- "version": "1.1.4",
3
+ "version": "2.0.0",
4
4
  "description": "Module for saving to a MongoDB instance",
5
5
  "homepage": "https://github.com/adapt-security/adapt-authoring-mongodb",
6
6
  "license": "GPL-3.0",
@@ -8,12 +8,13 @@
8
8
  "main": "index.js",
9
9
  "repository": "github:adapt-security/adapt-authoring-mongodb",
10
10
  "dependencies": {
11
+ "adapt-authoring-core": "^2.0.0",
11
12
  "mongodb": "^7.0.0"
12
13
  },
13
14
  "peerDependencies": {
14
- "adapt-authoring-config": "^1.1.4",
15
- "adapt-authoring-core": "^1.7.0",
16
- "adapt-authoring-jsonschema": "^1.2.0"
15
+ "adapt-authoring-config": "^1.3.0",
16
+ "adapt-authoring-core": "^2.0.0",
17
+ "adapt-authoring-jsonschema": "^1.2.2"
17
18
  },
18
19
  "peerDependenciesMeta": {
19
20
  "adapt-authoring-config": {
@@ -58,5 +59,8 @@
58
59
  }
59
60
  ]
60
61
  ]
62
+ },
63
+ "scripts": {
64
+ "test": "node --test 'tests/**/*.spec.js'"
61
65
  }
62
66
  }
@@ -0,0 +1,104 @@
1
+ import { describe, it, mock } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import MongoDBModule from '../lib/MongoDBModule.js'
4
+
5
+ /**
6
+ * MongoDBModule extends AbstractModule and requires a running MongoDB connection.
7
+ * We test parseOptions and getError in isolation.
8
+ */
9
+
10
+ function createInstance () {
11
+ const mockApp = {
12
+ waitForModule: mock.fn(async () => {}),
13
+ errors: {
14
+ MONGO_ERROR: { setData: mock.fn(function (d) { return { code: 'MONGO_ERROR', ...d } }) },
15
+ MONGO_IMMUTABLE_FIELD: { setData: mock.fn(function (d) { return { code: 'MONGO_IMMUTABLE_FIELD', ...d } }) },
16
+ MONGO_DUPL_INDEX: { setData: mock.fn(function (d) { return { code: 'MONGO_DUPL_INDEX', ...d } }) },
17
+ MONGO_CONN_FAILED: { setData: mock.fn(function () { return this }) }
18
+ },
19
+ dependencyloader: {
20
+ moduleLoadedHook: { tap: () => {}, untap: () => {} }
21
+ }
22
+ }
23
+
24
+ const originalInit = MongoDBModule.prototype.init
25
+ MongoDBModule.prototype.init = async function () {}
26
+
27
+ const instance = new MongoDBModule(mockApp, { name: 'adapt-authoring-mongodb' })
28
+
29
+ MongoDBModule.prototype.init = originalInit
30
+
31
+ instance.log = mock.fn()
32
+
33
+ return { instance, mockApp }
34
+ }
35
+
36
+ describe('MongoDBModule', () => {
37
+ describe('#parseOptions()', () => {
38
+ it('should parse string limit to integer', () => {
39
+ const { instance } = createInstance()
40
+ const options = { limit: '10' }
41
+ instance.parseOptions(options)
42
+ assert.equal(options.limit, 10)
43
+ })
44
+
45
+ it('should parse string skip to integer', () => {
46
+ const { instance } = createInstance()
47
+ const options = { skip: '5' }
48
+ instance.parseOptions(options)
49
+ assert.equal(options.skip, 5)
50
+ })
51
+
52
+ it('should handle undefined options gracefully', () => {
53
+ const { instance } = createInstance()
54
+ instance.parseOptions(undefined)
55
+ })
56
+
57
+ it('should handle options without limit or skip', () => {
58
+ const { instance } = createInstance()
59
+ const options = { sort: { name: 1 } }
60
+ instance.parseOptions(options)
61
+ assert.deepEqual(options, { sort: { name: 1 } })
62
+ })
63
+
64
+ it('should keep numeric limit as-is', () => {
65
+ const { instance } = createInstance()
66
+ const options = { limit: 10 }
67
+ instance.parseOptions(options)
68
+ assert.equal(options.limit, 10)
69
+ })
70
+
71
+ it('should skip undefined limit', () => {
72
+ const { instance } = createInstance()
73
+ const options = { limit: undefined }
74
+ instance.parseOptions(options)
75
+ assert.equal(options.limit, undefined)
76
+ })
77
+ })
78
+
79
+ describe('#getError()', () => {
80
+ it('should return MONGO_IMMUTABLE_FIELD for error code 66', () => {
81
+ const { instance } = createInstance()
82
+ const result = instance.getError('test', 'update', { code: 66, message: 'immutable' })
83
+ assert.equal(result.code, 'MONGO_IMMUTABLE_FIELD')
84
+ })
85
+
86
+ it('should return MONGO_DUPL_INDEX for error code 11000', () => {
87
+ const { instance } = createInstance()
88
+ const result = instance.getError('test', 'insert', { code: 11000, message: 'duplicate' })
89
+ assert.equal(result.code, 'MONGO_DUPL_INDEX')
90
+ })
91
+
92
+ it('should return MONGO_ERROR for other error codes', () => {
93
+ const { instance } = createInstance()
94
+ const result = instance.getError('test', 'find', { code: 999, message: 'unknown' })
95
+ assert.equal(result.code, 'MONGO_ERROR')
96
+ })
97
+
98
+ it('should return MONGO_ERROR for errors without code', () => {
99
+ const { instance } = createInstance()
100
+ const result = instance.getError('test', 'delete', { message: 'fail' })
101
+ assert.equal(result.code, 'MONGO_ERROR')
102
+ })
103
+ })
104
+ })
@@ -0,0 +1,22 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { ObjectId } from 'mongodb'
4
+ import { create } from '../lib/utils/create.js'
5
+
6
+ describe('create()', () => {
7
+ it('should return an ObjectId instance', () => {
8
+ const id = create()
9
+ assert.ok(id instanceof ObjectId)
10
+ })
11
+
12
+ it('should create unique IDs each time', () => {
13
+ const id1 = create()
14
+ const id2 = create()
15
+ assert.notEqual(id1.toString(), id2.toString())
16
+ })
17
+
18
+ it('should return a 24-character hex string representation', () => {
19
+ const id = create()
20
+ assert.match(id.toString(), /^[0-9a-f]{24}$/)
21
+ })
22
+ })
@@ -0,0 +1,38 @@
1
+ import { describe, it } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { ObjectId } from 'mongodb'
4
+ import { isObjectId } from '../lib/utils/isObjectId.js'
5
+
6
+ describe('isObjectId()', () => {
7
+ it('should return true for an ObjectId instance', () => {
8
+ assert.equal(isObjectId(new ObjectId()), true)
9
+ })
10
+
11
+ it('should return false for a valid ObjectId string', () => {
12
+ assert.equal(isObjectId('507f1f77bcf86cd799439011'), false)
13
+ })
14
+
15
+ it('should return false for a plain string', () => {
16
+ assert.equal(isObjectId('hello'), false)
17
+ })
18
+
19
+ it('should return false for null', () => {
20
+ assert.equal(isObjectId(null), false)
21
+ })
22
+
23
+ it('should return false for undefined', () => {
24
+ assert.equal(isObjectId(undefined), false)
25
+ })
26
+
27
+ it('should return false for a number', () => {
28
+ assert.equal(isObjectId(12345), false)
29
+ })
30
+
31
+ it('should return false for a plain object', () => {
32
+ assert.equal(isObjectId({}), false)
33
+ })
34
+
35
+ it('should return false for an array', () => {
36
+ assert.equal(isObjectId([]), false)
37
+ })
38
+ })
@@ -0,0 +1,53 @@
1
+ import { describe, it, mock } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { ObjectId } from 'mongodb'
4
+ import App from 'adapt-authoring-core/lib/App.js'
5
+
6
+ mock.getter(App, 'instance', () => ({
7
+ errors: {
8
+ INVALID_OBJECTID: {
9
+ setData (data) {
10
+ const e = new Error('INVALID_OBJECTID')
11
+ e.data = data
12
+ return e
13
+ }
14
+ }
15
+ }
16
+ }))
17
+
18
+ const { isValid } = await import('../lib/utils/isValid.js')
19
+
20
+ describe('isValid()', () => {
21
+ it('should return true for a valid ObjectId string', () => {
22
+ const id = new ObjectId()
23
+ assert.equal(isValid(id.toString()), true)
24
+ })
25
+
26
+ it('should return true for a 24-char hex string', () => {
27
+ assert.equal(isValid('507f1f77bcf86cd799439011'), true)
28
+ })
29
+
30
+ it('should return false for a random string', () => {
31
+ assert.equal(isValid('not-an-objectid'), false)
32
+ })
33
+
34
+ it('should return false for an empty string', () => {
35
+ assert.equal(isValid(''), false)
36
+ })
37
+
38
+ it('should return false for a number', () => {
39
+ assert.equal(isValid(12345), false)
40
+ })
41
+
42
+ it('should return false for a 23-char hex string', () => {
43
+ assert.equal(isValid('507f1f77bcf86cd79943901'), false)
44
+ })
45
+
46
+ it('should return false for null', () => {
47
+ assert.equal(isValid(null), false)
48
+ })
49
+
50
+ it('should return false for undefined', () => {
51
+ assert.equal(isValid(undefined), false)
52
+ })
53
+ })
@@ -0,0 +1,70 @@
1
+ import { describe, it, mock } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { ObjectId } from 'mongodb'
4
+ import App from 'adapt-authoring-core/lib/App.js'
5
+
6
+ mock.getter(App, 'instance', () => ({
7
+ errors: {
8
+ INVALID_OBJECTID: {
9
+ setData (data) {
10
+ const e = new Error('INVALID_OBJECTID')
11
+ e.data = data
12
+ return e
13
+ }
14
+ }
15
+ }
16
+ }))
17
+
18
+ const { parse } = await import('../lib/utils/parse.js')
19
+
20
+ describe('parse()', () => {
21
+ it('should return same ObjectId if already an ObjectId instance', () => {
22
+ const id = new ObjectId()
23
+ assert.equal(parse(id), id)
24
+ })
25
+
26
+ it('should convert a valid ObjectId string to an ObjectId', () => {
27
+ const str = '507f1f77bcf86cd799439011'
28
+ const result = parse(str)
29
+ assert.ok(result instanceof ObjectId)
30
+ assert.equal(result.toString(), str)
31
+ })
32
+
33
+ it('should throw INVALID_OBJECTID for an invalid string', () => {
34
+ assert.throws(
35
+ () => parse('not-a-valid-id'),
36
+ (err) => {
37
+ assert.equal(err.message, 'INVALID_OBJECTID')
38
+ assert.deepEqual(err.data, { value: 'not-a-valid-id' })
39
+ return true
40
+ }
41
+ )
42
+ })
43
+
44
+ it('should throw INVALID_OBJECTID for an empty string', () => {
45
+ assert.throws(
46
+ () => parse(''),
47
+ (err) => {
48
+ assert.equal(err.message, 'INVALID_OBJECTID')
49
+ return true
50
+ }
51
+ )
52
+ })
53
+
54
+ it('should convert a 24-char hex string', () => {
55
+ const hex = 'aabbccddeeff00112233aabb'
56
+ const result = parse(hex)
57
+ assert.ok(result instanceof ObjectId)
58
+ assert.equal(result.toString(), hex)
59
+ })
60
+
61
+ it('should throw for a 23-char string', () => {
62
+ assert.throws(
63
+ () => parse('507f1f77bcf86cd79943901'),
64
+ (err) => {
65
+ assert.equal(err.message, 'INVALID_OBJECTID')
66
+ return true
67
+ }
68
+ )
69
+ })
70
+ })
@@ -0,0 +1,110 @@
1
+ import { describe, it, mock } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { ObjectId } from 'mongodb'
4
+ import App from 'adapt-authoring-core/lib/App.js'
5
+
6
+ mock.getter(App, 'instance', () => ({
7
+ errors: {
8
+ INVALID_OBJECTID: {
9
+ setData (data) {
10
+ const e = new Error('INVALID_OBJECTID')
11
+ e.data = data
12
+ return e
13
+ }
14
+ }
15
+ }
16
+ }))
17
+
18
+ const { parseIds } = await import('../lib/utils/parseIds.js')
19
+
20
+ describe('parseIds()', () => {
21
+ it('should handle undefined input gracefully', () => {
22
+ assert.equal(parseIds(undefined), undefined)
23
+ })
24
+
25
+ it('should convert valid ObjectId strings in a flat object', () => {
26
+ const idStr = new ObjectId().toString()
27
+ const obj = { _id: idStr }
28
+ parseIds(obj)
29
+ assert.ok(obj._id instanceof ObjectId)
30
+ assert.equal(obj._id.toString(), idStr)
31
+ })
32
+
33
+ it('should leave non-ObjectId strings unchanged', () => {
34
+ const obj = { name: 'test', value: 'hello' }
35
+ parseIds(obj)
36
+ assert.equal(obj.name, 'test')
37
+ assert.equal(obj.value, 'hello')
38
+ })
39
+
40
+ it('should recurse into nested objects', () => {
41
+ const idStr = new ObjectId().toString()
42
+ const obj = { nested: { _id: idStr } }
43
+ parseIds(obj)
44
+ assert.ok(obj.nested._id instanceof ObjectId)
45
+ assert.equal(obj.nested._id.toString(), idStr)
46
+ })
47
+
48
+ it('should convert ObjectId strings in arrays', () => {
49
+ const idStr = new ObjectId().toString()
50
+ const obj = { ids: [idStr] }
51
+ parseIds(obj)
52
+ assert.ok(obj.ids[0] instanceof ObjectId)
53
+ assert.equal(obj.ids[0].toString(), idStr)
54
+ })
55
+
56
+ it('should recurse into objects within arrays', () => {
57
+ const idStr = new ObjectId().toString()
58
+ const obj = { items: [{ _id: idStr }] }
59
+ parseIds(obj)
60
+ assert.ok(obj.items[0]._id instanceof ObjectId)
61
+ })
62
+
63
+ it('should leave non-ObjectId strings in arrays unchanged', () => {
64
+ const obj = { tags: ['hello', 'world'] }
65
+ parseIds(obj)
66
+ assert.equal(obj.tags[0], 'hello')
67
+ assert.equal(obj.tags[1], 'world')
68
+ })
69
+
70
+ it('should leave numbers and booleans unchanged', () => {
71
+ const obj = { count: 42, active: true }
72
+ parseIds(obj)
73
+ assert.equal(obj.count, 42)
74
+ assert.equal(obj.active, true)
75
+ })
76
+
77
+ it('should handle deeply nested structures', () => {
78
+ const idStr = new ObjectId().toString()
79
+ const obj = { a: { b: { c: { _id: idStr } } } }
80
+ parseIds(obj)
81
+ assert.ok(obj.a.b.c._id instanceof ObjectId)
82
+ })
83
+
84
+ it('should handle mixed content in an object', () => {
85
+ const idStr = new ObjectId().toString()
86
+ const obj = {
87
+ _id: idStr,
88
+ name: 'test',
89
+ count: 5,
90
+ nested: { value: 'keep' }
91
+ }
92
+ parseIds(obj)
93
+ assert.ok(obj._id instanceof ObjectId)
94
+ assert.equal(obj.name, 'test')
95
+ assert.equal(obj.count, 5)
96
+ assert.equal(obj.nested.value, 'keep')
97
+ })
98
+
99
+ it('should handle empty objects', () => {
100
+ const obj = {}
101
+ parseIds(obj)
102
+ assert.deepEqual(obj, {})
103
+ })
104
+
105
+ it('should handle objects with empty arrays', () => {
106
+ const obj = { items: [] }
107
+ parseIds(obj)
108
+ assert.deepEqual(obj.items, [])
109
+ })
110
+ })