adapt-authoring-mongodb 3.0.1 → 3.1.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/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 { convertObjectIds } from './utils/convertObjectIds.js'
|
|
4
|
+
import { findDuplicates } from './utils/findDuplicates.js'
|
|
4
5
|
import { isValidObjectId } from './utils/isValidObjectId.js'
|
|
5
6
|
import { parseObjectId } from './utils/parseObjectId.js'
|
|
6
7
|
/**
|
|
@@ -67,7 +68,29 @@ class MongoDBModule extends AbstractModule {
|
|
|
67
68
|
try {
|
|
68
69
|
await this.getCollection(collectionName).createIndex(fieldOrSpec, options)
|
|
69
70
|
} catch (e) {
|
|
70
|
-
|
|
71
|
+
if (e.code === 11000) {
|
|
72
|
+
await this.logDuplicateIndexError(collectionName, fieldOrSpec, e)
|
|
73
|
+
} else {
|
|
74
|
+
this.log('warn', e.toString())
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Logs detailed information about duplicate index errors
|
|
81
|
+
* @param {String} collectionName The name of the MongoDB collection
|
|
82
|
+
* @param {String|Array|Object} fieldOrSpec Definition of the index
|
|
83
|
+
* @param {Error} error The original MongoDB error
|
|
84
|
+
*/
|
|
85
|
+
async logDuplicateIndexError (collectionName, fieldOrSpec, error) {
|
|
86
|
+
this.log('warn', `Duplicate index error on '${collectionName}': ${error.message}`)
|
|
87
|
+
try {
|
|
88
|
+
const duplicates = await findDuplicates(this.getCollection(collectionName), fieldOrSpec)
|
|
89
|
+
for (const { keyValue, _ids } of duplicates) {
|
|
90
|
+
this.log('warn', ` ${JSON.stringify(keyValue)} → [${_ids.join(', ')}]`)
|
|
91
|
+
}
|
|
92
|
+
} catch (queryError) {
|
|
93
|
+
this.log('warn', ` Could not query duplicates: ${queryError.message}`)
|
|
71
94
|
}
|
|
72
95
|
}
|
|
73
96
|
|
|
@@ -94,18 +117,20 @@ class MongoDBModule extends AbstractModule {
|
|
|
94
117
|
* Adds a new object to the database
|
|
95
118
|
* @param {String} collectionName The name of the MongoDB collection
|
|
96
119
|
* @param {Object} data
|
|
97
|
-
* @param {
|
|
120
|
+
* @param {Object} options Options to pass to the MongoDB driver
|
|
121
|
+
* @param {Boolean} [options.preserveId] If true, retains a pre-set data._id instead of auto-generating one
|
|
98
122
|
* @return {Promise} promise
|
|
99
123
|
* @see https://mongodb.github.io/node-mongodb-native/4.2/classes/Collection.html#insertOne
|
|
100
124
|
*/
|
|
101
|
-
async insert (collectionName, data, options) {
|
|
125
|
+
async insert (collectionName, data, options = {}) {
|
|
126
|
+
const { preserveId, ...mongoOptions } = options
|
|
102
127
|
convertObjectIds(data)
|
|
103
|
-
this.parseOptions(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
128
|
+
this.parseOptions(mongoOptions)
|
|
129
|
+
if (!preserveId) {
|
|
130
|
+
delete data._id
|
|
131
|
+
}
|
|
107
132
|
try {
|
|
108
|
-
const { insertedId } = await this.getCollection(collectionName).insertOne(data,
|
|
133
|
+
const { insertedId } = await this.getCollection(collectionName).insertOne(data, mongoOptions)
|
|
109
134
|
const [doc] = await this.find(collectionName, { _id: insertedId })
|
|
110
135
|
return doc
|
|
111
136
|
} catch (e) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finds documents with duplicate values for the given index fields.
|
|
3
|
+
* @param {import('mongodb').Collection} collection The MongoDB collection to search
|
|
4
|
+
* @param {String|Object} fieldOrSpec The index field spec (string key or object with field:direction pairs)
|
|
5
|
+
* @returns {Promise<Array<{keyValue: Object, _ids: Array}>>} Array of duplicate groups
|
|
6
|
+
*/
|
|
7
|
+
export async function findDuplicates (collection, fieldOrSpec) {
|
|
8
|
+
const fields = getFieldNames(fieldOrSpec)
|
|
9
|
+
const groupId = Object.fromEntries(fields.map(f => [f, `$${f}`]))
|
|
10
|
+
return collection.aggregate([
|
|
11
|
+
{ $group: { _id: groupId, _ids: { $push: '$_id' }, count: { $sum: 1 } } },
|
|
12
|
+
{ $match: { count: { $gt: 1 } } },
|
|
13
|
+
{ $project: { _id: 0, keyValue: '$_id', _ids: 1 } }
|
|
14
|
+
]).toArray()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Normalises a fieldOrSpec value into an array of field name strings.
|
|
19
|
+
* @param {String|Object} fieldOrSpec
|
|
20
|
+
* @returns {String[]}
|
|
21
|
+
*/
|
|
22
|
+
export function getFieldNames (fieldOrSpec) {
|
|
23
|
+
if (typeof fieldOrSpec === 'string') return [fieldOrSpec]
|
|
24
|
+
return Object.keys(fieldOrSpec)
|
|
25
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, it, mock } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { ObjectId } from 'mongodb'
|
|
4
|
+
|
|
5
|
+
describe('insert() preserveId', () => {
|
|
6
|
+
const presetId = new ObjectId()
|
|
7
|
+
const generatedId = new ObjectId()
|
|
8
|
+
|
|
9
|
+
function createContext () {
|
|
10
|
+
let insertedData
|
|
11
|
+
return {
|
|
12
|
+
context: {
|
|
13
|
+
getCollection: mock.fn(() => ({
|
|
14
|
+
insertOne: mock.fn(async (data) => {
|
|
15
|
+
insertedData = { ...data }
|
|
16
|
+
return { insertedId: data._id ?? generatedId }
|
|
17
|
+
})
|
|
18
|
+
})),
|
|
19
|
+
find: mock.fn(async () => [{ _id: generatedId, name: 'test' }])
|
|
20
|
+
},
|
|
21
|
+
getInsertedData: () => insertedData
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function insert (context, collectionName, data, options = {}) {
|
|
26
|
+
const { preserveId, ...mongoOptions } = options
|
|
27
|
+
if (!preserveId) {
|
|
28
|
+
delete data._id
|
|
29
|
+
if (data.$set) delete data.$set._id
|
|
30
|
+
}
|
|
31
|
+
const { insertedId } = await context.getCollection(collectionName).insertOne(data, mongoOptions)
|
|
32
|
+
const [doc] = await context.find(collectionName, { _id: insertedId })
|
|
33
|
+
return doc
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
it('should strip _id by default', async () => {
|
|
37
|
+
const { context, getInsertedData } = createContext()
|
|
38
|
+
await insert(context, 'test', { _id: presetId, name: 'test' })
|
|
39
|
+
assert.equal(getInsertedData()._id, undefined)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('should strip _id from $set by default', async () => {
|
|
43
|
+
const { context, getInsertedData } = createContext()
|
|
44
|
+
await insert(context, 'test', { $set: { _id: presetId, name: 'test' } })
|
|
45
|
+
assert.equal(getInsertedData().$set._id, undefined)
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should preserve _id when preserveId is true', async () => {
|
|
49
|
+
const { context, getInsertedData } = createContext()
|
|
50
|
+
await insert(context, 'test', { _id: presetId, name: 'test' }, { preserveId: true })
|
|
51
|
+
assert.deepEqual(getInsertedData()._id, presetId)
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('should not pass preserveId to the MongoDB driver', async () => {
|
|
55
|
+
const { context } = createContext()
|
|
56
|
+
await insert(context, 'test', { _id: presetId, name: 'test' }, { preserveId: true })
|
|
57
|
+
const insertOne = context.getCollection.mock.calls[0].result.insertOne
|
|
58
|
+
const driverOptions = insertOne.mock.calls[0].arguments[1]
|
|
59
|
+
assert.equal(driverOptions?.preserveId, undefined)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should return the inserted document', async () => {
|
|
63
|
+
const { context } = createContext()
|
|
64
|
+
const result = await insert(context, 'test', { name: 'test' })
|
|
65
|
+
assert.deepEqual(result, { _id: generatedId, name: 'test' })
|
|
66
|
+
})
|
|
67
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, it } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { findDuplicates, getFieldNames } from '../lib/utils/findDuplicates.js'
|
|
4
|
+
|
|
5
|
+
describe('getFieldNames()', () => {
|
|
6
|
+
it('should return a single-element array for a string input', () => {
|
|
7
|
+
assert.deepEqual(getFieldNames('email'), ['email'])
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('should return the keys of an object input', () => {
|
|
11
|
+
assert.deepEqual(getFieldNames({ _courseId: 1, _friendlyId: 1 }), ['_courseId', '_friendlyId'])
|
|
12
|
+
})
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
describe('findDuplicates()', () => {
|
|
16
|
+
function mockCollection (aggregateResult) {
|
|
17
|
+
return {
|
|
18
|
+
aggregate () {
|
|
19
|
+
return { toArray: async () => aggregateResult }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
it('should return duplicate groups from the collection', async () => {
|
|
25
|
+
const expected = [
|
|
26
|
+
{ keyValue: { email: 'a@b.com' }, _ids: ['id1', 'id2'] }
|
|
27
|
+
]
|
|
28
|
+
const result = await findDuplicates(mockCollection(expected), 'email')
|
|
29
|
+
assert.deepEqual(result, expected)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('should return an empty array when no duplicates exist', async () => {
|
|
33
|
+
const result = await findDuplicates(mockCollection([]), 'email')
|
|
34
|
+
assert.deepEqual(result, [])
|
|
35
|
+
})
|
|
36
|
+
})
|