adapt-authoring-mongodb 3.0.1 → 3.2.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.
|
@@ -1,32 +1,17 @@
|
|
|
1
1
|
name: Release
|
|
2
|
+
|
|
2
3
|
on:
|
|
3
4
|
push:
|
|
4
5
|
branches:
|
|
5
6
|
- master
|
|
6
7
|
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write
|
|
10
|
+
issues: write
|
|
11
|
+
pull-requests: write
|
|
12
|
+
id-token: write
|
|
13
|
+
packages: write
|
|
14
|
+
|
|
7
15
|
jobs:
|
|
8
16
|
release:
|
|
9
|
-
|
|
10
|
-
runs-on: ubuntu-latest
|
|
11
|
-
permissions:
|
|
12
|
-
contents: write # to be able to publish a GitHub release
|
|
13
|
-
issues: write # to be able to comment on released issues
|
|
14
|
-
pull-requests: write # to be able to comment on released pull requests
|
|
15
|
-
id-token: write # to enable use of OIDC for trusted publishing and npm provenance
|
|
16
|
-
steps:
|
|
17
|
-
- name: Checkout
|
|
18
|
-
uses: actions/checkout@v3
|
|
19
|
-
with:
|
|
20
|
-
fetch-depth: 0
|
|
21
|
-
- name: Setup Node.js
|
|
22
|
-
uses: actions/setup-node@v3
|
|
23
|
-
with:
|
|
24
|
-
node-version: 'lts/*'
|
|
25
|
-
- name: Update npm
|
|
26
|
-
run: npm install -g npm@latest
|
|
27
|
-
- name: Install dependencies
|
|
28
|
-
run: npm install
|
|
29
|
-
- name: Release
|
|
30
|
-
env:
|
|
31
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
32
|
-
run: npx semantic-release
|
|
17
|
+
uses: adaptlearning/semantic-release-config/.github/workflows/release.yml@master
|
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
|
/**
|
|
@@ -11,7 +12,6 @@ import { parseObjectId } from './utils/parseObjectId.js'
|
|
|
11
12
|
class MongoDBModule extends AbstractModule {
|
|
12
13
|
/** @override */
|
|
13
14
|
async init () {
|
|
14
|
-
await this.app.waitForModule('config')
|
|
15
15
|
/**
|
|
16
16
|
* Reference to the MongDB client
|
|
17
17
|
* @type {external:MongoDBMongoClient}
|
|
@@ -67,7 +67,29 @@ class MongoDBModule extends AbstractModule {
|
|
|
67
67
|
try {
|
|
68
68
|
await this.getCollection(collectionName).createIndex(fieldOrSpec, options)
|
|
69
69
|
} catch (e) {
|
|
70
|
-
|
|
70
|
+
if (e.code === 11000) {
|
|
71
|
+
await this.logDuplicateIndexError(collectionName, fieldOrSpec, e)
|
|
72
|
+
} else {
|
|
73
|
+
this.log('warn', e.toString())
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Logs detailed information about duplicate index errors
|
|
80
|
+
* @param {String} collectionName The name of the MongoDB collection
|
|
81
|
+
* @param {String|Array|Object} fieldOrSpec Definition of the index
|
|
82
|
+
* @param {Error} error The original MongoDB error
|
|
83
|
+
*/
|
|
84
|
+
async logDuplicateIndexError (collectionName, fieldOrSpec, error) {
|
|
85
|
+
this.log('warn', `Duplicate index error on '${collectionName}': ${error.message}`)
|
|
86
|
+
try {
|
|
87
|
+
const duplicates = await findDuplicates(this.getCollection(collectionName), fieldOrSpec)
|
|
88
|
+
for (const { keyValue, _ids } of duplicates) {
|
|
89
|
+
this.log('warn', ` ${JSON.stringify(keyValue)} → [${_ids.join(', ')}]`)
|
|
90
|
+
}
|
|
91
|
+
} catch (queryError) {
|
|
92
|
+
this.log('warn', ` Could not query duplicates: ${queryError.message}`)
|
|
71
93
|
}
|
|
72
94
|
}
|
|
73
95
|
|
|
@@ -94,18 +116,20 @@ class MongoDBModule extends AbstractModule {
|
|
|
94
116
|
* Adds a new object to the database
|
|
95
117
|
* @param {String} collectionName The name of the MongoDB collection
|
|
96
118
|
* @param {Object} data
|
|
97
|
-
* @param {
|
|
119
|
+
* @param {Object} options Options to pass to the MongoDB driver
|
|
120
|
+
* @param {Boolean} [options.preserveId] If true, retains a pre-set data._id instead of auto-generating one
|
|
98
121
|
* @return {Promise} promise
|
|
99
122
|
* @see https://mongodb.github.io/node-mongodb-native/4.2/classes/Collection.html#insertOne
|
|
100
123
|
*/
|
|
101
|
-
async insert (collectionName, data, options) {
|
|
124
|
+
async insert (collectionName, data, options = {}) {
|
|
125
|
+
const { preserveId, ...mongoOptions } = options
|
|
102
126
|
convertObjectIds(data)
|
|
103
|
-
this.parseOptions(
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
127
|
+
this.parseOptions(mongoOptions)
|
|
128
|
+
if (!preserveId) {
|
|
129
|
+
delete data._id
|
|
130
|
+
}
|
|
107
131
|
try {
|
|
108
|
-
const { insertedId } = await this.getCollection(collectionName).insertOne(data,
|
|
132
|
+
const { insertedId } = await this.getCollection(collectionName).insertOne(data, mongoOptions)
|
|
109
133
|
const [doc] = await this.find(collectionName, { _id: insertedId })
|
|
110
134
|
return doc
|
|
111
135
|
} catch (e) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Finds documents with duplicate values for the given index fields.
|
|
3
|
+
* @param {external:MongoDBCollection} 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "adapt-authoring-mongodb",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.2.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,46 +8,19 @@
|
|
|
8
8
|
"main": "index.js",
|
|
9
9
|
"repository": "github:adapt-security/adapt-authoring-mongodb",
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"adapt-authoring-core": "^
|
|
11
|
+
"adapt-authoring-core": "^3.0.0",
|
|
12
12
|
"mongodb": "^7.0.0"
|
|
13
13
|
},
|
|
14
14
|
"peerDependencies": {
|
|
15
|
-
"adapt-authoring-
|
|
16
|
-
"adapt-authoring-core": "^2.0.0",
|
|
15
|
+
"adapt-authoring-core": "^3.0.0",
|
|
17
16
|
"adapt-authoring-jsonschema": "^1.2.2"
|
|
18
17
|
},
|
|
19
18
|
"devDependencies": {
|
|
20
|
-
"@semantic-release
|
|
21
|
-
"conventional-changelog-eslint": "^6.0.0",
|
|
22
|
-
"semantic-release": "^25.0.2",
|
|
19
|
+
"@adaptlearning/semantic-release-config": "^1.0.0",
|
|
23
20
|
"standard": "^17.1.0"
|
|
24
21
|
},
|
|
25
22
|
"release": {
|
|
26
|
-
"
|
|
27
|
-
[
|
|
28
|
-
"@semantic-release/commit-analyzer",
|
|
29
|
-
{
|
|
30
|
-
"preset": "eslint"
|
|
31
|
-
}
|
|
32
|
-
],
|
|
33
|
-
[
|
|
34
|
-
"@semantic-release/release-notes-generator",
|
|
35
|
-
{
|
|
36
|
-
"preset": "eslint"
|
|
37
|
-
}
|
|
38
|
-
],
|
|
39
|
-
"@semantic-release/npm",
|
|
40
|
-
"@semantic-release/github",
|
|
41
|
-
[
|
|
42
|
-
"@semantic-release/git",
|
|
43
|
-
{
|
|
44
|
-
"assets": [
|
|
45
|
-
"package.json"
|
|
46
|
-
],
|
|
47
|
-
"message": "Chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
|
48
|
-
}
|
|
49
|
-
]
|
|
50
|
-
]
|
|
23
|
+
"extends": "@adaptlearning/semantic-release-config"
|
|
51
24
|
},
|
|
52
25
|
"scripts": {
|
|
53
26
|
"test": "node --test 'tests/**/*.spec.js'"
|
|
@@ -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
|
+
})
|