monastery 1.32.5 → 1.35.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 +21 -0
- package/docs/readme.md +17 -0
- package/docs/schema/index.md +3 -3
- package/lib/index.js +3 -0
- package/lib/model-crud.js +194 -252
- package/lib/model-validate.js +16 -47
- package/lib/model.js +8 -53
- package/package.json +1 -1
- package/plugins/images/index.js +2 -2
- package/test/blacklisting.js +256 -78
- package/test/crud.js +5 -1
- package/test/model.js +0 -165
- 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,56 +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', 'one', 'populate', 'project', 'query', 'respond']
|
|
69
69
|
let lookups = []
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
// Can be an inclusion or exclusion projection
|
|
76
|
-
if (util.isString(opts.project)) {
|
|
77
|
-
opts.project = opts.project.trim().split(/\s+/)
|
|
78
|
-
}
|
|
79
|
-
if (util.isArray(opts.project)) {
|
|
80
|
-
options.projection = opts.project.reduce((o, v) => {
|
|
81
|
-
o[v.replace(/^-/, '')] = v.match(/^-/)? 0 : 1
|
|
82
|
-
return o
|
|
83
|
-
}, {})
|
|
84
|
-
}
|
|
85
|
-
} else {
|
|
86
|
-
// Calculate the exclusion-projection
|
|
87
|
-
let blacklistProjection = { ...this.findBLProject }
|
|
88
|
-
blacklistProjection = this._addDeepBlacklists(blacklistProjection, opts.populate)
|
|
89
|
-
options.projection = this._addBlacklist(blacklistProjection, opts.blacklist)
|
|
90
|
-
}
|
|
70
|
+
|
|
71
|
+
// Get projection
|
|
72
|
+
if (opts.project) opts.projection = this._getProjectionFromProject(opts.project)
|
|
73
|
+
else opts.projection = this._getProjectionFromBlacklist(opts.type, opts.blacklist)
|
|
74
|
+
|
|
91
75
|
// Has text search?
|
|
92
76
|
// if (opts.query.$text) {
|
|
93
|
-
//
|
|
94
|
-
//
|
|
77
|
+
// opts.projection.score = { $meta: 'textScore' }
|
|
78
|
+
// opts.sort = { score: { $meta: 'textScore' }}
|
|
95
79
|
// }
|
|
80
|
+
|
|
96
81
|
// Wanting to populate?
|
|
97
82
|
if (!opts.populate) {
|
|
98
|
-
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))
|
|
99
84
|
} else {
|
|
100
85
|
loop: for (let item of opts.populate) {
|
|
101
86
|
let path = util.isObject(item)? item.as : item
|
|
102
87
|
// Blacklisted?
|
|
103
|
-
if (
|
|
88
|
+
if (this._pathBlacklisted(path, opts.projection)) continue loop
|
|
104
89
|
// Custom $lookup definition
|
|
105
90
|
// https://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-36-lookup-expr
|
|
106
91
|
if (util.isObject(item)) {
|
|
@@ -121,7 +106,7 @@ module.exports = {
|
|
|
121
106
|
continue
|
|
122
107
|
}
|
|
123
108
|
// Populate model (convert array into document & create lookup)
|
|
124
|
-
|
|
109
|
+
(opts.addFields = opts.addFields || {})[path] = { '$arrayElemAt': [ '$' + path, 0 ] }
|
|
125
110
|
lookups.push({ $lookup: {
|
|
126
111
|
from: modelName,
|
|
127
112
|
localField: path,
|
|
@@ -130,26 +115,26 @@ module.exports = {
|
|
|
130
115
|
}})
|
|
131
116
|
}
|
|
132
117
|
}
|
|
133
|
-
// console.log(1,
|
|
118
|
+
// console.log(1, opts.projection)
|
|
134
119
|
// console.log(2, lookups)
|
|
135
120
|
let aggregate = [
|
|
136
121
|
{ $match: opts.query },
|
|
137
|
-
{ $sort:
|
|
138
|
-
{ $skip:
|
|
139
|
-
...(
|
|
122
|
+
{ $sort: opts.sort },
|
|
123
|
+
{ $skip: opts.skip },
|
|
124
|
+
...(opts.limit? [{ $limit: opts.limit }] : []),
|
|
140
125
|
...lookups,
|
|
141
|
-
...
|
|
142
|
-
{ $project:
|
|
126
|
+
...(opts.addFields? [{ $addFields: opts.addFields }] : []),
|
|
127
|
+
...(opts.projection? [{ $project: opts.projection }] : []),
|
|
143
128
|
]
|
|
144
129
|
response = await this._aggregate(aggregate)
|
|
145
130
|
this.info('aggregate', JSON.stringify(aggregate))
|
|
146
131
|
}
|
|
147
132
|
|
|
133
|
+
// Returning one?
|
|
148
134
|
if (opts.one && util.isArray(response)) response = response[0]
|
|
149
|
-
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
response = await this._processAfterFind(response, options.projection, opts)
|
|
135
|
+
|
|
136
|
+
// Process afterFind hooks
|
|
137
|
+
response = await this._processAfterFind(response, opts.projection, opts)
|
|
153
138
|
|
|
154
139
|
// Success
|
|
155
140
|
if (cb) cb(null, response)
|
|
@@ -167,26 +152,27 @@ module.exports = {
|
|
|
167
152
|
return this.find(opts, cb, true)
|
|
168
153
|
},
|
|
169
154
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
155
|
+
findOneAndUpdate: function(opts, cb) {
|
|
156
|
+
return this._findOneAndUpdate(opts, cb)
|
|
157
|
+
},
|
|
173
158
|
|
|
174
159
|
update: async function(opts, cb) {
|
|
175
160
|
/**
|
|
176
161
|
* Updates document(s) with monk after validating data & before hooks.
|
|
177
162
|
* @param {object} opts
|
|
163
|
+
* @param {object|array} opts.data - mongodb document update object(s)
|
|
164
|
+
* @param {array|string|false} <opts.blacklist> - augment schema.updateBL, `false` will remove blacklisting
|
|
165
|
+
* @param {array|string} <opts.project> - return only these fields, ignores blacklisting
|
|
178
166
|
* @param {object} <opts.query> - mongodb query object
|
|
179
|
-
* @param {object|array} <opts.data> - mongodb document update object(s)
|
|
180
167
|
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
|
|
181
|
-
* @param {array|string|false} validateUndefined - validates all 'required' undefined fields, true by
|
|
182
|
-
* default, but false on update
|
|
183
168
|
* @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
|
|
184
169
|
* @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
|
|
185
|
-
* @param {array|string|false} <opts.
|
|
186
|
-
*
|
|
170
|
+
* @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
|
|
171
|
+
* default, but false on update
|
|
172
|
+
* @param {any} <any mongodb option>
|
|
187
173
|
* @param {function} <cb> - execute cb(err, data) instead of responding
|
|
188
|
-
* @this model
|
|
189
174
|
* @return promise(data)
|
|
175
|
+
* @this model
|
|
190
176
|
*/
|
|
191
177
|
if (cb && !util.isFunction(cb)) {
|
|
192
178
|
throw new Error(`The callback passed to ${this.name}.update() is not a function`)
|
|
@@ -196,7 +182,7 @@ module.exports = {
|
|
|
196
182
|
let data = opts.data
|
|
197
183
|
let response = null
|
|
198
184
|
let operators = util.pluck(opts, [/^\$/])
|
|
199
|
-
let
|
|
185
|
+
let custom = ['blacklist', 'data', 'query', 'respond', 'skipValidation', 'validateUndefined']
|
|
200
186
|
|
|
201
187
|
// Validate
|
|
202
188
|
if (util.isDefined(data)) data = await this.validate(opts.data, { ...opts })
|
|
@@ -215,7 +201,7 @@ module.exports = {
|
|
|
215
201
|
operators['$set'] = { ...data, ...(operators['$set'] || {}) }
|
|
216
202
|
}
|
|
217
203
|
// Update
|
|
218
|
-
let update = await this._update(opts.query, operators,
|
|
204
|
+
let update = await this._update(opts.query, operators, util.omit(opts, custom))
|
|
219
205
|
if (update.n) response = Object.assign(Object.create({ _output: update }), operators['$set']||{})
|
|
220
206
|
|
|
221
207
|
// Hook: afterUpdate (doesn't have access to validated data)
|
|
@@ -240,22 +226,22 @@ module.exports = {
|
|
|
240
226
|
* @param {object} <opts.query> - mongodb query object
|
|
241
227
|
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
|
|
242
228
|
* @param {boolean=true} <opts.multi> - set to false to limit the deletion to just one document
|
|
243
|
-
* @param {any} <
|
|
229
|
+
* @param {any} <any mongodb option>
|
|
244
230
|
* @param {function} <cb> - execute cb(err, data) instead of responding
|
|
245
|
-
* @this model
|
|
246
231
|
* @return promise
|
|
232
|
+
* @this model
|
|
247
233
|
*/
|
|
248
234
|
if (cb && !util.isFunction(cb)) {
|
|
249
235
|
throw new Error(`The callback passed to ${this.name}.remove() is not a function`)
|
|
250
236
|
}
|
|
251
237
|
try {
|
|
252
238
|
opts = await this._queryObject(opts, 'remove')
|
|
253
|
-
let
|
|
239
|
+
let custom = ['query', 'respond']
|
|
254
240
|
if (util.isEmpty(opts.query)) throw new Error('Please specify opts.query')
|
|
255
241
|
|
|
256
242
|
// Remove
|
|
257
243
|
await util.runSeries(this.beforeRemove.map(f => f.bind(opts)))
|
|
258
|
-
let response = await this._remove(opts.query,
|
|
244
|
+
let response = await this._remove(opts.query, util.omit(opts, custom))
|
|
259
245
|
await util.runSeries(this.afterRemove.map(f => f.bind(response)))
|
|
260
246
|
|
|
261
247
|
// Success
|
|
@@ -270,15 +256,124 @@ module.exports = {
|
|
|
270
256
|
}
|
|
271
257
|
},
|
|
272
258
|
|
|
259
|
+
_getProjectionFromBlacklist: function(type, customBlacklist) {
|
|
260
|
+
/**
|
|
261
|
+
* Returns an exclusion projection
|
|
262
|
+
*
|
|
263
|
+
* Path collisions are removed
|
|
264
|
+
* E.g. ['pets.dogs', 'pets.dogs.name', '-cat', 'pets.dogs.age'] = { 'pets.dog': 0 }
|
|
265
|
+
*
|
|
266
|
+
* @param {string} type - find, insert, or update
|
|
267
|
+
* @param {array|string|false} customBlacklist - normally passed through options
|
|
268
|
+
* @return {array|undefined} exclusion $project {'pets.name': 0}
|
|
269
|
+
* @this model
|
|
270
|
+
*
|
|
271
|
+
* 1. collate deep-blacklists
|
|
272
|
+
* 2. concatenate the model's blacklist and any custom blacklist
|
|
273
|
+
* 3. create an exclusion projection object from the blacklist, overriding from left to right
|
|
274
|
+
*/
|
|
275
|
+
|
|
276
|
+
let list = []
|
|
277
|
+
let manager = this.manager
|
|
278
|
+
let projection = {}
|
|
279
|
+
if (customBlacklist === false) return
|
|
280
|
+
|
|
281
|
+
// String?
|
|
282
|
+
if (typeof customBlacklist === 'string') {
|
|
283
|
+
customBlacklist = customBlacklist.trim().split(/\s+/)
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Concat deep blacklists
|
|
287
|
+
if (type == 'find') {
|
|
288
|
+
util.forEach(this.fieldsFlattened, (schema, path) => {
|
|
289
|
+
if (!schema.model) return
|
|
290
|
+
let deepBL = manager.model[schema.model][`${type}BL`] || []
|
|
291
|
+
let pathWithoutArrays = path.replace(/\.0(\.|$)/, '$1')
|
|
292
|
+
list = list.concat(deepBL.map(o => {
|
|
293
|
+
return `${o.charAt(0) == '-'? '-' : ''}${pathWithoutArrays}.${o.replace(/^-/, '')}`
|
|
294
|
+
}))
|
|
295
|
+
})
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Concat model, and custom blacklists
|
|
299
|
+
list = list.concat([...this[`${type}BL`]]).concat(customBlacklist || [])
|
|
300
|
+
|
|
301
|
+
// Loop blacklists
|
|
302
|
+
for (let _key of list) {
|
|
303
|
+
let key = _key.replace(/^-/, '')
|
|
304
|
+
let whitelisted = _key.match(/^-/)
|
|
305
|
+
|
|
306
|
+
// Remove any child fields. E.g remove { user.token: 0 } = key2 if iterating { user: 0 } = key
|
|
307
|
+
for (let key2 in projection) {
|
|
308
|
+
// todo: need to write a test, testing that this is scoped to \.
|
|
309
|
+
if (key2.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) {
|
|
310
|
+
delete projection[key2]
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Whitelist
|
|
315
|
+
if (whitelisted) {
|
|
316
|
+
projection[key] = 1
|
|
317
|
+
// Whitelisting a child of a blacklisted field (blacklist expansion)
|
|
318
|
+
// let parent = '' // highest blacklisted parent
|
|
319
|
+
// for (let key2 in projection) {
|
|
320
|
+
// if (key2.length > parent.length && key.match(new RegExp('^' + key2.replace(/\./g, '\\.')))) {
|
|
321
|
+
// parent = key2
|
|
322
|
+
// }
|
|
323
|
+
// }
|
|
324
|
+
|
|
325
|
+
// Blacklist (only if there isn't a parent blacklisted)
|
|
326
|
+
} else {
|
|
327
|
+
let parent
|
|
328
|
+
for (let key2 in projection) { // E.g. [address = key2, addresses.country = key]
|
|
329
|
+
if (projection[key2] == 0 && key.match(new RegExp('^' + key2.replace(/\./g, '\\.') + '\\.'))) {
|
|
330
|
+
parent = key2
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (!parent) projection[key] = 0
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Remove whitelist projections
|
|
338
|
+
for (let key in projection) {
|
|
339
|
+
if (projection[key]) delete projection[key]
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return util.isEmpty(projection) ? undefined : projection
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
_getProjectionFromProject: function(customProject) {
|
|
346
|
+
/**
|
|
347
|
+
* Returns an in/exclusion projection
|
|
348
|
+
* todo: tests
|
|
349
|
+
*
|
|
350
|
+
* @param {object|array|string} customProject - normally passed through options
|
|
351
|
+
* @return {array|undefined} in/exclusion projection {'pets.name': 0}
|
|
352
|
+
* @this model
|
|
353
|
+
*/
|
|
354
|
+
let projection
|
|
355
|
+
if (util.isString(customProject)) {
|
|
356
|
+
customProject = customProject.trim().split(/\s+/)
|
|
357
|
+
}
|
|
358
|
+
if (util.isArray(customProject)) {
|
|
359
|
+
projection = customProject.reduce((o, v) => {
|
|
360
|
+
o[v.replace(/^-/, '')] = v.match(/^-/)? 0 : 1
|
|
361
|
+
return o
|
|
362
|
+
}, {})
|
|
363
|
+
}
|
|
364
|
+
return projection
|
|
365
|
+
},
|
|
366
|
+
|
|
273
367
|
_queryObject: async function(opts, type, one) {
|
|
274
368
|
/**
|
|
275
369
|
* Normalise options
|
|
276
370
|
* @param {MongoId|string|object} opts
|
|
277
371
|
* @param {string} type - operation type
|
|
278
372
|
* @param {boolean} one - return one document
|
|
279
|
-
* @this model
|
|
280
373
|
* @return {Promise} opts
|
|
374
|
+
* @this model
|
|
281
375
|
*
|
|
376
|
+
* Query parsing logic:
|
|
282
377
|
* opts == string|MongodId - treated as an id
|
|
283
378
|
* opts == undefined|null|false - throw error
|
|
284
379
|
* opts.query == string|MongodID - treated as an id
|
|
@@ -321,6 +416,7 @@ module.exports = {
|
|
|
321
416
|
if (!util.isDefined(opts.data) && util.isDefined((opts.req||{}).body)) opts.data = opts.req.body
|
|
322
417
|
if (util.isDefined(opts.data)) opts.data = await util.parseData(opts.data)
|
|
323
418
|
|
|
419
|
+
opts.type = type
|
|
324
420
|
opts[type] = true
|
|
325
421
|
opts.model = this
|
|
326
422
|
return opts
|
|
@@ -332,11 +428,12 @@ module.exports = {
|
|
|
332
428
|
* Recurses through fields that are models and populates missing default-fields and calls model.afterFind([fn,..])
|
|
333
429
|
* Be sure to add any virtual fields to the schema that your populating on,
|
|
334
430
|
* e.g. "nurses": [{ model: 'user' }]
|
|
431
|
+
*
|
|
335
432
|
* @param {object|array|null} data
|
|
336
433
|
* @param {object} projection - $project object
|
|
337
434
|
* @param {object} afterFindContext - handy context object given to schema.afterFind
|
|
338
|
-
* @this model
|
|
339
435
|
* @return Promise(data)
|
|
436
|
+
* @this model
|
|
340
437
|
*/
|
|
341
438
|
// Recurse down from the parent model, ending with the parent model as the parent afterFind hook may
|
|
342
439
|
// want to manipulate any populated models
|
|
@@ -347,17 +444,18 @@ module.exports = {
|
|
|
347
444
|
|
|
348
445
|
// Loop found model/deep-model data objects, and populate missing default-fields and call afterFind on each
|
|
349
446
|
for (let item of modelData) {
|
|
350
|
-
//
|
|
447
|
+
// Populate missing default fields if data !== null
|
|
351
448
|
// NOTE: maybe only call functions if default is being set.. fine for now
|
|
352
449
|
if (item.dataRef) {
|
|
353
|
-
util.forEach(model[item.modelName].
|
|
450
|
+
util.forEach(model[item.modelName].fieldsFlattened, (schema, path) => {
|
|
451
|
+
if (!util.isDefined(schema.default) || path.match(/^\.?(createdAt|updatedAt)$/)) return
|
|
354
452
|
let parentPath = item.fieldName? item.fieldName + '.' : ''
|
|
355
453
|
let pathWithoutArrays = (parentPath + path).replace(/\.0(\.|$)/, '$1')
|
|
356
|
-
// Ignore default fields that are
|
|
357
|
-
if (
|
|
358
|
-
//
|
|
454
|
+
// Ignore default fields that are blacklisted
|
|
455
|
+
if (this._pathBlacklisted(pathWithoutArrays, projection)) return
|
|
456
|
+
// console.log(pathWithoutArrays, path, projection)
|
|
457
|
+
// Set value
|
|
359
458
|
let value = util.isFunction(schema.default)? schema.default(this.manager) : schema.default
|
|
360
|
-
// console.log(item.dataRef)
|
|
361
459
|
util.setDeepValue(item.dataRef, path.replace(/\.0(\.|$)/g, '.$$$1'), value, true, false, true)
|
|
362
460
|
})
|
|
363
461
|
}
|
|
@@ -369,190 +467,34 @@ module.exports = {
|
|
|
369
467
|
return util.runSeries(callbackSeries).then(() => data)
|
|
370
468
|
},
|
|
371
469
|
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* Include deep-model blacklists into the projection
|
|
375
|
-
* @param {object} blacklistProject
|
|
376
|
-
* @param {array} populate - find populate array
|
|
377
|
-
* @return {object} exclusion blacklist
|
|
378
|
-
* @this model
|
|
379
|
-
*/
|
|
380
|
-
let model = this
|
|
381
|
-
let manager = this.manager
|
|
382
|
-
let paths = (populate||[]).map(o => o && o.as? o.as : o)
|
|
383
|
-
|
|
384
|
-
if (!paths.length) return blacklistProjection
|
|
385
|
-
this._recurseFields(model.fields, '', function(path, field) {
|
|
386
|
-
// Remove array indexes from the path e.g. '0.'
|
|
387
|
-
path = path.replace(/(\.[0-9]+)(\.|$)/, '$2')
|
|
388
|
-
if (!field.model || !paths.includes(path)) return
|
|
389
|
-
loop: for (let prop of manager.model[field.model].findBL) {
|
|
390
|
-
// Don't include any deep model projection keys that already have a parent specified. E.g if
|
|
391
|
-
// both { users.secrets: 1 } and { users.secrets.token: 1 } exist, remove the later
|
|
392
|
-
for (let key in blacklistProjection) {
|
|
393
|
-
if ((path + '.' + prop).match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) {
|
|
394
|
-
continue loop
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
// Add model property to blacklist
|
|
398
|
-
blacklistProjection[path + '.' + prop] = 0
|
|
399
|
-
}
|
|
400
|
-
})
|
|
401
|
-
return blacklistProjection
|
|
402
|
-
},
|
|
403
|
-
|
|
404
|
-
_addBlacklist: function(blacklistProjection, blacklist) {
|
|
405
|
-
/**
|
|
406
|
-
* Merge blacklist in
|
|
407
|
-
* @param {object} blacklistProjection
|
|
408
|
-
* @param {array} paths - e.g. ['password', '-email'] - email will be whitelisted / removed from
|
|
409
|
-
* exlcusion projection
|
|
410
|
-
* @return {object} exclusion blacklist
|
|
411
|
-
* @this model
|
|
412
|
-
*/
|
|
413
|
-
if (blacklist === false) {
|
|
414
|
-
return {}
|
|
415
|
-
} else if (!blacklist) {
|
|
416
|
-
return blacklistProjection
|
|
417
|
-
} else {
|
|
418
|
-
// Loop blacklist
|
|
419
|
-
for (let _key of blacklist) {
|
|
420
|
-
let key = _key.replace(/^-/, '')
|
|
421
|
-
let negated = _key.match(/^-/)
|
|
422
|
-
// Remove any deep projection keys that already have a parent specified. E.g if
|
|
423
|
-
// both { users.secrets: 0 } and { users.secrets.token: 0 } exist, remove the later
|
|
424
|
-
for (let key2 in blacklistProjection) {
|
|
425
|
-
if (key2.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) {
|
|
426
|
-
delete blacklistProjection[key2]
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
if (negated) {
|
|
430
|
-
if (blacklistProjection.hasOwnProperty(key)) delete blacklistProjection[key]
|
|
431
|
-
} else {
|
|
432
|
-
blacklistProjection[key] = 0
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
return blacklistProjection
|
|
436
|
-
}
|
|
437
|
-
},
|
|
438
|
-
|
|
439
|
-
_depreciated_removeBlacklisted: function(data, populate, whitelist) {
|
|
470
|
+
_pathBlacklisted: function(path, projection, matchDeepWhitelistedKeys=true) {
|
|
440
471
|
/**
|
|
441
|
-
*
|
|
442
|
-
* @param {object|array} data
|
|
443
|
-
* @param {array} <populate> find populate list
|
|
444
|
-
* @param {array} <whitelist>
|
|
445
|
-
*/
|
|
446
|
-
let findBL = this.findBL
|
|
447
|
-
let findWL = [ '_id', ...this.findWL ]
|
|
448
|
-
let model = this.manager.model
|
|
449
|
-
|
|
450
|
-
this._recurseFields(this.fields, '', function(path, field) {
|
|
451
|
-
// Remove array indexes from the path e.g. '0.'
|
|
452
|
-
path = path.replace(/(\.[0-9]+)(\.|$)/, '$2')
|
|
453
|
-
//if (field.type == 'any') findWL.push(path) //exclude.push(path)
|
|
454
|
-
|
|
455
|
-
// Below: Merge in model whitelists (we should cache the results)
|
|
456
|
-
if (!field.model) return
|
|
457
|
-
else if (!model[field.model]) return
|
|
458
|
-
|
|
459
|
-
// Has this path been blacklisted already?
|
|
460
|
-
for (let blacklisted of findBL) {
|
|
461
|
-
if (blacklisted === path || path.includes(blacklisted + '.', 0)) {
|
|
462
|
-
return
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Populating?
|
|
467
|
-
if ((populate||[]).includes(path)) {
|
|
468
|
-
// Remove the model-field from the whitelist if populating
|
|
469
|
-
let parentIndex = findWL.indexOf(path)
|
|
470
|
-
if (parentIndex !== -1) findWL.splice(parentIndex, 1)
|
|
471
|
-
|
|
472
|
-
// Okay, merge in the model's whitelist
|
|
473
|
-
findWL.push(path + '.' + '_id')
|
|
474
|
-
for (let prop of model[field.model].findWL) {
|
|
475
|
-
// Dont add model properties that are already blacklisted
|
|
476
|
-
if (findBL.includes(path + '.' + prop)) continue
|
|
477
|
-
// Add model prop to whitelist
|
|
478
|
-
findWL.push(path + '.' + prop)
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
})
|
|
482
|
-
|
|
483
|
-
// Merge in the passed in whitelist
|
|
484
|
-
findWL = findWL.concat(whitelist || [])
|
|
485
|
-
|
|
486
|
-
// Fill in missing parents e.g. pets.dog.name, pets.dog doesn't exist
|
|
487
|
-
// Doesn't matter what order these are added in
|
|
488
|
-
// for (let whitelisted of findWL) {
|
|
489
|
-
// let parents = whitelisted.split('.')
|
|
490
|
-
// let target = ''
|
|
491
|
-
// for (let parent of parents) {
|
|
492
|
-
// if (!findWL.includes(target + parent)) findWL.push(target + parent)
|
|
493
|
-
// target = target + parent + '.'
|
|
494
|
-
// }
|
|
495
|
-
// }
|
|
496
|
-
|
|
497
|
-
console.log(1, findWL)
|
|
498
|
-
console.log(2, data)
|
|
499
|
-
|
|
500
|
-
// "data.cat.colour" needs to add "data.cat"
|
|
501
|
-
// "data.cat" needs to everything
|
|
502
|
-
|
|
503
|
-
function recurseAndDeleteData(data, path) {
|
|
504
|
-
// Remove array indexes from the path e.g. '0.'
|
|
505
|
-
let newpath = path.replace(/(\.[0-9]+)(\.|$)/, '$2')
|
|
506
|
-
util.forEach(data, function(field, fieldName) {
|
|
507
|
-
if ((util.isArray(field) || util.isObjectAndNotID(field))/* && !exclude.includes(newpath + fieldName)*/) {
|
|
508
|
-
if (findWL.includes(newpath + fieldName)) return
|
|
509
|
-
recurseAndDeleteData(field, newpath + fieldName + '.')
|
|
510
|
-
} else if (!findWL.includes(newpath + fieldName) && !util.isNumber(fieldName)) {
|
|
511
|
-
console.log(3, fieldName)
|
|
512
|
-
delete data[fieldName]
|
|
513
|
-
}
|
|
514
|
-
})
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
for (let doc of util.toArray(data)) {
|
|
518
|
-
recurseAndDeleteData(doc, '')
|
|
519
|
-
}
|
|
520
|
-
return data
|
|
521
|
-
},
|
|
522
|
-
|
|
523
|
-
_pathInProjection: function(path, projection, matchParentPaths) {
|
|
524
|
-
/**
|
|
525
|
-
* Checks if the path is valid within a inclusion/exclusion projection
|
|
472
|
+
* Checks if the path is blacklisted within a inclusion/exclusion projection
|
|
526
473
|
* @param {string} path - path without array brackets e.g. '.[]'
|
|
527
|
-
* @param {object} projection
|
|
528
|
-
* @param {boolean}
|
|
474
|
+
* @param {object} projection - inclusion/exclusion projection, not mixed
|
|
475
|
+
* @param {boolean} matchDeepWhitelistedKeys - match deep whitelisted keys containing path
|
|
476
|
+
* E.g. pets.color == pets.color.age
|
|
529
477
|
* @return {boolean}
|
|
530
478
|
*/
|
|
531
|
-
let inc
|
|
532
|
-
// console.log(path, projection)
|
|
533
479
|
for (let key in projection) {
|
|
534
|
-
if (projection[key]
|
|
535
|
-
// Inclusion
|
|
536
|
-
// E.g.
|
|
537
|
-
// E.g.
|
|
538
|
-
|
|
539
|
-
inc = true
|
|
540
|
-
if (key.match(new RegExp('^' + path.replace(/\./g, '\\.') + '(\\.|$)'))) return true
|
|
541
|
-
if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return true
|
|
542
|
-
} else if (projection[key]) {
|
|
543
|
-
// Inclusion (equal to key, or key included in path)
|
|
544
|
-
// E.g. 'pets.color.age'.match(/^pets.color.age(.|$)/) = match
|
|
545
|
-
// E.g. 'pets.color.age'.match(/^pets.color(.|$)/) = match
|
|
546
|
-
// E.g. 'pets.color'.match(/^pets.color.age(.|$)/) = no match
|
|
547
|
-
inc = true
|
|
548
|
-
if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return true
|
|
549
|
-
} else {
|
|
550
|
-
// Exclusion
|
|
551
|
-
// E.g. 'pets.color.age'.match(/^pets.color(.|$)/) = match
|
|
480
|
+
if (projection[key]) {
|
|
481
|
+
// Inclusion (whitelisted)
|
|
482
|
+
// E.g. pets.color.age == pets.color.age (exact match)
|
|
483
|
+
// E.g. pets.color.age == pets.color (path contains key)
|
|
484
|
+
var inclusion = true
|
|
552
485
|
if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return false
|
|
486
|
+
if (matchDeepWhitelistedKeys) {
|
|
487
|
+
// E.g. pets.color == pets.color.age (key contains path)
|
|
488
|
+
if (key.match(new RegExp('^' + path.replace(/\./g, '\\.') + '\\.'))) return false
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
// Exclusion (blacklisted)
|
|
492
|
+
// E.g. pets.color.age == pets.color.age (exact match)
|
|
493
|
+
// E.g. pets.color.age == pets.color (path contains key)
|
|
494
|
+
if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return true
|
|
553
495
|
}
|
|
554
496
|
}
|
|
555
|
-
return
|
|
497
|
+
return inclusion? true : false
|
|
556
498
|
},
|
|
557
499
|
|
|
558
500
|
_recurseAndFindModels: function(fields, dataArr) {
|
|
@@ -630,6 +572,6 @@ module.exports = {
|
|
|
630
572
|
cb(path + fieldName, field)
|
|
631
573
|
}
|
|
632
574
|
}, this)
|
|
633
|
-
}
|
|
575
|
+
},
|
|
634
576
|
|
|
635
577
|
}
|