adapt-authoring-mongodb 0.0.1
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/.eslintignore +1 -0
- package/.eslintrc +14 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +55 -0
- package/.github/ISSUE_TEMPLATE/config.yml +1 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +22 -0
- package/.github/dependabot.yml +11 -0
- package/.github/pull_request_template.md +25 -0
- package/.github/workflows/labelled_prs.yml +16 -0
- package/.github/workflows/new.yml +19 -0
- package/adapt-authoring.json +9 -0
- package/conf/config.schema.json +16 -0
- package/docs/using-mongodb.md +66 -0
- package/errors/errors.json +43 -0
- package/index.js +7 -0
- package/lib/MongoDBModule.js +270 -0
- package/lib/ObjectIdUtils.js +102 -0
- package/lib/typedefs.js +63 -0
- package/package.json +20 -0
- package/tests/mongoDBModule.spec.js +109 -0
- package/tests/objectIdUtils.spec.js +37 -0
package/.eslintignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
node_modules
|
package/.eslintrc
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
name: Bug Report
|
|
2
|
+
description: File a bug report
|
|
3
|
+
labels: ["bug"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
Thanks for taking the time to fill out this bug report!
|
|
9
|
+
- type: textarea
|
|
10
|
+
id: description
|
|
11
|
+
attributes:
|
|
12
|
+
label: What happened?
|
|
13
|
+
description: Please describe the issue
|
|
14
|
+
validations:
|
|
15
|
+
required: true
|
|
16
|
+
- type: textarea
|
|
17
|
+
id: expected
|
|
18
|
+
attributes:
|
|
19
|
+
label: Expected behaviour
|
|
20
|
+
description: Tell us what should have happened
|
|
21
|
+
- type: textarea
|
|
22
|
+
id: repro-steps
|
|
23
|
+
attributes:
|
|
24
|
+
label: Steps to reproduce
|
|
25
|
+
description: Tell us how to reproduce the issue
|
|
26
|
+
validations:
|
|
27
|
+
required: true
|
|
28
|
+
- type: input
|
|
29
|
+
id: aat-version
|
|
30
|
+
attributes:
|
|
31
|
+
label: Authoring tool version
|
|
32
|
+
description: What version of the Adapt authoring tool are you running?
|
|
33
|
+
validations:
|
|
34
|
+
required: true
|
|
35
|
+
- type: input
|
|
36
|
+
id: fw-version
|
|
37
|
+
attributes:
|
|
38
|
+
label: Framework version
|
|
39
|
+
description: What version of the Adapt framework are you running?
|
|
40
|
+
- type: dropdown
|
|
41
|
+
id: browsers
|
|
42
|
+
attributes:
|
|
43
|
+
label: What browsers are you seeing the problem on?
|
|
44
|
+
multiple: true
|
|
45
|
+
options:
|
|
46
|
+
- Firefox
|
|
47
|
+
- Chrome
|
|
48
|
+
- Safari
|
|
49
|
+
- Microsoft Edge
|
|
50
|
+
- type: textarea
|
|
51
|
+
id: logs
|
|
52
|
+
attributes:
|
|
53
|
+
label: Relevant log output
|
|
54
|
+
description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
|
|
55
|
+
render: sh
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
blank_issues_enabled: false
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Feature request
|
|
2
|
+
description: Request a new feature
|
|
3
|
+
labels: ["enhancement"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
Thanks for taking the time to request a new feature in the Adapt authoring tool! The Adapt team will consider all new feature requests, but unfortunately cannot commit to every one.
|
|
9
|
+
- type: textarea
|
|
10
|
+
id: description
|
|
11
|
+
attributes:
|
|
12
|
+
label: Feature description
|
|
13
|
+
description: Please describe your feature request
|
|
14
|
+
validations:
|
|
15
|
+
required: true
|
|
16
|
+
- type: checkboxes
|
|
17
|
+
id: contribute
|
|
18
|
+
attributes:
|
|
19
|
+
label: Can you work on this feature?
|
|
20
|
+
description: If you are able to commit your own time to work on this feature, it will greatly increase the liklihood of it being implemented by the core dev team. Otherwise, it will be triaged and prioritised alongside the core team's current priorities.
|
|
21
|
+
options:
|
|
22
|
+
- label: I can contribute
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# To get started with Dependabot version updates, you'll need to specify which
|
|
2
|
+
# package ecosystems to update and where the package manifests are located.
|
|
3
|
+
# Please see the documentation for all configuration options:
|
|
4
|
+
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
5
|
+
|
|
6
|
+
version: 2
|
|
7
|
+
updates:
|
|
8
|
+
- package-ecosystem: "npm" # See documentation for possible values
|
|
9
|
+
directory: "/" # Location of package manifests
|
|
10
|
+
schedule:
|
|
11
|
+
interval: "weekly"
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[//]: # (Please title your PR according to eslint commit conventions)
|
|
2
|
+
[//]: # (See https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-eslint#eslint-convention for details)
|
|
3
|
+
|
|
4
|
+
[//]: # (Add a link to the original issue)
|
|
5
|
+
|
|
6
|
+
[//]: # (Delete as appropriate)
|
|
7
|
+
### Fix
|
|
8
|
+
* A sentence describing each fix
|
|
9
|
+
|
|
10
|
+
### Update
|
|
11
|
+
* A sentence describing each udpate
|
|
12
|
+
|
|
13
|
+
### New
|
|
14
|
+
* A sentence describing each new feature
|
|
15
|
+
|
|
16
|
+
### Breaking
|
|
17
|
+
* A sentence describing each breaking change
|
|
18
|
+
|
|
19
|
+
[//]: # (List appropriate steps for testing if needed)
|
|
20
|
+
### Testing
|
|
21
|
+
1. Steps for testing
|
|
22
|
+
|
|
23
|
+
[//]: # (Mention any other dependencies)
|
|
24
|
+
|
|
25
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: Add labelled PRs to project
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
types: [ labeled ]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
add-to-project:
|
|
9
|
+
if: ${{ github.event.label.name == 'dependencies' }}
|
|
10
|
+
name: Add to main project
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/add-to-project@v0.1.0
|
|
14
|
+
with:
|
|
15
|
+
project-url: https://github.com/orgs/adapt-security/projects/5
|
|
16
|
+
github-token: ${{ secrets.PROJECTS_SECRET }}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
name: Add to main project
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
issues:
|
|
5
|
+
types:
|
|
6
|
+
- opened
|
|
7
|
+
pull_request:
|
|
8
|
+
types:
|
|
9
|
+
- opened
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
add-to-project:
|
|
13
|
+
name: Add to main project
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/add-to-project@v0.1.0
|
|
17
|
+
with:
|
|
18
|
+
project-url: https://github.com/orgs/adapt-security/projects/5
|
|
19
|
+
github-token: ${{ secrets.PROJECTS_SECRET }}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"connectionUri": {
|
|
6
|
+
"description": "The MongoDB connection URI used to connect to your MongoDB deployment.",
|
|
7
|
+
"type": "string",
|
|
8
|
+
"format": "uri",
|
|
9
|
+
"examples": [
|
|
10
|
+
"mongodb://0.0.0.0/adapt-authoring",
|
|
11
|
+
"mongodb://test1.adaptlearning.org:27018,test2.adaptlearning.org:27019/?replicaSet=test"
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"required": ["connectionUri"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Using MongoDB
|
|
2
|
+
The `adapt-authoring-mongodb` module adds the ability to work with MongoDB databases.
|
|
3
|
+
|
|
4
|
+
## Installing MongoDB
|
|
5
|
+
To save data with the authoring tool, you'll need either a local MongoDB install, or a hosted solution.
|
|
6
|
+
|
|
7
|
+
If you're new to hosting web applications/MongoDB and are intending to make your install accessible via the internet, we'd suggest going with a hosted solution.
|
|
8
|
+
|
|
9
|
+
For instructions on installing MongoDB, see the [MongoDB docs](https://docs.mongodb.com/manual/installation/#mongodb-community-edition-installation-tutorials).
|
|
10
|
+
|
|
11
|
+
## Using the module
|
|
12
|
+
The `adapt-authoring-mongodb` module uses MongoDB's [Node.js driver](https://mongodb.github.io/node-mongodb-native/4.2) behind-the-scenes for communicating with MongoDB.
|
|
13
|
+
|
|
14
|
+
Where possible, the MongoDBModule API has been designed to mirror the MongoDB Node.js driver API in both naming convensions and parameter naming/order. In some cases this has been changed for ease-of-use (e.g. `insertOne` has been renamed to `insert`). Please see the [Adapt authoring reference for the MongoDBModule](/class/node_modules/adapt-authoring-mongodb/lib/MongoDBModule.js~MongoDBModule.html) for more information (details on which of the MongoDB Node.js driver functions are used is specified there).
|
|
15
|
+
|
|
16
|
+
> If you're new to working with MongoDB, check out this [Quick Start](https://mongodb.github.io/node-mongodb-native/4.2/#quick-start) guide in the official docs for a good overview.
|
|
17
|
+
|
|
18
|
+
### Basic use
|
|
19
|
+
The following functions provide the most common functionality, and will likely be the functions you use most often. Please see the [API reference]([/class/node_modules/adapt-authoring-mongodb/lib/MongoDBModule.js~MongoDBModule.html](https://tomtaylor.codes/ls/jsdoc3/MongoDBModule.html)) for full details.
|
|
20
|
+
|
|
21
|
+
#### `find(collectionName, query, options)`
|
|
22
|
+
Retrieves a document.
|
|
23
|
+
|
|
24
|
+
#### `insert(collectionName, data, options)`
|
|
25
|
+
Inserts a new document.
|
|
26
|
+
|
|
27
|
+
#### `replace(collectionName, query, data, options)`
|
|
28
|
+
Completely replaces an existing document.
|
|
29
|
+
|
|
30
|
+
#### `update(collectionName, query, data, options)`
|
|
31
|
+
Updates only specific fields of an existing document.
|
|
32
|
+
|
|
33
|
+
#### `delete(collectionName, query, options)`
|
|
34
|
+
Removes an existing document.
|
|
35
|
+
|
|
36
|
+
### Querying the database
|
|
37
|
+
The `find` function is used to retrieve documents from the database.
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
#### Examples
|
|
41
|
+
Note that the MongoDB module, as with the rest of the application, makes heavy use of promises, and will return a pending Promise for all relevant functions.
|
|
42
|
+
|
|
43
|
+
Inserting a document into the 'test' collection:
|
|
44
|
+
```
|
|
45
|
+
try {
|
|
46
|
+
const data = await mongodb.insert('test', { hello: 'world' });
|
|
47
|
+
} catch(e) {
|
|
48
|
+
// handle error
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Advanced use
|
|
53
|
+
It is possible to access the Node.js driver API directly from the MongoDBModule instance to allow for extra functionality not covered by the MongoDBModule itself. An example of this being creating an aggregation pipeline.
|
|
54
|
+
|
|
55
|
+
There are two methods of accessing the driver API. Which one you use is entirely up to you, and mostly comes down to code brevity:
|
|
56
|
+
- Using the MongoDB client instance [[Adapt docs](https://tomtaylor.codes/ls/jsdoc3/MongoDBModule.html#client), [MongoDB Node.js driver docs](https://mongodb.github.io/node-mongodb-native/3.6/api/MongoClient.html)]
|
|
57
|
+
- Using the MongoDB collection [[Adapt docs](), [MongoDB Node.js driver docs](https://tomtaylor.codes/ls/jsdoc3/MongoDBModule.html#getCollection)]
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
// using the client instance
|
|
61
|
+
mongodb.client.db.collection('mycollection').aggregate(/* args */)
|
|
62
|
+
// using MongoDBModule#getCollection
|
|
63
|
+
mongodb.getCollection('mycollection').aggregate(/* args */)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
See the [MongoDB Node.js driver docs](https://mongodb.github.io/node-mongodb-native/4.2/classes/Collection.html) for the full API.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"INVALID_OBJECTID": {
|
|
3
|
+
"data": {
|
|
4
|
+
"value": "The value"
|
|
5
|
+
},
|
|
6
|
+
"description": "Not a valid ObjectId",
|
|
7
|
+
"statusCode": 400
|
|
8
|
+
},
|
|
9
|
+
"MONGO_CONN_FAILED": {
|
|
10
|
+
"data": {
|
|
11
|
+
"error": "The error message"
|
|
12
|
+
},
|
|
13
|
+
"description": "An error occurred connecting to the MongoDB instance",
|
|
14
|
+
"statusCode": 500
|
|
15
|
+
},
|
|
16
|
+
"MONGO_DUPL_INDEX": {
|
|
17
|
+
"data": {
|
|
18
|
+
"action": "The action being performed on the database collection",
|
|
19
|
+
"collectionName": "Name of the collection being processed",
|
|
20
|
+
"error": "The error message"
|
|
21
|
+
},
|
|
22
|
+
"description": "A document already exists with the same indexed value",
|
|
23
|
+
"statusCode": 400
|
|
24
|
+
},
|
|
25
|
+
"MONGO_ERROR": {
|
|
26
|
+
"data": {
|
|
27
|
+
"action": "The action being performed on the database collection",
|
|
28
|
+
"collectionName": "Name of the collection being processed",
|
|
29
|
+
"error": "The error message"
|
|
30
|
+
},
|
|
31
|
+
"description": "An error occurred while performing a MongoDB action",
|
|
32
|
+
"statusCode": 500
|
|
33
|
+
},
|
|
34
|
+
"MONGO_IMMUTABLE_FIELD": {
|
|
35
|
+
"data": {
|
|
36
|
+
"action": "The action being performed on the database collection",
|
|
37
|
+
"collectionName": "Name of the collection being processed",
|
|
38
|
+
"error": "The error message"
|
|
39
|
+
},
|
|
40
|
+
"description": "Attempting to modify an immutable field",
|
|
41
|
+
"statusCode": 400
|
|
42
|
+
}
|
|
43
|
+
}
|
package/index.js
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { AbstractModule } from 'adapt-authoring-core'
|
|
2
|
+
import { MongoClient } from 'mongodb'
|
|
3
|
+
import ObjectIdUtils from './ObjectIdUtils.js'
|
|
4
|
+
/**
|
|
5
|
+
* Represents a single MongoDB server instance
|
|
6
|
+
* @memberof mongodb
|
|
7
|
+
* @extends {AbstractModule}
|
|
8
|
+
*/
|
|
9
|
+
class MongoDBModule extends AbstractModule {
|
|
10
|
+
/** @override */
|
|
11
|
+
async init () {
|
|
12
|
+
await this.app.waitForModule('config')
|
|
13
|
+
/**
|
|
14
|
+
* Reference to the MongDB client
|
|
15
|
+
* @type {external:MongoDBMongoClient}
|
|
16
|
+
*/
|
|
17
|
+
this.client = new MongoClient(this.getConfig('connectionUri'), { ignoreUndefined: true })
|
|
18
|
+
await this.connect()
|
|
19
|
+
// add custom keywords to JSON Schema validator
|
|
20
|
+
await ObjectIdUtils.addSchemaKeyword()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Connects to the database
|
|
25
|
+
* @return {Promise}
|
|
26
|
+
*/
|
|
27
|
+
async connect () {
|
|
28
|
+
try {
|
|
29
|
+
await this.client.connect()
|
|
30
|
+
const { hosts, dbName } = this.client.options
|
|
31
|
+
this.log('info', `connected to ${dbName} on ${hosts}`)
|
|
32
|
+
} catch (e) {
|
|
33
|
+
throw this.app.errors.MONGO_CONN_FAILED
|
|
34
|
+
.setData({ error: e.message })
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Get all the db statistics
|
|
40
|
+
* @return {Promise}
|
|
41
|
+
* @see https://mongodb.github.io/node-mongodb-native/4.2/classes/Db.html#stats
|
|
42
|
+
*/
|
|
43
|
+
async getStats () {
|
|
44
|
+
return this.client.db().stats()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Returns the associated MongoDB collection
|
|
49
|
+
* @param {String} collectionName The name of the MongoDB collection
|
|
50
|
+
* @return {external:MongoDBCollection}
|
|
51
|
+
* @see https://mongodb.github.io/node-mongodb-native/4.2/classes/Collection.html
|
|
52
|
+
*/
|
|
53
|
+
getCollection (collectionName) {
|
|
54
|
+
return this.client.db().collection(collectionName)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Set an index on a MongoDB collection
|
|
59
|
+
* @param {String} collectionName The name of the MongoDB collection
|
|
60
|
+
* @param {String|Array|Object} fieldOrSpec Definition of the index
|
|
61
|
+
* @param {external:MongoDBCreateIndexesOptions} options Options to pass to the MongoDB driver
|
|
62
|
+
* @return {Promise}
|
|
63
|
+
* @see https://mongodb.github.io/node-mongodb-native/4.2/classes/Collection.html#createIndex
|
|
64
|
+
*/
|
|
65
|
+
async setIndex (collectionName, fieldOrSpec, options) {
|
|
66
|
+
try {
|
|
67
|
+
await this.getCollection(collectionName).createIndex(fieldOrSpec, options)
|
|
68
|
+
} catch (e) {
|
|
69
|
+
this.log('warn', e.toString())
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Makes sure options are in the correct format.
|
|
75
|
+
* @param {Object} options The options to parse
|
|
76
|
+
*/
|
|
77
|
+
parseOptions (options) {
|
|
78
|
+
if (!options) {
|
|
79
|
+
return
|
|
80
|
+
}
|
|
81
|
+
['limit', 'skip'].forEach(o => {
|
|
82
|
+
if (options[o] === undefined) return
|
|
83
|
+
try {
|
|
84
|
+
options[o] = parseInt(options[o])
|
|
85
|
+
} catch (e) {
|
|
86
|
+
this.log('warn', `value for option '${o}' is in an unexpected format and will be ignored`)
|
|
87
|
+
delete options[o]
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Adds a new object to the database
|
|
94
|
+
* @param {String} collectionName The name of the MongoDB collection
|
|
95
|
+
* @param {Object} data
|
|
96
|
+
* @param {external:MongoDBInsertOneOptions} options Options to pass to the MongoDB driver
|
|
97
|
+
* @return {Promise} promise
|
|
98
|
+
* @see https://mongodb.github.io/node-mongodb-native/4.2/classes/Collection.html#insertOne
|
|
99
|
+
*/
|
|
100
|
+
async insert (collectionName, data, options) {
|
|
101
|
+
this.ObjectId.parseIds(data)
|
|
102
|
+
this.parseOptions(options)
|
|
103
|
+
// MongoDB doesn't like the explicit setting of _id
|
|
104
|
+
delete data._id
|
|
105
|
+
if (data.$set) delete data.$set._id
|
|
106
|
+
try {
|
|
107
|
+
const { insertedId } = await this.getCollection(collectionName).insertOne(data, options)
|
|
108
|
+
const [doc] = await this.find(collectionName, { _id: insertedId })
|
|
109
|
+
return doc
|
|
110
|
+
} catch (e) {
|
|
111
|
+
this.log('error', `failed to insert doc, ${e.message}`)
|
|
112
|
+
throw this.getError(collectionName, 'insert', e)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Retrieves a new object from the database
|
|
118
|
+
* @param {String} collectionName The name of the MongoDB collection
|
|
119
|
+
* @param {Object} query
|
|
120
|
+
* @param {external:MongoDBFindOptions} options Options to pass to the MongoDB driver
|
|
121
|
+
* @return {Promise} promise
|
|
122
|
+
* @see https://mongodb.github.io/node-mongodb-native/4.2/classes/Collection.html#find
|
|
123
|
+
*/
|
|
124
|
+
async find (collectionName, query, options) {
|
|
125
|
+
this.ObjectId.parseIds(query)
|
|
126
|
+
this.parseOptions(options)
|
|
127
|
+
try {
|
|
128
|
+
return await this.getCollection(collectionName).find(query, options).toArray()
|
|
129
|
+
} catch (e) {
|
|
130
|
+
this.log('error', `failed to find docs, ${e.message}`)
|
|
131
|
+
throw this.getError(collectionName, 'find', e)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Updates an existing object in the database
|
|
137
|
+
* @param {String} collectionName The name of the MongoDB collection
|
|
138
|
+
* @param {Object} query
|
|
139
|
+
* @param {Object} data
|
|
140
|
+
* @param {external:MongoDBFindOneAndUpdateOptions} options Options to pass to the MongoDB driver
|
|
141
|
+
* @return {Promise} promise
|
|
142
|
+
* @see https://mongodb.github.io/node-mongodb-native/4.2/classes/Collection.html#findOneAndUpdate
|
|
143
|
+
*/
|
|
144
|
+
async update (collectionName, query, data, options) {
|
|
145
|
+
const opts = Object.assign({ includeResultMetadata: false, returnDocument: 'after' }, options)
|
|
146
|
+
this.parseOptions(opts)
|
|
147
|
+
this.ObjectId.parseIds(query)
|
|
148
|
+
this.ObjectId.parseIds(data)
|
|
149
|
+
// MongoDB doesn't like the explicit setting of _id
|
|
150
|
+
delete data._id
|
|
151
|
+
if (data.$set) delete data.$set._id
|
|
152
|
+
try {
|
|
153
|
+
return await this.getCollection(collectionName).findOneAndUpdate(query, data, opts)
|
|
154
|
+
} catch (e) {
|
|
155
|
+
this.log('error', `failed to update doc, ${e.message}`)
|
|
156
|
+
throw this.getError(collectionName, 'update', e)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Updates multiple objects in the database
|
|
162
|
+
* @param {String} collectionName The name of the MongoDB collection
|
|
163
|
+
* @param {Object} query
|
|
164
|
+
* @param {external:MongoDBUpdateManyOptions} options Options to pass to the MongoDB driver
|
|
165
|
+
* @return {Promise} promise
|
|
166
|
+
* @see https://mongodb.github.io/node-mongodb-native/4.2/classes/Collection.html#updateMany
|
|
167
|
+
*/
|
|
168
|
+
async updateMany (collectionName, query, data, options) {
|
|
169
|
+
this.parseOptions(options)
|
|
170
|
+
this.ObjectId.parseIds(query)
|
|
171
|
+
this.ObjectId.parseIds(data)
|
|
172
|
+
// MongoDB doesn't like the explicit setting of _id
|
|
173
|
+
delete data._id
|
|
174
|
+
if (data.$set) delete data.$set._id
|
|
175
|
+
try {
|
|
176
|
+
await this.getCollection(collectionName).updateMany(query, data, options)
|
|
177
|
+
return this.find(collectionName, query)
|
|
178
|
+
} catch (e) {
|
|
179
|
+
this.log('error', `failed to update docs, ${e.message}`)
|
|
180
|
+
throw this.getError(collectionName, 'update', e)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Replaces an existing object in the database
|
|
186
|
+
* @param {String} collectionName The name of the MongoDB collection
|
|
187
|
+
* @param {Object} query
|
|
188
|
+
* @param {Object} data
|
|
189
|
+
* @param {external:MongoDBFindOneAndReplaceOptions} options Options to pass to the MongoDB driver
|
|
190
|
+
* @return {Promise} promise
|
|
191
|
+
* @see https://mongodb.github.io/node-mongodb-native/4.2/classes/Collection.html#findOneAndReplace
|
|
192
|
+
*/
|
|
193
|
+
async replace (collectionName, query, data, options) {
|
|
194
|
+
const opts = Object.assign({ includeResultMetadata: false, returnDocument: 'after' }, options)
|
|
195
|
+
this.ObjectId.parseIds(query)
|
|
196
|
+
this.ObjectId.parseIds(data)
|
|
197
|
+
this.parseOptions(options)
|
|
198
|
+
// MongoDB doesn't like the explicit setting of _id
|
|
199
|
+
delete data._id
|
|
200
|
+
if (data.$set) delete data.$set._id
|
|
201
|
+
try {
|
|
202
|
+
return await this.getCollection(collectionName).findOneAndReplace(query, data, opts)
|
|
203
|
+
} catch (e) {
|
|
204
|
+
this.log('error', `failed to replace doc, ${e.message}`)
|
|
205
|
+
throw this.getError(collectionName, 'replace', e)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Removes an existing object from the database
|
|
211
|
+
* @param {String} collectionName The name of the MongoDB collection
|
|
212
|
+
* @param {Object} query
|
|
213
|
+
* @param {external:MongoDBDeleteOptions} options Options to pass to the MongoDB driver
|
|
214
|
+
* @return {Promise} promise
|
|
215
|
+
* @see https://mongodb.github.io/node-mongodb-native/4.2/classes/Collection.html#deleteOne
|
|
216
|
+
*/
|
|
217
|
+
async delete (collectionName, query, options) {
|
|
218
|
+
this.ObjectId.parseIds(query)
|
|
219
|
+
this.parseOptions(options)
|
|
220
|
+
try {
|
|
221
|
+
await this.getCollection(collectionName).deleteOne(query, options)
|
|
222
|
+
} catch (e) {
|
|
223
|
+
this.log('error', `failed to delete doc, ${e.message}`)
|
|
224
|
+
throw this.getError(collectionName, 'delete', e)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Removes multiple objects from the database
|
|
230
|
+
* @param {String} collectionName The name of the MongoDB collection
|
|
231
|
+
* @param {Object} query
|
|
232
|
+
* @param {external:MongoDBDeleteOptions} options Options to pass to the MongoDB driver
|
|
233
|
+
* @return {Promise} promise
|
|
234
|
+
* @see https://mongodb.github.io/node-mongodb-native/4.2/classes/Collection.html#deleteMany
|
|
235
|
+
*/
|
|
236
|
+
async deleteMany (collectionName, query, options) {
|
|
237
|
+
this.ObjectId.parseIds(query)
|
|
238
|
+
this.parseOptions(options)
|
|
239
|
+
try {
|
|
240
|
+
await this.getCollection(collectionName).deleteMany(query, options)
|
|
241
|
+
} catch (e) {
|
|
242
|
+
this.log('error', `failed to delete docs, ${e.message}`)
|
|
243
|
+
throw this.getError(collectionName, 'delete', e)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Returns the relevant AdaptError instance to match the MongoError
|
|
249
|
+
* @param {String} collectionName DB collection being processed
|
|
250
|
+
* @param {String} action DB action being performed
|
|
251
|
+
* @param {String} error The error message
|
|
252
|
+
* @returns {AdaptError}
|
|
253
|
+
*/
|
|
254
|
+
getError (collectionName, action, error) {
|
|
255
|
+
let e = this.app.errors.MONGO_ERROR
|
|
256
|
+
if (error.code === 66) e = this.app.errors.MONGO_IMMUTABLE_FIELD
|
|
257
|
+
else if (error.code === 11000) e = this.app.errors.MONGO_DUPL_INDEX
|
|
258
|
+
return e.setData({ collectionName, action, error: error.message })
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* ObjectId utility functions
|
|
263
|
+
* @type {ObjectIdUtils}
|
|
264
|
+
*/
|
|
265
|
+
get ObjectId () {
|
|
266
|
+
return ObjectIdUtils
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export default MongoDBModule
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { App, Utils } from 'adapt-authoring-core'
|
|
2
|
+
import { ObjectId } from 'mongodb'
|
|
3
|
+
/**
|
|
4
|
+
* Utility functions for dealing with MongoDB ObjectIds
|
|
5
|
+
* @memberof mongodb
|
|
6
|
+
*/
|
|
7
|
+
class ObjectIdUtils {
|
|
8
|
+
/**
|
|
9
|
+
* Registers the isObjectId JSON schema keyword
|
|
10
|
+
*/
|
|
11
|
+
static async addSchemaKeyword () {
|
|
12
|
+
const jsonschema = await App.instance.waitForModule('jsonschema')
|
|
13
|
+
jsonschema.addKeyword({
|
|
14
|
+
keyword: 'isObjectId',
|
|
15
|
+
type: 'string',
|
|
16
|
+
modifying: true,
|
|
17
|
+
schemaType: 'boolean',
|
|
18
|
+
compile: () => {
|
|
19
|
+
return (value, { parentData, parentDataProperty }) => {
|
|
20
|
+
if (!ObjectIdUtils.isValid(value)) {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
parentData[parentDataProperty] = ObjectIdUtils.parse(value)
|
|
25
|
+
} catch (e) {
|
|
26
|
+
return false
|
|
27
|
+
}
|
|
28
|
+
return true
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
}
|
|
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
|
+
}
|
|
101
|
+
|
|
102
|
+
export default ObjectIdUtils
|
package/lib/typedefs.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file exists to define the below types for documentation purposes.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* MongoDB collection
|
|
6
|
+
* @memberof mongodb
|
|
7
|
+
* @external MongoDBCollection
|
|
8
|
+
* @see {@link https://mongodb.github.io/node-mongodb-native/4.2/api/classes/Collection.html}
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Options passed to the createIndex function
|
|
12
|
+
* @memberof mongodb
|
|
13
|
+
* @external MongoDBCreateIndexesOptions
|
|
14
|
+
* @see {@link https://mongodb.github.io/node-mongodb-native/4.2/interfaces/CreateIndexesOptions.html}
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Options passed to the delete functions
|
|
18
|
+
* @memberof mongodb
|
|
19
|
+
* @external MongoDBDeleteOptions
|
|
20
|
+
* @see {@link https://mongodb.github.io/node-mongodb-native/4.2/interfaces/DeleteOptions.html}
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* Options passed to the findOneAndReplace function
|
|
24
|
+
* @memberof mongodb
|
|
25
|
+
* @external MongoDBFindOneAndReplaceOptions
|
|
26
|
+
* @see {@link https://mongodb.github.io/node-mongodb-native/4.2/interfaces/FindOneAndReplaceOptions.html}
|
|
27
|
+
*/
|
|
28
|
+
/**
|
|
29
|
+
* Options passed to the findOneAndUpdate function
|
|
30
|
+
* @memberof mongodb
|
|
31
|
+
* @external MongoDBFindOneAndUpdateOptions
|
|
32
|
+
* @see {@link https://mongodb.github.io/node-mongodb-native/4.2/interfaces/FindOneAndUpdateOptions.html}
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Options passed to the find function
|
|
36
|
+
* @memberof mongodb
|
|
37
|
+
* @external MongoDBFindOptions
|
|
38
|
+
* @see {@link https://mongodb.github.io/node-mongodb-native/4.2/interfaces/FindOptions.html}
|
|
39
|
+
*/
|
|
40
|
+
/**
|
|
41
|
+
* Options passed to the insertOne function
|
|
42
|
+
* @memberof mongodb
|
|
43
|
+
* @external MongoDBInsertOneOptions
|
|
44
|
+
* @see {@link https://mongodb.github.io/node-mongodb-native/4.2/interfaces/InsertOneOptions.html}
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* MongoDB client for DB connections
|
|
48
|
+
* @memberof mongodb
|
|
49
|
+
* @external MongoDBMongoClient
|
|
50
|
+
* @see {@link https://mongodb.github.io/node-mongodb-native/4.2/api/MongoClient.html}
|
|
51
|
+
*/
|
|
52
|
+
/**
|
|
53
|
+
* MongoDB representation of BSON ObjectId type
|
|
54
|
+
* @memberof mongodb
|
|
55
|
+
* @external MongoDBObjectId
|
|
56
|
+
* @see {@link https://mongodb.github.io/node-mongodb-native/4.2/api/ObjectID.html}
|
|
57
|
+
*/
|
|
58
|
+
/**
|
|
59
|
+
* Options passed to the update function
|
|
60
|
+
* @memberof mongodb
|
|
61
|
+
* @external MongoDBUpdateOptions
|
|
62
|
+
* @see {@link https://mongodb.github.io/node-mongodb-native/4.2/interfaces/UpdateOptions.html}
|
|
63
|
+
*/
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "adapt-authoring-mongodb",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Module for saving to a MongoDB instance",
|
|
5
|
+
"homepage": "https://github.com/adapt-security/adapt-authoring-mongodb",
|
|
6
|
+
"license": "GPL-3.0",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "index.js",
|
|
9
|
+
"repository": "github:adapt-security/adapt-authoring-mongodb",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"mongodb": "^6.7.0"
|
|
12
|
+
},
|
|
13
|
+
"peerDependencies": {
|
|
14
|
+
"adapt-authoring-core": "github:adapt-security/adapt-authoring-core"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"eslint": "^9.12.0",
|
|
18
|
+
"standard": "^17.1.0"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const MongoDBModule = require('../lib/MongoDBModule');
|
|
2
|
+
const should = require('should');
|
|
3
|
+
|
|
4
|
+
describe('MongoDB module', function() {
|
|
5
|
+
describe('#readyState()', function() {
|
|
6
|
+
it('should return a number', function() {
|
|
7
|
+
false.should.be.true();
|
|
8
|
+
});
|
|
9
|
+
it('should return a value even if no connection', function() {
|
|
10
|
+
false.should.be.true();
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
describe('#isConnected()', function() {
|
|
14
|
+
it('should return true if connected', function() {
|
|
15
|
+
false.should.be.true();
|
|
16
|
+
});
|
|
17
|
+
it('should return false if not connected', function() {
|
|
18
|
+
false.should.be.true();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
describe('#connectionURI()', function() {
|
|
22
|
+
it('should return a valid connection string', function() {
|
|
23
|
+
false.should.be.true();
|
|
24
|
+
});
|
|
25
|
+
it('should not include undefined values', function() {
|
|
26
|
+
false.should.be.true();
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
describe('#getCollection()', function() {
|
|
30
|
+
it('should return a MongoDB collection', function() {
|
|
31
|
+
false.should.be.true();
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe('#connect()', function() {
|
|
35
|
+
it('should establish a connection to specified MongoDB instance', function() {
|
|
36
|
+
false.should.be.true();
|
|
37
|
+
});
|
|
38
|
+
it('should fail gracefully on error', function() {
|
|
39
|
+
false.should.be.true();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe('#insert()', function() {
|
|
43
|
+
it('should return a promise', function() {
|
|
44
|
+
false.should.be.true();
|
|
45
|
+
});
|
|
46
|
+
it('should return an error if no data is passed', function() {
|
|
47
|
+
false.should.be.true();
|
|
48
|
+
});
|
|
49
|
+
it('should return the inserted document', function() {
|
|
50
|
+
false.should.be.true();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe('#find()', function() {
|
|
54
|
+
it('should return a promise', function() {
|
|
55
|
+
false.should.be.true();
|
|
56
|
+
});
|
|
57
|
+
it('should return an error if invalid query data is passed', function() {
|
|
58
|
+
false.should.be.true();
|
|
59
|
+
});
|
|
60
|
+
it('should return an error if invalid model type is specified', function() {
|
|
61
|
+
false.should.be.true();
|
|
62
|
+
});
|
|
63
|
+
it('should populate specified attributes', function() {
|
|
64
|
+
false.should.be.true();
|
|
65
|
+
});
|
|
66
|
+
it('should return matching documents', function() {
|
|
67
|
+
false.should.be.true();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
describe('#replace()', function() {
|
|
71
|
+
it('should return a promise', function() {
|
|
72
|
+
false.should.be.true();
|
|
73
|
+
});
|
|
74
|
+
it('should return an error if invalid query data is passed', function() {
|
|
75
|
+
false.should.be.true();
|
|
76
|
+
});
|
|
77
|
+
it('should return an error if invalid model type is specified', function() {
|
|
78
|
+
false.should.be.true();
|
|
79
|
+
});
|
|
80
|
+
it('should replace matching document(s)', function() {
|
|
81
|
+
false.should.be.true();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
describe('#delete()', function() {
|
|
85
|
+
it('should return a promise', function() {
|
|
86
|
+
false.should.be.true();
|
|
87
|
+
});
|
|
88
|
+
it('should return an error if invalid query data is passed', function() {
|
|
89
|
+
false.should.be.true();
|
|
90
|
+
});
|
|
91
|
+
it('should return an error if invalid model type is specified', function() {
|
|
92
|
+
false.should.be.true();
|
|
93
|
+
});
|
|
94
|
+
it('should delete matching document(s)', function() {
|
|
95
|
+
false.should.be.true();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe('#formatError()', function() {
|
|
99
|
+
it('should return an Error', function() {
|
|
100
|
+
false.should.be.true();
|
|
101
|
+
});
|
|
102
|
+
it('should set custom statusCode', function() {
|
|
103
|
+
false.should.be.true();
|
|
104
|
+
});
|
|
105
|
+
it('should set default statusCode', function() {
|
|
106
|
+
false.should.be.true();
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const should = require('should');
|
|
2
|
+
|
|
3
|
+
describe('ObjectId Utils', function() {
|
|
4
|
+
describe('#isValid', function() {
|
|
5
|
+
it('should return false for empty/undefined param', function() {
|
|
6
|
+
false.should.be.true();
|
|
7
|
+
});
|
|
8
|
+
it('should return true for valid string', function() {
|
|
9
|
+
false.should.be.true();
|
|
10
|
+
});
|
|
11
|
+
it('should return true for invalid type', function() {
|
|
12
|
+
false.should.be.true();
|
|
13
|
+
});
|
|
14
|
+
it('should return true for invalid string', function() {
|
|
15
|
+
false.should.be.true();
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
describe('#parse', function() {
|
|
19
|
+
it('should return an ObjectId instance', function() {
|
|
20
|
+
false.should.be.true();
|
|
21
|
+
});
|
|
22
|
+
it('should throw an error on invalid input', function() {
|
|
23
|
+
false.should.be.true();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
describe('#parseParamIds', function() {
|
|
27
|
+
it('parse a valid _id to an ObjectId', function() {
|
|
28
|
+
false.should.be.true();
|
|
29
|
+
});
|
|
30
|
+
it('throw an error on invalid _id', function() {
|
|
31
|
+
false.should.be.true();
|
|
32
|
+
});
|
|
33
|
+
it('ignore non _id fields', function() {
|
|
34
|
+
false.should.be.true();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
});
|