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