adapt-authoring-mongodb 1.2.0 → 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.
- package/index.js +1 -0
- package/lib/MongoDBModule.js +11 -18
- package/lib/ObjectIdUtils.js +7 -72
- package/lib/utils/create.js +10 -0
- package/lib/utils/isObjectId.js +11 -0
- package/lib/utils/isValid.js +15 -0
- package/lib/utils/parse.js +20 -0
- package/lib/utils/parseIds.js +30 -0
- package/lib/utils.js +5 -0
- package/package.json +5 -4
- package/tests/MongoDBModule.spec.js +0 -9
- package/tests/utils-create.spec.js +22 -0
- package/tests/utils-isObjectId.spec.js +38 -0
- package/tests/utils-isValid.spec.js +53 -0
- package/tests/utils-parse.spec.js +70 -0
- package/tests/utils-parseIds.spec.js +110 -0
- package/tests/ObjectIdUtils.spec.js +0 -85
package/index.js
CHANGED
package/lib/MongoDBModule.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
149
|
-
|
|
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
|
-
|
|
172
|
-
|
|
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
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/lib/ObjectIdUtils.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { App
|
|
2
|
-
import {
|
|
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
|
-
*
|
|
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 (!
|
|
22
|
+
if (!isValid(value)) {
|
|
21
23
|
return false
|
|
22
24
|
}
|
|
23
25
|
try {
|
|
24
|
-
parentData[parentDataProperty] =
|
|
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,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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adapt-authoring-mongodb",
|
|
3
|
-
"version": "
|
|
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.
|
|
15
|
-
"adapt-authoring-core": "^
|
|
16
|
-
"adapt-authoring-jsonschema": "^1.2.
|
|
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": {
|
|
@@ -101,13 +101,4 @@ describe('MongoDBModule', () => {
|
|
|
101
101
|
assert.equal(result.code, 'MONGO_ERROR')
|
|
102
102
|
})
|
|
103
103
|
})
|
|
104
|
-
|
|
105
|
-
describe('#ObjectId', () => {
|
|
106
|
-
it('should return ObjectIdUtils', () => {
|
|
107
|
-
const { instance } = createInstance()
|
|
108
|
-
assert.ok(instance.ObjectId)
|
|
109
|
-
assert.equal(typeof instance.ObjectId.isValid, 'function')
|
|
110
|
-
assert.equal(typeof instance.ObjectId.create, 'function')
|
|
111
|
-
})
|
|
112
|
-
})
|
|
113
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
|
+
})
|
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test'
|
|
2
|
-
import assert from 'node:assert/strict'
|
|
3
|
-
import { ObjectId } from 'mongodb'
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* ObjectIdUtils relies on App.instance for error throwing in parse(),
|
|
7
|
-
* but we can test isValid, create, and isObjectId which don't.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
// Import will fail due to App.instance dependency in parse, so we
|
|
11
|
-
// extract the pure logic inline
|
|
12
|
-
const isObjectId = (data) => data instanceof ObjectId
|
|
13
|
-
|
|
14
|
-
function isValid (s) {
|
|
15
|
-
try {
|
|
16
|
-
const parsed = new ObjectId(s)
|
|
17
|
-
return parsed.equals(s)
|
|
18
|
-
} catch (e) {
|
|
19
|
-
return false
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
describe('ObjectIdUtils', () => {
|
|
24
|
-
describe('.create()', () => {
|
|
25
|
-
it('should create a new ObjectId', () => {
|
|
26
|
-
const id = new ObjectId()
|
|
27
|
-
assert.ok(id instanceof ObjectId)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('should create unique IDs each time', () => {
|
|
31
|
-
const id1 = new ObjectId()
|
|
32
|
-
const id2 = new ObjectId()
|
|
33
|
-
assert.notEqual(id1.toString(), id2.toString())
|
|
34
|
-
})
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
describe('.isValid()', () => {
|
|
38
|
-
it('should return true for a valid ObjectId string', () => {
|
|
39
|
-
const id = new ObjectId()
|
|
40
|
-
assert.equal(isValid(id.toString()), true)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('should return false for a random string', () => {
|
|
44
|
-
assert.equal(isValid('not-an-objectid'), false)
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('should return false for an empty string', () => {
|
|
48
|
-
assert.equal(isValid(''), false)
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('should return false for a number', () => {
|
|
52
|
-
assert.equal(isValid(12345), false)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('should return true for a 24-char hex string', () => {
|
|
56
|
-
assert.equal(isValid('507f1f77bcf86cd799439011'), true)
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('should return false for a 23-char hex string', () => {
|
|
60
|
-
assert.equal(isValid('507f1f77bcf86cd79943901'), false)
|
|
61
|
-
})
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
describe('.isObjectId()', () => {
|
|
65
|
-
it('should return true for an ObjectId instance', () => {
|
|
66
|
-
assert.equal(isObjectId(new ObjectId()), true)
|
|
67
|
-
})
|
|
68
|
-
|
|
69
|
-
it('should return false for a string', () => {
|
|
70
|
-
assert.equal(isObjectId('507f1f77bcf86cd799439011'), false)
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('should return false for null', () => {
|
|
74
|
-
assert.equal(isObjectId(null), false)
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
it('should return false for undefined', () => {
|
|
78
|
-
assert.equal(isObjectId(undefined), false)
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it('should return false for a plain object', () => {
|
|
82
|
-
assert.equal(isObjectId({}), false)
|
|
83
|
-
})
|
|
84
|
-
})
|
|
85
|
-
})
|