monastery 1.34.0 → 1.36.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/.eslintrc.json +1 -1
- package/changelog.md +29 -0
- package/docs/readme.md +6 -4
- package/docs/schema/index.md +3 -3
- package/lib/index.js +36 -34
- package/lib/model-crud.js +203 -115
- package/lib/model-validate.js +37 -62
- package/lib/model.js +3 -4
- package/lib/monk-monkey-patches.js +73 -0
- package/lib/util.js +20 -18
- package/package.json +1 -1
- package/test/blacklisting.js +215 -203
- package/test/crud.js +60 -2
- package/test/mock/blacklisting.js +122 -0
- package/test/monk.js +1 -1
package/lib/model-validate.js
CHANGED
|
@@ -3,88 +3,63 @@ let rules = require('./rules')
|
|
|
3
3
|
|
|
4
4
|
module.exports = {
|
|
5
5
|
|
|
6
|
-
validate: function(data, opts, cb) {
|
|
6
|
+
validate: async function(data, opts, cb) {
|
|
7
7
|
/**
|
|
8
8
|
* Validates a model
|
|
9
|
-
* @param {instance} model
|
|
10
9
|
* @param {object} data
|
|
11
10
|
* @param {object} <opts>
|
|
12
|
-
* @param {
|
|
13
|
-
* @param {array|string
|
|
14
|
-
* @param {array|string}
|
|
15
|
-
* @param {
|
|
16
|
-
* default, but false on update
|
|
17
|
-
* @param {array|string|true} skipValidation - skip validation on these fields
|
|
18
|
-
* @param {boolean} timestamps - whether `createdAt` and `updatedAt` are inserted, or `updatedAt` is
|
|
11
|
+
* @param {array|string|false} <opts.blacklist> - augment insertBL/updateBL, `false` will remove blacklisting
|
|
12
|
+
* @param {array|string} <opts.project> - return only these fields, ignores blacklisting
|
|
13
|
+
* @param {array|string|true} <opts.skipValidation> - skip validation on these fields
|
|
14
|
+
* @param {boolean} <opts.timestamps> - whether `createdAt` and `updatedAt` are inserted, or `updatedAt` is
|
|
19
15
|
* updated, depending on the `options.update` value
|
|
16
|
+
* @param {boolean(false)} <opts.update> - are we validating for insert or update? todo: change to `type`
|
|
17
|
+
* @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
|
|
18
|
+
* default, but false on update
|
|
20
19
|
* @param {function} <cb> - instead of returning a promise
|
|
21
|
-
* @this model
|
|
22
|
-
|
|
23
20
|
* @return promise(errors[] || pruned data{})
|
|
21
|
+
* @this model
|
|
24
22
|
*/
|
|
25
23
|
|
|
26
24
|
// Optional cb and opts
|
|
27
|
-
if (util.isFunction(opts)) {
|
|
25
|
+
if (util.isFunction(opts)) {
|
|
26
|
+
cb = opts; opts = undefined
|
|
27
|
+
}
|
|
28
28
|
if (cb && !util.isFunction(cb)) {
|
|
29
29
|
throw new Error(`The callback passed to ${this.name}.validate() is not a function`)
|
|
30
30
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
try {
|
|
32
|
+
data = util.deepCopy(data)
|
|
33
|
+
opts = opts || {}
|
|
34
|
+
opts.update = opts.update || opts.findOneAndUpdate
|
|
35
|
+
opts.insert = !opts.update
|
|
36
|
+
opts.skipValidation = opts.skipValidation === true ? true : util.toArray(opts.skipValidation||[])
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
// Auguemnt the schema blacklist
|
|
45
|
-
for (let _path of opts.blacklist) {
|
|
46
|
-
let path = _path.replace(/^-/, '')
|
|
47
|
-
if (_path.match(/^-/)) whitelist.push(path)
|
|
48
|
-
else blacklist.push(path)
|
|
49
|
-
}
|
|
50
|
-
// Remove whitelisted/negated fields
|
|
51
|
-
blacklist = blacklist.filter(o => !whitelist.includes(o))
|
|
52
|
-
// Remove any deep blacklisted fields that have a whitelisted parent specified.
|
|
53
|
-
// E.g remove ['deep.deep2.deep3'] if ['deep'] exists in the whitelist
|
|
54
|
-
for (let i=blacklist.length; i--;) {
|
|
55
|
-
let split = blacklist[i].split('.')
|
|
56
|
-
for (let j=split.length; j--;) {
|
|
57
|
-
if (split.length > 1) split.pop()
|
|
58
|
-
else continue
|
|
59
|
-
if (whitelist.includes(split.join())) {
|
|
60
|
-
blacklist.splice(i, 1)
|
|
61
|
-
break
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
opts.blacklist = blacklist
|
|
66
|
-
} else {
|
|
67
|
-
opts.blacklist = [ ...this[`${opts.action}BL`] ]
|
|
68
|
-
}
|
|
38
|
+
// Get projection
|
|
39
|
+
if (opts.project) opts.projectionValidate = this._getProjectionFromProject(opts.project)
|
|
40
|
+
else opts.projectionValidate = this._getProjectionFromBlacklist(opts.update ? 'update' : 'insert', opts.blacklist)
|
|
41
|
+
|
|
42
|
+
// Hook: beforeValidate
|
|
43
|
+
await util.runSeries(this.beforeValidate.map(f => f.bind(opts, data)))
|
|
69
44
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return util.toArray(data).map(item => {
|
|
45
|
+
// Recurse and validate fields
|
|
46
|
+
let response = util.toArray(data).map(item => {
|
|
73
47
|
let validated = this._validateFields(item, this.fields, item, opts, '')
|
|
74
48
|
if (validated[0].length) throw validated[0]
|
|
75
49
|
else return validated[1]
|
|
76
50
|
})
|
|
77
51
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
52
|
+
// Single document?
|
|
53
|
+
response = util.isArray(data)? response : response[0]
|
|
54
|
+
|
|
55
|
+
// Success/error
|
|
81
56
|
if (cb) cb(null, response)
|
|
82
57
|
else return Promise.resolve(response)
|
|
83
58
|
|
|
84
|
-
}
|
|
85
|
-
if (cb) cb(
|
|
86
|
-
else throw
|
|
87
|
-
}
|
|
59
|
+
} catch (e) {
|
|
60
|
+
if (cb) cb(e)
|
|
61
|
+
else throw e
|
|
62
|
+
}
|
|
88
63
|
},
|
|
89
64
|
|
|
90
65
|
_getMostSpecificKeyMatchingPath: function(object, path) {
|
|
@@ -140,7 +115,7 @@ module.exports = {
|
|
|
140
115
|
let value = util.isArray(fields)? data : (data||{})[fieldName]
|
|
141
116
|
let indexOrFieldName = util.isArray(fields)? i : fieldName
|
|
142
117
|
let path2 = `${path}.${indexOrFieldName}`.replace(/^\./, '')
|
|
143
|
-
let path3 = path2.replace(/(^|\.)[0-9]+(\.|$)/, '$2') // no numerical keys, e.g. pets.1.name
|
|
118
|
+
let path3 = path2.replace(/(^|\.)[0-9]+(\.|$)/, '$2') // no numerical keys, e.g. pets.1.name = pets.name
|
|
144
119
|
let isType = 'is' + util.ucFirst(schema.type)
|
|
145
120
|
let isTypeRule = this.rules[isType] || rules[isType]
|
|
146
121
|
|
|
@@ -157,7 +132,7 @@ module.exports = {
|
|
|
157
132
|
}
|
|
158
133
|
|
|
159
134
|
// Ignore blacklisted
|
|
160
|
-
if (
|
|
135
|
+
if (this._pathBlacklisted(path3, opts.projectionValidate) && !schema.defaultOverride) return
|
|
161
136
|
// Ignore insert only
|
|
162
137
|
if (opts.update && schema.insertOnly) return
|
|
163
138
|
// Ignore virtual fields
|
|
@@ -218,8 +193,8 @@ module.exports = {
|
|
|
218
193
|
* @param {object} field - field schema
|
|
219
194
|
* @param {string} path - full field path
|
|
220
195
|
* @param {object} opts - original validate() options
|
|
221
|
-
* @this model
|
|
222
196
|
* @return {array} errors
|
|
197
|
+
* @this model
|
|
223
198
|
*/
|
|
224
199
|
let errors = []
|
|
225
200
|
if (opts.skipValidation === true) return []
|
package/lib/model.js
CHANGED
|
@@ -9,8 +9,8 @@ let Model = module.exports = function(name, opts, manager) {
|
|
|
9
9
|
* @param {string} name
|
|
10
10
|
* @param {object} opts - see mongodb colleciton documentation
|
|
11
11
|
* @param {boolean} opts.waitForIndexes
|
|
12
|
-
* @this model
|
|
13
12
|
* @return Promise(model) | this
|
|
13
|
+
* @this model
|
|
14
14
|
*/
|
|
15
15
|
if (!(this instanceof Model)) {
|
|
16
16
|
return new Model(name, opts, this)
|
|
@@ -72,7 +72,7 @@ let Model = module.exports = function(name, opts, manager) {
|
|
|
72
72
|
this._setupFields(this.fields = Object.assign({}, this._timestampFields, this.fields))
|
|
73
73
|
this.fieldsFlattened = this._getFieldsFlattened(this.fields, '') // test output?
|
|
74
74
|
|
|
75
|
-
// Extend model with monk collection
|
|
75
|
+
// Extend model with monk collection queries
|
|
76
76
|
this._collection = manager.get? manager.get(name, { castIds: false }) : null
|
|
77
77
|
if (!this._collection) {
|
|
78
78
|
this.info('There is no mongodb connection, a lot of the monk/monastery methods will be unavailable')
|
|
@@ -83,8 +83,7 @@ let Model = module.exports = function(name, opts, manager) {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// Add model to manager
|
|
86
|
-
if (typeof this.manager[name] === 'undefined'
|
|
87
|
-
|| typeof this.manager.model[name] !== 'undefined') {
|
|
86
|
+
if (typeof this.manager[name] === 'undefined' || typeof this.manager.model[name] !== 'undefined') {
|
|
88
87
|
this.manager[name] = this
|
|
89
88
|
} else {
|
|
90
89
|
this.warn(`Your model name '${name}' is conflicting, you are only able to
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
let MongoClient = require('mongodb').MongoClient
|
|
2
|
+
|
|
3
|
+
module.exports.open = function(uri, opts, fn) {
|
|
4
|
+
/*
|
|
5
|
+
* Monkey patch to remove db event listener warnings
|
|
6
|
+
* @todo remove when monk is removed
|
|
7
|
+
* @see https://www.mongodb.com/community/forums/t/node-44612-deprecationwarning-listening-to-events-on-
|
|
8
|
+
the-db-class-has-been-deprecated-and-will-be-removed-in-the-next-major-version/15849/4
|
|
9
|
+
*/
|
|
10
|
+
var STATE = {
|
|
11
|
+
CLOSED: 'closed',
|
|
12
|
+
OPENING: 'opening',
|
|
13
|
+
OPEN: 'open'
|
|
14
|
+
}
|
|
15
|
+
MongoClient.connect(uri, opts, function (err, client) {
|
|
16
|
+
// this = Manager
|
|
17
|
+
if (err) {
|
|
18
|
+
this._state = STATE.CLOSED
|
|
19
|
+
this.emit('error-opening', err)
|
|
20
|
+
} else {
|
|
21
|
+
this._state = STATE.OPEN
|
|
22
|
+
|
|
23
|
+
this._client = client
|
|
24
|
+
this._db = client.db()
|
|
25
|
+
|
|
26
|
+
// set up events
|
|
27
|
+
var self = this
|
|
28
|
+
;['authenticated', 'close', 'error', 'parseError', 'timeout'].forEach(function (eventName) {
|
|
29
|
+
self._client.on(eventName, function (e) {
|
|
30
|
+
self.emit(eventName, e)
|
|
31
|
+
})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
this.emit('open', this._db)
|
|
35
|
+
}
|
|
36
|
+
if (fn) {
|
|
37
|
+
fn(err, this)
|
|
38
|
+
}
|
|
39
|
+
}.bind(this))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports.findOneAndUpdate = function(query, update, opts, fn) {
|
|
43
|
+
/*
|
|
44
|
+
* Monkey patch to use returnDocument
|
|
45
|
+
* @todo remove when monk is removed
|
|
46
|
+
* @see https://github.com/Automattic/monk/blob/master/lib/collection.js#L265
|
|
47
|
+
*/
|
|
48
|
+
// this = model
|
|
49
|
+
if (typeof opts === 'function') {
|
|
50
|
+
fn = opts
|
|
51
|
+
opts = {}
|
|
52
|
+
}
|
|
53
|
+
return this._dispatch(function findOneAndUpdate(args) {
|
|
54
|
+
var method = 'findOneAndUpdate'
|
|
55
|
+
if (typeof (args.options || {}).returnDocument === 'undefined') {
|
|
56
|
+
args.options.returnDocument = 'after'
|
|
57
|
+
}
|
|
58
|
+
if (args.options.replaceOne | args.options.replace) {
|
|
59
|
+
method = 'findOneAndReplace'
|
|
60
|
+
}
|
|
61
|
+
return args.col[method](args.query, args.update, args.options)
|
|
62
|
+
.then(function (doc) {
|
|
63
|
+
if (doc && typeof doc.value !== 'undefined') {
|
|
64
|
+
return doc.value
|
|
65
|
+
}
|
|
66
|
+
if (doc.ok && doc.lastErrorObject && doc.lastErrorObject.n === 0) {
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
return doc
|
|
70
|
+
})
|
|
71
|
+
})({options: opts, query: query, update: update, callback: fn}, 'findOneAndUpdate')
|
|
72
|
+
}
|
|
73
|
+
|
package/lib/util.js
CHANGED
|
@@ -140,14 +140,13 @@ module.exports = {
|
|
|
140
140
|
return typeof value === 'undefined'
|
|
141
141
|
},
|
|
142
142
|
|
|
143
|
-
omit: function(obj,
|
|
144
|
-
|
|
145
|
-
for (let i
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
target[i] = obj[i]
|
|
143
|
+
omit: function(obj, fields) {
|
|
144
|
+
const shallowCopy = Object.assign({}, obj)
|
|
145
|
+
for (let i=0; i<fields.length; i+=1) {
|
|
146
|
+
const key = fields[i]
|
|
147
|
+
delete shallowCopy[key]
|
|
149
148
|
}
|
|
150
|
-
return
|
|
149
|
+
return shallowCopy
|
|
151
150
|
},
|
|
152
151
|
|
|
153
152
|
parseData: function(obj) {
|
|
@@ -228,21 +227,24 @@ module.exports = {
|
|
|
228
227
|
}
|
|
229
228
|
},
|
|
230
229
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
230
|
+
pick: function(obj, keys) {
|
|
231
|
+
// Similiar to underscore.pick
|
|
232
|
+
// @param {string[] | regex[]} keys
|
|
233
|
+
if (!this.isObject(obj) && !this.isFunction(obj)) return {}
|
|
234
|
+
keys = this.toArray(keys)
|
|
235
|
+
let res = {}
|
|
236
|
+
for (let key of keys) {
|
|
237
|
+
if (this.isString(key) && obj.hasOwnProperty(key)) res[key] = obj[key]
|
|
238
|
+
if (this.isRegex(key)) {
|
|
239
|
+
for (let key2 in obj) {
|
|
240
|
+
if (obj.hasOwnProperty(key2) && key2.match(key)) res[key2] = obj[key2]
|
|
241
|
+
}
|
|
239
242
|
}
|
|
240
|
-
if (match) target[prop] = obj[prop]
|
|
241
243
|
}
|
|
242
|
-
return
|
|
244
|
+
return res
|
|
243
245
|
},
|
|
244
246
|
|
|
245
|
-
removeUndefined: (variable)
|
|
247
|
+
removeUndefined: function(variable) {
|
|
246
248
|
// takes an array or object
|
|
247
249
|
if (Array.isArray(variable)) {
|
|
248
250
|
for (let i=variable.length; i--;) {
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "monastery",
|
|
3
3
|
"description": "⛪ A straight forward MongoDB ODM built around Monk",
|
|
4
4
|
"author": "Ricky Boyce",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.36.1",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": "github:boycce/monastery",
|
|
8
8
|
"homepage": "https://boycce.github.io/monastery/",
|