monastery 1.33.0 → 1.36.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/.eslintrc.json +1 -1
- package/changelog.md +29 -0
- package/docs/readme.md +5 -2
- package/docs/schema/index.md +3 -3
- package/lib/index.js +37 -32
- package/lib/model-crud.js +189 -113
- 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 +83 -26
- package/test/crud.js +59 -1
- package/test/monk.js +1 -1
- package/test/util.js +7 -0
package/lib/model-crud.js
CHANGED
|
@@ -6,33 +6,32 @@ module.exports = {
|
|
|
6
6
|
/**
|
|
7
7
|
* Inserts document(s) with monk after validating data & before hooks.
|
|
8
8
|
* @param {object} opts
|
|
9
|
-
* @param {object|array}
|
|
10
|
-
* @param {array|string|false} <opts.blacklist> - augment schema.insertBL, `false` will remove
|
|
9
|
+
* @param {object|array} opts.data - documents to insert
|
|
10
|
+
* @param {array|string|false} <opts.blacklist> - augment schema.insertBL, `false` will remove blacklisting
|
|
11
|
+
* @param {array|string} <opts.project> - return only these fields, ignores blacklisting
|
|
11
12
|
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
|
|
12
|
-
* @param {array|string|false} validateUndefined - validates all 'required' undefined fields, true by
|
|
13
|
-
* default, but false on update
|
|
14
13
|
* @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
|
|
15
14
|
* @param {boolean} <opts.timestamps> - whether `createdAt` and `updatedAt` are automatically inserted
|
|
16
|
-
* @param {
|
|
15
|
+
* @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
|
|
16
|
+
* default, but false on update
|
|
17
|
+
* @param {any} <any mongodb option>
|
|
17
18
|
* @param {function} <cb> - execute cb(err, data) instead of responding
|
|
18
|
-
* @this model
|
|
19
19
|
* @return promise
|
|
20
|
+
* @this model
|
|
20
21
|
*/
|
|
21
22
|
if (cb && !util.isFunction(cb)) {
|
|
22
23
|
throw new Error(`The callback passed to ${this.name}.insert() is not a function`)
|
|
23
24
|
}
|
|
24
25
|
try {
|
|
25
26
|
opts = await this._queryObject(opts, 'insert')
|
|
26
|
-
let
|
|
27
|
-
'data', 'insert', 'model', 'respond', 'validateUndefined', 'skipValidation', 'blacklist'
|
|
28
|
-
])
|
|
27
|
+
let custom = ['blacklist', 'data', 'insert', 'model', 'respond', 'skipValidation', 'validateUndefined']
|
|
29
28
|
|
|
30
29
|
// Validate
|
|
31
|
-
let data = await this.validate(opts.data||{}, { ...opts })
|
|
30
|
+
let data = await this.validate(opts.data || {}, { ...opts })
|
|
32
31
|
|
|
33
32
|
// Insert
|
|
34
33
|
await util.runSeries(this.beforeInsert.map(f => f.bind(opts, data)))
|
|
35
|
-
let response = await this._insert(data,
|
|
34
|
+
let response = await this._insert(data, util.omit(opts, custom))
|
|
36
35
|
await util.runSeries(this.afterInsert.map(f => f.bind(opts, response)))
|
|
37
36
|
|
|
38
37
|
// Success/error
|
|
@@ -51,53 +50,42 @@ module.exports = {
|
|
|
51
50
|
/**
|
|
52
51
|
* Finds document(s) with monk, also auto populates
|
|
53
52
|
* @param {object} opts
|
|
54
|
-
* @param {object} <opts.query> - mongodb query object
|
|
55
|
-
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
|
|
56
|
-
* @param {array} <opts.populate> - population, see docs
|
|
57
53
|
* @param {array|string|false} <opts.blacklist> - augment schema.findBL, `false` will remove all blacklisting
|
|
54
|
+
* @param {array} <opts.populate> - population, see docs
|
|
58
55
|
* @param {array|string} <opts.project> - return only these fields, ignores blacklisting
|
|
59
|
-
* @param {
|
|
56
|
+
* @param {object} <opts.query> - mongodb query object
|
|
57
|
+
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
|
|
58
|
+
* @param {any} <any mongodb option>
|
|
60
59
|
* @param {function} <cb> - execute cb(err, data) instead of responding
|
|
61
|
-
* @this model
|
|
62
60
|
* @return promise
|
|
61
|
+
* @this model
|
|
63
62
|
*/
|
|
64
63
|
if (cb && !util.isFunction(cb)) {
|
|
65
64
|
throw new Error(`The callback passed to ${this.name}.find() is not a function`)
|
|
66
65
|
}
|
|
67
66
|
try {
|
|
68
67
|
opts = await this._queryObject(opts, 'find', one)
|
|
68
|
+
let custom = ['blacklist', 'model', 'one', 'populate', 'project', 'query', 'respond']
|
|
69
69
|
let lookups = []
|
|
70
|
-
let options = util.omit(opts, ['blacklist', 'one', 'populate', 'project', 'query', 'respond'])
|
|
71
|
-
options.addFields = options.addFields || {}
|
|
72
70
|
|
|
73
|
-
//
|
|
74
|
-
if (opts.project)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
if (util.isArray(opts.project)) {
|
|
79
|
-
options.projection = opts.project.reduce((o, v) => {
|
|
80
|
-
o[v.replace(/^-/, '')] = v.match(/^-/)? 0 : 1
|
|
81
|
-
return o
|
|
82
|
-
}, {})
|
|
83
|
-
}
|
|
84
|
-
// Or blacklisting
|
|
85
|
-
} else {
|
|
86
|
-
options.projection = this._getBlacklistProjection('find', opts.blacklist)
|
|
87
|
-
}
|
|
71
|
+
// Get projection
|
|
72
|
+
if (opts.project) opts.projection = this._getProjectionFromProject(opts.project)
|
|
73
|
+
else opts.projection = this._getProjectionFromBlacklist(opts.type, opts.blacklist)
|
|
74
|
+
|
|
88
75
|
// Has text search?
|
|
89
76
|
// if (opts.query.$text) {
|
|
90
|
-
//
|
|
91
|
-
//
|
|
77
|
+
// opts.projection.score = { $meta: 'textScore' }
|
|
78
|
+
// opts.sort = { score: { $meta: 'textScore' }}
|
|
92
79
|
// }
|
|
80
|
+
|
|
93
81
|
// Wanting to populate?
|
|
94
82
|
if (!opts.populate) {
|
|
95
|
-
var response = await this[`_find${opts.one? 'One' : ''}`](opts.query,
|
|
83
|
+
var response = await this[`_find${opts.one? 'One' : ''}`](opts.query, util.omit(opts, custom))
|
|
96
84
|
} else {
|
|
97
85
|
loop: for (let item of opts.populate) {
|
|
98
86
|
let path = util.isObject(item)? item.as : item
|
|
99
87
|
// Blacklisted?
|
|
100
|
-
if (
|
|
88
|
+
if (this._pathBlacklisted(path, opts.projection)) continue loop
|
|
101
89
|
// Custom $lookup definition
|
|
102
90
|
// https://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-36-lookup-expr
|
|
103
91
|
if (util.isObject(item)) {
|
|
@@ -118,7 +106,7 @@ module.exports = {
|
|
|
118
106
|
continue
|
|
119
107
|
}
|
|
120
108
|
// Populate model (convert array into document & create lookup)
|
|
121
|
-
|
|
109
|
+
(opts.addFields = opts.addFields || {})[path] = { '$arrayElemAt': [ '$' + path, 0 ] }
|
|
122
110
|
lookups.push({ $lookup: {
|
|
123
111
|
from: modelName,
|
|
124
112
|
localField: path,
|
|
@@ -127,23 +115,26 @@ module.exports = {
|
|
|
127
115
|
}})
|
|
128
116
|
}
|
|
129
117
|
}
|
|
130
|
-
// console.log(1,
|
|
118
|
+
// console.log(1, opts.projection)
|
|
131
119
|
// console.log(2, lookups)
|
|
132
120
|
let aggregate = [
|
|
133
121
|
{ $match: opts.query },
|
|
134
|
-
{ $sort:
|
|
135
|
-
{ $skip:
|
|
136
|
-
...(
|
|
122
|
+
{ $sort: opts.sort },
|
|
123
|
+
{ $skip: opts.skip },
|
|
124
|
+
...(opts.limit? [{ $limit: opts.limit }] : []),
|
|
137
125
|
...lookups,
|
|
138
|
-
...
|
|
139
|
-
{ $project:
|
|
126
|
+
...(opts.addFields? [{ $addFields: opts.addFields }] : []),
|
|
127
|
+
...(opts.projection? [{ $project: opts.projection }] : []),
|
|
140
128
|
]
|
|
141
129
|
response = await this._aggregate(aggregate)
|
|
142
130
|
this.info('aggregate', JSON.stringify(aggregate))
|
|
143
131
|
}
|
|
144
132
|
|
|
133
|
+
// Returning one?
|
|
145
134
|
if (opts.one && util.isArray(response)) response = response[0]
|
|
146
|
-
|
|
135
|
+
|
|
136
|
+
// Process afterFind hooks
|
|
137
|
+
response = await this._processAfterFind(response, opts.projection, opts)
|
|
147
138
|
|
|
148
139
|
// Success
|
|
149
140
|
if (cb) cb(null, response)
|
|
@@ -161,59 +152,112 @@ module.exports = {
|
|
|
161
152
|
return this.find(opts, cb, true)
|
|
162
153
|
},
|
|
163
154
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
155
|
+
findOneAndUpdate: async function(opts, cb) {
|
|
156
|
+
/**
|
|
157
|
+
* Find and update document(s) with monk, also auto populates
|
|
158
|
+
* @param {object} opts
|
|
159
|
+
* @param {array|string|false} <opts.blacklist> - augment findBL/updateBL, `false` will remove all blacklisting
|
|
160
|
+
* @param {array|string|false} <opts.blacklistFind> - augment findBL, `false` will remove all blacklisting
|
|
161
|
+
* @param {array} <opts.populate> - find population, see docs
|
|
162
|
+
* @param {array|string} <opts.project> - return only these fields, ignores blacklisting
|
|
163
|
+
* @param {array|string} <opts.projectFind> - return only these fields, ignores blacklisting
|
|
164
|
+
* @param {object} <opts.query> - mongodb query object
|
|
165
|
+
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
|
|
166
|
+
* @param {any} <any mongodb option>
|
|
167
|
+
*
|
|
168
|
+
* Update options:
|
|
169
|
+
* @param {object|array} opts.data - mongodb document update object(s)
|
|
170
|
+
* @param {array|string|false} <opts.blacklistUpdate> - augment updateBL, `false` will remove all blacklisting
|
|
171
|
+
* @param {array|string} <opts.projectUpdate> - return only these fields, ignores blacklisting
|
|
172
|
+
* @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
|
|
173
|
+
* @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
|
|
174
|
+
* @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
|
|
175
|
+
* default, but false on update
|
|
176
|
+
* @param {function} <cb> - execute cb(err, data) instead of responding
|
|
177
|
+
* @return promise
|
|
178
|
+
* @this model
|
|
179
|
+
*/
|
|
167
180
|
|
|
168
|
-
|
|
181
|
+
if (opts.populate) {
|
|
182
|
+
try {
|
|
183
|
+
// todo: add transaction flag
|
|
184
|
+
delete opts.multi
|
|
185
|
+
let update = await this.update(opts, null, 'findOneAndUpdate')
|
|
186
|
+
if (update) var response = await this.findOne(opts)
|
|
187
|
+
else response = update
|
|
188
|
+
|
|
189
|
+
// Success
|
|
190
|
+
if (cb) cb(null, response)
|
|
191
|
+
else if (opts.req && opts.respond) opts.req.res.json(response)
|
|
192
|
+
else return Promise.resolve(response)
|
|
193
|
+
|
|
194
|
+
} catch (e) {
|
|
195
|
+
if (cb) cb(e)
|
|
196
|
+
else if (opts && opts.req && opts.respond) opts.req.res.error(e)
|
|
197
|
+
else throw e
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
return this.update(opts, cb, 'findOneAndUpdate')
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
update: async function(opts, cb, type='update') {
|
|
169
205
|
/**
|
|
170
206
|
* Updates document(s) with monk after validating data & before hooks.
|
|
171
207
|
* @param {object} opts
|
|
208
|
+
* @param {object|array} opts.data - mongodb document update object(s)
|
|
209
|
+
* @param {array|string|false} <opts.blacklist> - augment schema.updateBL, `false` will remove blacklisting
|
|
210
|
+
* @param {array|string} <opts.project> - return only these fields, ignores blacklisting
|
|
172
211
|
* @param {object} <opts.query> - mongodb query object
|
|
173
|
-
* @param {object|array} <opts.data> - mongodb document update object(s)
|
|
174
212
|
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
|
|
175
|
-
* @param {array|string|false} validateUndefined - validates all 'required' undefined fields, true by
|
|
176
|
-
* default, but false on update
|
|
177
213
|
* @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
|
|
178
214
|
* @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
|
|
179
|
-
* @param {array|string|false} <opts.
|
|
180
|
-
*
|
|
215
|
+
* @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
|
|
216
|
+
* default, but false on update
|
|
217
|
+
* @param {any} <any mongodb option>
|
|
181
218
|
* @param {function} <cb> - execute cb(err, data) instead of responding
|
|
182
|
-
* @
|
|
219
|
+
* @param {function} <type> - 'update', or 'findOneAndUpdate'
|
|
183
220
|
* @return promise(data)
|
|
221
|
+
* @this model
|
|
184
222
|
*/
|
|
185
223
|
if (cb && !util.isFunction(cb)) {
|
|
186
|
-
throw new Error(`The callback passed to ${this.name}
|
|
224
|
+
throw new Error(`The callback passed to ${this.name}.${type}() is not a function`)
|
|
187
225
|
}
|
|
188
226
|
try {
|
|
189
|
-
opts = await this._queryObject(opts,
|
|
227
|
+
opts = await this._queryObject(opts, type)
|
|
190
228
|
let data = opts.data
|
|
191
229
|
let response = null
|
|
192
|
-
let operators = util.
|
|
193
|
-
let
|
|
230
|
+
let operators = util.pick(opts, [/^\$/])
|
|
231
|
+
let custom = [
|
|
232
|
+
'blacklist', 'data', 'model', 'one', 'populate', 'project', 'query', 'respond', 'skipValidation',
|
|
233
|
+
'validateUndefined'
|
|
234
|
+
]
|
|
194
235
|
|
|
195
236
|
// Validate
|
|
196
237
|
if (util.isDefined(data)) data = await this.validate(opts.data, { ...opts })
|
|
197
238
|
if (!util.isDefined(data) && util.isEmpty(operators)) {
|
|
198
|
-
throw new Error(`Please pass an update operator to ${this.name}
|
|
239
|
+
throw new Error(`Please pass an update operator to ${this.name}.${type}(), e.g. data, $unset, etc`)
|
|
199
240
|
}
|
|
200
241
|
if (util.isDefined(data) && (!data || util.isEmpty(data))) {
|
|
201
|
-
throw new Error(`No valid data passed to ${this.name}
|
|
242
|
+
throw new Error(`No valid data passed to ${this.name}.${type}({ data: .. })`)
|
|
202
243
|
}
|
|
203
244
|
// Hook: beforeUpdate (has access to original, non-validated opts.data)
|
|
204
245
|
await util.runSeries(this.beforeUpdate.map(f => f.bind(opts, data||{})))
|
|
205
246
|
if (data && operators['$set']) {
|
|
206
|
-
this.warn(`'$set' fields take precedence over the data fields for \`${this.name}
|
|
247
|
+
this.warn(`'$set' fields take precedence over the data fields for \`${this.name}.${type}()\``)
|
|
207
248
|
}
|
|
208
249
|
if (data || operators['$set']) {
|
|
209
250
|
operators['$set'] = { ...data, ...(operators['$set'] || {}) }
|
|
210
251
|
}
|
|
252
|
+
// Just peform a normal update if we need to populate a findOneAndUpdate
|
|
253
|
+
if (opts.populate && type == 'findOneAndUpdate') type ='update'
|
|
211
254
|
// Update
|
|
212
|
-
let update = await this
|
|
213
|
-
if (
|
|
255
|
+
let update = await this['_' + type](opts.query, operators, util.omit(opts, custom))
|
|
256
|
+
if (type == 'findOneAndUpdate') response = update
|
|
257
|
+
else if (update.n) response = Object.assign(Object.create({ _output: update }), operators['$set']||{})
|
|
214
258
|
|
|
215
259
|
// Hook: afterUpdate (doesn't have access to validated data)
|
|
216
|
-
if (
|
|
260
|
+
if (response) await util.runSeries(this.afterUpdate.map(f => f.bind(opts, response)))
|
|
217
261
|
|
|
218
262
|
// Success
|
|
219
263
|
if (cb) cb(null, response)
|
|
@@ -234,22 +278,22 @@ module.exports = {
|
|
|
234
278
|
* @param {object} <opts.query> - mongodb query object
|
|
235
279
|
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
|
|
236
280
|
* @param {boolean=true} <opts.multi> - set to false to limit the deletion to just one document
|
|
237
|
-
* @param {any} <
|
|
281
|
+
* @param {any} <any mongodb option>
|
|
238
282
|
* @param {function} <cb> - execute cb(err, data) instead of responding
|
|
239
|
-
* @this model
|
|
240
283
|
* @return promise
|
|
284
|
+
* @this model
|
|
241
285
|
*/
|
|
242
286
|
if (cb && !util.isFunction(cb)) {
|
|
243
287
|
throw new Error(`The callback passed to ${this.name}.remove() is not a function`)
|
|
244
288
|
}
|
|
245
289
|
try {
|
|
246
290
|
opts = await this._queryObject(opts, 'remove')
|
|
247
|
-
let
|
|
291
|
+
let custom = ['model', 'query', 'respond']
|
|
248
292
|
if (util.isEmpty(opts.query)) throw new Error('Please specify opts.query')
|
|
249
293
|
|
|
250
294
|
// Remove
|
|
251
295
|
await util.runSeries(this.beforeRemove.map(f => f.bind(opts)))
|
|
252
|
-
let response = await this._remove(opts.query,
|
|
296
|
+
let response = await this._remove(opts.query, util.omit(opts, custom))
|
|
253
297
|
await util.runSeries(this.afterRemove.map(f => f.bind(response)))
|
|
254
298
|
|
|
255
299
|
// Success
|
|
@@ -264,13 +308,16 @@ module.exports = {
|
|
|
264
308
|
}
|
|
265
309
|
},
|
|
266
310
|
|
|
267
|
-
|
|
311
|
+
_getProjectionFromBlacklist: function(type, customBlacklist) {
|
|
268
312
|
/**
|
|
269
313
|
* Returns an exclusion projection
|
|
270
314
|
*
|
|
315
|
+
* Path collisions are removed
|
|
316
|
+
* E.g. ['pets.dogs', 'pets.dogs.name', '-cat', 'pets.dogs.age'] = { 'pets.dog': 0 }
|
|
317
|
+
*
|
|
271
318
|
* @param {string} type - find, insert, or update
|
|
272
|
-
* @param {array} customBlacklist - normally passed through options
|
|
273
|
-
* @return {array} exclusion
|
|
319
|
+
* @param {array|string|false} customBlacklist - normally passed through options
|
|
320
|
+
* @return {array|undefined} exclusion $project {'pets.name': 0}
|
|
274
321
|
* @this model
|
|
275
322
|
*
|
|
276
323
|
* 1. collate deep-blacklists
|
|
@@ -281,16 +328,24 @@ module.exports = {
|
|
|
281
328
|
let list = []
|
|
282
329
|
let manager = this.manager
|
|
283
330
|
let projection = {}
|
|
331
|
+
if (customBlacklist === false) return
|
|
332
|
+
|
|
333
|
+
// String?
|
|
334
|
+
if (typeof customBlacklist === 'string') {
|
|
335
|
+
customBlacklist = customBlacklist.trim().split(/\s+/)
|
|
336
|
+
}
|
|
284
337
|
|
|
285
338
|
// Concat deep blacklists
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
339
|
+
if (type == 'find') {
|
|
340
|
+
util.forEach(this.fieldsFlattened, (schema, path) => {
|
|
341
|
+
if (!schema.model) return
|
|
342
|
+
let deepBL = manager.model[schema.model][`${type}BL`] || []
|
|
343
|
+
let pathWithoutArrays = path.replace(/\.0(\.|$)/, '$1')
|
|
344
|
+
list = list.concat(deepBL.map(o => {
|
|
345
|
+
return `${o.charAt(0) == '-'? '-' : ''}${pathWithoutArrays}.${o.replace(/^-/, '')}`
|
|
346
|
+
}))
|
|
347
|
+
})
|
|
348
|
+
}
|
|
294
349
|
|
|
295
350
|
// Concat model, and custom blacklists
|
|
296
351
|
list = list.concat([...this[`${type}BL`]]).concat(customBlacklist || [])
|
|
@@ -302,7 +357,8 @@ module.exports = {
|
|
|
302
357
|
|
|
303
358
|
// Remove any child fields. E.g remove { user.token: 0 } = key2 if iterating { user: 0 } = key
|
|
304
359
|
for (let key2 in projection) {
|
|
305
|
-
|
|
360
|
+
// todo: need to write a test, testing that this is scoped to \.
|
|
361
|
+
if (key2.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) {
|
|
306
362
|
delete projection[key2]
|
|
307
363
|
}
|
|
308
364
|
}
|
|
@@ -335,6 +391,28 @@ module.exports = {
|
|
|
335
391
|
if (projection[key]) delete projection[key]
|
|
336
392
|
}
|
|
337
393
|
|
|
394
|
+
return util.isEmpty(projection) ? undefined : projection
|
|
395
|
+
},
|
|
396
|
+
|
|
397
|
+
_getProjectionFromProject: function(customProject) {
|
|
398
|
+
/**
|
|
399
|
+
* Returns an in/exclusion projection
|
|
400
|
+
* todo: tests
|
|
401
|
+
*
|
|
402
|
+
* @param {object|array|string} customProject - normally passed through options
|
|
403
|
+
* @return {array|undefined} in/exclusion projection {'pets.name': 0}
|
|
404
|
+
* @this model
|
|
405
|
+
*/
|
|
406
|
+
let projection
|
|
407
|
+
if (util.isString(customProject)) {
|
|
408
|
+
customProject = customProject.trim().split(/\s+/)
|
|
409
|
+
}
|
|
410
|
+
if (util.isArray(customProject)) {
|
|
411
|
+
projection = customProject.reduce((o, v) => {
|
|
412
|
+
o[v.replace(/^-/, '')] = v.match(/^-/)? 0 : 1
|
|
413
|
+
return o
|
|
414
|
+
}, {})
|
|
415
|
+
}
|
|
338
416
|
return projection
|
|
339
417
|
},
|
|
340
418
|
|
|
@@ -342,11 +420,12 @@ module.exports = {
|
|
|
342
420
|
/**
|
|
343
421
|
* Normalise options
|
|
344
422
|
* @param {MongoId|string|object} opts
|
|
345
|
-
* @param {string} type -
|
|
423
|
+
* @param {string} type - insert, update, find, remove, findOneAndUpdate
|
|
346
424
|
* @param {boolean} one - return one document
|
|
347
|
-
* @this model
|
|
348
425
|
* @return {Promise} opts
|
|
426
|
+
* @this model
|
|
349
427
|
*
|
|
428
|
+
* Query parsing logic:
|
|
350
429
|
* opts == string|MongodId - treated as an id
|
|
351
430
|
* opts == undefined|null|false - throw error
|
|
352
431
|
* opts.query == string|MongodID - treated as an id
|
|
@@ -370,7 +449,7 @@ module.exports = {
|
|
|
370
449
|
throw new Error('Please pass an object or MongoId to options.query')
|
|
371
450
|
}
|
|
372
451
|
if (util.isId(opts.query._id)) opts.query._id = this.manager.id(opts.query._id)
|
|
373
|
-
if (isIdType(opts.query._id) || one) opts.one = true
|
|
452
|
+
if (isIdType(opts.query._id) || one || type == 'findOneAndUpdate') opts.one = true
|
|
374
453
|
opts.query = util.removeUndefined(opts.query)
|
|
375
454
|
|
|
376
455
|
// Query options
|
|
@@ -389,7 +468,8 @@ module.exports = {
|
|
|
389
468
|
if (!util.isDefined(opts.data) && util.isDefined((opts.req||{}).body)) opts.data = opts.req.body
|
|
390
469
|
if (util.isDefined(opts.data)) opts.data = await util.parseData(opts.data)
|
|
391
470
|
|
|
392
|
-
opts
|
|
471
|
+
opts.type = type
|
|
472
|
+
opts[type] = true // still being included in the operation options..
|
|
393
473
|
opts.model = this
|
|
394
474
|
return opts
|
|
395
475
|
},
|
|
@@ -400,11 +480,12 @@ module.exports = {
|
|
|
400
480
|
* Recurses through fields that are models and populates missing default-fields and calls model.afterFind([fn,..])
|
|
401
481
|
* Be sure to add any virtual fields to the schema that your populating on,
|
|
402
482
|
* e.g. "nurses": [{ model: 'user' }]
|
|
483
|
+
*
|
|
403
484
|
* @param {object|array|null} data
|
|
404
485
|
* @param {object} projection - $project object
|
|
405
486
|
* @param {object} afterFindContext - handy context object given to schema.afterFind
|
|
406
|
-
* @this model
|
|
407
487
|
* @return Promise(data)
|
|
488
|
+
* @this model
|
|
408
489
|
*/
|
|
409
490
|
// Recurse down from the parent model, ending with the parent model as the parent afterFind hook may
|
|
410
491
|
// want to manipulate any populated models
|
|
@@ -422,8 +503,8 @@ module.exports = {
|
|
|
422
503
|
if (!util.isDefined(schema.default) || path.match(/^\.?(createdAt|updatedAt)$/)) return
|
|
423
504
|
let parentPath = item.fieldName? item.fieldName + '.' : ''
|
|
424
505
|
let pathWithoutArrays = (parentPath + path).replace(/\.0(\.|$)/, '$1')
|
|
425
|
-
// Ignore default fields that are
|
|
426
|
-
if (
|
|
506
|
+
// Ignore default fields that are blacklisted
|
|
507
|
+
if (this._pathBlacklisted(pathWithoutArrays, projection)) return
|
|
427
508
|
// console.log(pathWithoutArrays, path, projection)
|
|
428
509
|
// Set value
|
|
429
510
|
let value = util.isFunction(schema.default)? schema.default(this.manager) : schema.default
|
|
@@ -438,39 +519,34 @@ module.exports = {
|
|
|
438
519
|
return util.runSeries(callbackSeries).then(() => data)
|
|
439
520
|
},
|
|
440
521
|
|
|
441
|
-
|
|
522
|
+
_pathBlacklisted: function(path, projection, matchDeepWhitelistedKeys=true) {
|
|
442
523
|
/**
|
|
443
|
-
* Checks if the path is
|
|
524
|
+
* Checks if the path is blacklisted within a inclusion/exclusion projection
|
|
444
525
|
* @param {string} path - path without array brackets e.g. '.[]'
|
|
445
|
-
* @param {object} projection
|
|
446
|
-
* @param {boolean}
|
|
526
|
+
* @param {object} projection - inclusion/exclusion projection, not mixed
|
|
527
|
+
* @param {boolean} matchDeepWhitelistedKeys - match deep whitelisted keys containing path
|
|
528
|
+
* E.g. pets.color == pets.color.age
|
|
447
529
|
* @return {boolean}
|
|
448
530
|
*/
|
|
449
|
-
let inc
|
|
450
|
-
// console.log(path, projection)
|
|
451
531
|
for (let key in projection) {
|
|
452
|
-
if (projection[key]
|
|
453
|
-
// Inclusion
|
|
454
|
-
// E.g.
|
|
455
|
-
// E.g.
|
|
456
|
-
|
|
457
|
-
inc = true
|
|
458
|
-
if (key.match(new RegExp('^' + path.replace(/\./g, '\\.') + '(\\.|$)'))) return true
|
|
459
|
-
if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return true
|
|
460
|
-
} else if (projection[key]) {
|
|
461
|
-
// Inclusion (equal to key, or key included in path)
|
|
462
|
-
// E.g. 'pets.color.age'.match(/^pets.color.age(.|$)/) = match
|
|
463
|
-
// E.g. 'pets.color.age'.match(/^pets.color(.|$)/) = match
|
|
464
|
-
// E.g. 'pets.color'.match(/^pets.color.age(.|$)/) = no match
|
|
465
|
-
inc = true
|
|
466
|
-
if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return true
|
|
467
|
-
} else {
|
|
468
|
-
// Exclusion
|
|
469
|
-
// E.g. 'pets.color.age'.match(/^pets.color(.|$)/) = match
|
|
532
|
+
if (projection[key]) {
|
|
533
|
+
// Inclusion (whitelisted)
|
|
534
|
+
// E.g. pets.color.age == pets.color.age (exact match)
|
|
535
|
+
// E.g. pets.color.age == pets.color (path contains key)
|
|
536
|
+
var inclusion = true
|
|
470
537
|
if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return false
|
|
538
|
+
if (matchDeepWhitelistedKeys) {
|
|
539
|
+
// E.g. pets.color == pets.color.age (key contains path)
|
|
540
|
+
if (key.match(new RegExp('^' + path.replace(/\./g, '\\.') + '\\.'))) return false
|
|
541
|
+
}
|
|
542
|
+
} else {
|
|
543
|
+
// Exclusion (blacklisted)
|
|
544
|
+
// E.g. pets.color.age == pets.color.age (exact match)
|
|
545
|
+
// E.g. pets.color.age == pets.color (path contains key)
|
|
546
|
+
if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return true
|
|
471
547
|
}
|
|
472
548
|
}
|
|
473
|
-
return
|
|
549
|
+
return inclusion? true : false
|
|
474
550
|
},
|
|
475
551
|
|
|
476
552
|
_recurseAndFindModels: function(fields, dataArr) {
|