monastery 2.2.2 → 3.0.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 +10 -1
- package/changelog.md +2 -0
- package/docs/_config.yml +2 -2
- package/docs/assets/imgs/monastery.jpg +0 -0
- package/docs/definition/index.md +1 -2
- package/docs/manager/index.md +19 -11
- package/docs/manager/model.md +1 -1
- package/docs/manager/models.md +2 -3
- package/docs/model/...rawMethods.md +289 -0
- package/docs/model/count.md +25 -0
- package/docs/model/find.md +5 -9
- package/docs/model/findOne.md +1 -1
- package/docs/model/findOneAndUpdate.md +1 -1
- package/docs/model/index.md +5 -30
- package/docs/model/insert.md +4 -6
- package/docs/model/remove.md +4 -6
- package/docs/model/update.md +4 -6
- package/docs/readme.md +78 -45
- package/lib/collection.js +324 -0
- package/lib/index.js +207 -67
- package/lib/model-crud.js +605 -619
- package/lib/model-validate.js +227 -245
- package/lib/model.js +70 -91
- package/lib/rules.js +36 -35
- package/lib/util.js +69 -15
- package/package.json +12 -11
- package/plugins/images/index.js +11 -11
- package/test/blacklisting.js +506 -537
- package/test/collection.js +445 -0
- package/test/crud.js +810 -730
- package/test/index.test.js +26 -0
- package/test/manager.js +77 -0
- package/test/mock/blacklisting.js +23 -23
- package/test/model.js +611 -572
- package/test/plugin-images.js +880 -965
- package/test/populate.js +249 -262
- package/test/util.js +126 -45
- package/test/validate.js +1074 -1121
- package/test/virtuals.js +222 -227
- package/lib/monk-monkey-patches.js +0 -90
- package/test/monk.js +0 -40
- package/test/test.js +0 -38
package/lib/model-crud.js
CHANGED
|
@@ -1,669 +1,655 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// Validate
|
|
29
|
-
let data = await this.validate(opts.data || {}, opts) // was { ...opts }
|
|
30
|
-
|
|
31
|
-
// Insert
|
|
32
|
-
await util.runSeries(this.beforeInsert.map(f => f.bind(opts, data)))
|
|
33
|
-
let response = await this._insert(data, util.omit(opts, this._queryOptions))
|
|
34
|
-
await util.runSeries(this.afterInsert.map(f => f.bind(opts, response)))
|
|
35
|
-
|
|
36
|
-
// Success/error
|
|
37
|
-
if (cb) cb(null, response)
|
|
38
|
-
else if (opts.req && opts.respond) opts.req.res.json(response)
|
|
39
|
-
else return Promise.resolve(response)
|
|
40
|
-
|
|
41
|
-
} catch (err) {
|
|
42
|
-
if (cb) cb(err)
|
|
43
|
-
else if (opts && opts.req && opts.respond) opts.req.res.error(err)
|
|
44
|
-
else throw err
|
|
45
|
-
}
|
|
46
|
-
},
|
|
47
|
-
|
|
48
|
-
find: async function(opts, cb, one) {
|
|
49
|
-
/**
|
|
50
|
-
* Finds document(s) with monk, also auto populates
|
|
51
|
-
* @param {object} opts
|
|
52
|
-
* @param {array|string|false} <opts.blacklist> - augment schema.findBL, `false` will remove all blacklisting
|
|
53
|
-
* @param {array} <opts.populate> - population, see docs
|
|
54
|
-
* @param {array|string} <opts.project> - return only these fields, ignores blacklisting
|
|
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>
|
|
58
|
-
* @param {function} <cb> - execute cb(err, data) instead of responding
|
|
59
|
-
* @return promise
|
|
60
|
-
* @this model
|
|
61
|
-
*/
|
|
62
|
-
if (cb && !util.isFunction(cb)) {
|
|
63
|
-
throw new Error(`The callback passed to ${this.name}.find() is not a function`)
|
|
64
|
-
}
|
|
65
|
-
try {
|
|
66
|
-
let lookups = []
|
|
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)
|
|
1
|
+
const util = require('./util.js')
|
|
2
|
+
const Model = require('./model.js')
|
|
3
|
+
|
|
4
|
+
Model.prototype.count = async function (opts) {
|
|
5
|
+
/**
|
|
6
|
+
* Count document(s)
|
|
7
|
+
* @param {object} opts
|
|
8
|
+
* @param {object} <opts.query> - mongodb query object
|
|
9
|
+
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
|
|
10
|
+
* @param {any} <any mongodb option>
|
|
11
|
+
* @return promise
|
|
12
|
+
* @this model
|
|
13
|
+
*/
|
|
14
|
+
try {
|
|
15
|
+
opts = await this._queryObject(opts, 'remove')
|
|
16
|
+
// Remove
|
|
17
|
+
const response = await this._count(opts.query, util.omit(opts, this._queryOptions))
|
|
18
|
+
// Success
|
|
19
|
+
if (opts.req && opts.respond) opts.req.res.json(response)
|
|
20
|
+
else return Promise.resolve(response)
|
|
21
|
+
|
|
22
|
+
} catch (err) {
|
|
23
|
+
if (opts && opts.req && opts.respond) opts.req.res.error(err)
|
|
24
|
+
else throw err
|
|
25
|
+
}
|
|
26
|
+
}
|
|
72
27
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
28
|
+
Model.prototype.insert = async function (opts) {
|
|
29
|
+
/**
|
|
30
|
+
* Inserts document(s) after validating data & before hooks.
|
|
31
|
+
* @param {object} opts
|
|
32
|
+
* @param {object|array} opts.data - documents to insert
|
|
33
|
+
* @param {array|string|false} <opts.blacklist> - augment schema.insertBL, `false` will remove blacklisting
|
|
34
|
+
* @param {array|string} <opts.project> - return only these fields, ignores blacklisting
|
|
35
|
+
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
|
|
36
|
+
* @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
|
|
37
|
+
* @param {boolean} <opts.timestamps> - whether `createdAt` and `updatedAt` are automatically inserted
|
|
38
|
+
* @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
|
|
39
|
+
* default, but false on update
|
|
40
|
+
* @param {any} <any mongodb option>
|
|
41
|
+
* @return promise
|
|
42
|
+
* @this model
|
|
43
|
+
*/
|
|
44
|
+
try {
|
|
45
|
+
opts = await this._queryObject(opts, 'insert')
|
|
46
|
+
|
|
47
|
+
// Validate
|
|
48
|
+
let data = await this.validate(opts.data || {}, opts) // was { ...opts }
|
|
49
|
+
|
|
50
|
+
// Insert
|
|
51
|
+
await util.runSeries(this.beforeInsert.map(f => f.bind(opts, data)))
|
|
52
|
+
let response = await this._insert(data, util.omit(opts, this._queryOptions))
|
|
53
|
+
await util.runSeries(this.afterInsert.map(f => f.bind(opts, response)))
|
|
54
|
+
|
|
55
|
+
// Success/error
|
|
56
|
+
if (opts.req && opts.respond) opts.req.res.json(response)
|
|
57
|
+
else return Promise.resolve(response)
|
|
58
|
+
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (opts && opts.req && opts.respond) opts.req.res.error(err)
|
|
61
|
+
else throw err
|
|
62
|
+
}
|
|
63
|
+
}
|
|
78
64
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
// Convert array into a document for non-array targets
|
|
131
|
-
if (!arrayTarget) {
|
|
132
|
-
(opts.addFields = opts.addFields || {})[path] = { '$arrayElemAt': [ '$' + path, 0 ] }
|
|
65
|
+
Model.prototype.find = async function (opts, _one) {
|
|
66
|
+
/**
|
|
67
|
+
* Finds document(s), with auto population
|
|
68
|
+
* @param {object} opts
|
|
69
|
+
* @param {array|string|false} <opts.blacklist> - augment schema.findBL, `false` will remove all blacklisting
|
|
70
|
+
* @param {array} <opts.populate> - population, see docs
|
|
71
|
+
* @param {array|string} <opts.project> - return only these fields, ignores blacklisting
|
|
72
|
+
* @param {object} <opts.query> - mongodb query object
|
|
73
|
+
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
|
|
74
|
+
* @param {any} <any mongodb option>
|
|
75
|
+
* @param {boolean} <_one> - return one document
|
|
76
|
+
* @return promise
|
|
77
|
+
* @this model
|
|
78
|
+
*/
|
|
79
|
+
try {
|
|
80
|
+
let lookups = []
|
|
81
|
+
opts = await this._queryObject(opts, 'find', _one)
|
|
82
|
+
|
|
83
|
+
// Get projection
|
|
84
|
+
if (opts.project) opts.projection = this._getProjectionFromProject(opts.project)
|
|
85
|
+
else opts.projection = this._getProjectionFromBlacklist(opts.type, opts.blacklist)
|
|
86
|
+
|
|
87
|
+
// Has text search?
|
|
88
|
+
// if (opts.query.$text) {
|
|
89
|
+
// opts.projection.score = { $meta: 'textScore' }
|
|
90
|
+
// opts.sort = { score: { $meta: 'textScore' }}
|
|
91
|
+
// }
|
|
92
|
+
|
|
93
|
+
// Wanting to populate?
|
|
94
|
+
if (!opts.populate) {
|
|
95
|
+
var response = await this[`_find${opts._one? 'One' : ''}`](opts.query, util.omit(opts, this._queryOptions))
|
|
96
|
+
} else {
|
|
97
|
+
loop: for (let item of opts.populate) {
|
|
98
|
+
let path = util.isObject(item)? item.as : item
|
|
99
|
+
// Blacklisted?
|
|
100
|
+
if (this._pathBlacklisted(path, opts.projection)) continue loop
|
|
101
|
+
// Custom $lookup definition
|
|
102
|
+
// https://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-36-lookup-expr
|
|
103
|
+
if (util.isObject(item)) {
|
|
104
|
+
lookups.push({ $lookup: item })
|
|
105
|
+
} else {
|
|
106
|
+
let arrayTarget
|
|
107
|
+
let arrayCount = 0
|
|
108
|
+
let schema = path.split('.').reduce((o, i) => {
|
|
109
|
+
if (util.isArray(o[i])) {
|
|
110
|
+
arrayCount++
|
|
111
|
+
arrayTarget = true
|
|
112
|
+
return o[i][0]
|
|
113
|
+
} else {
|
|
114
|
+
arrayTarget = false
|
|
115
|
+
return o[i]
|
|
133
116
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
117
|
+
}, this.fields)
|
|
118
|
+
let modelName = (schema||{}).model
|
|
119
|
+
if (!modelName) {
|
|
120
|
+
this.error(
|
|
121
|
+
`The field "${path}" passed to populate is not of type model. You would ` +
|
|
122
|
+
'need to add the field option e.g. { model: \'comment\' } in your schema.'
|
|
123
|
+
)
|
|
124
|
+
continue
|
|
125
|
+
} else if (arrayCount > 1) {
|
|
126
|
+
this.error(
|
|
127
|
+
`You cannot populate on array's nested in array's: ${path}: ` +
|
|
128
|
+
`{ model: "${modelName}" }`
|
|
129
|
+
)
|
|
130
|
+
continue
|
|
131
|
+
} else if (arrayCount == 1 && !arrayTarget) {
|
|
132
|
+
this.error(
|
|
133
|
+
`You cannot populate within an array of sub-documents: ${path}: ` +
|
|
134
|
+
`{ model: "${modelName}" }`
|
|
135
|
+
)
|
|
136
|
+
continue
|
|
137
|
+
} else if (!this.manager.models[modelName]) {
|
|
138
|
+
this.error(
|
|
139
|
+
`The field's model defined in your schema does not exist: ${path}: ` +
|
|
140
|
+
`{ model: "${modelName}" }`
|
|
141
|
+
)
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
// Convert array into a document for non-array targets
|
|
145
|
+
if (!arrayTarget) {
|
|
146
|
+
(opts.addFields = opts.addFields || {})[path] = { '$arrayElemAt': [ '$' + path, 0 ] }
|
|
141
147
|
}
|
|
148
|
+
// Create lookup
|
|
149
|
+
lookups.push({ $lookup: {
|
|
150
|
+
from: modelName,
|
|
151
|
+
localField: path,
|
|
152
|
+
foreignField: '_id',
|
|
153
|
+
as: path,
|
|
154
|
+
}})
|
|
142
155
|
}
|
|
143
|
-
// console.log(1, opts.projection)
|
|
144
|
-
// console.log(2, lookups)
|
|
145
|
-
let aggregate = [
|
|
146
|
-
{ $match: opts.query },
|
|
147
|
-
{ $sort: opts.sort },
|
|
148
|
-
{ $skip: opts.skip },
|
|
149
|
-
...(opts.limit? [{ $limit: opts.limit }] : []),
|
|
150
|
-
...lookups,
|
|
151
|
-
...(opts.addFields? [{ $addFields: opts.addFields }] : []),
|
|
152
|
-
...(opts.projection? [{ $project: opts.projection }] : []),
|
|
153
|
-
]
|
|
154
|
-
response = await this._aggregate(aggregate)
|
|
155
|
-
this.info('aggregate', JSON.stringify(aggregate))
|
|
156
156
|
}
|
|
157
|
+
// console.log(1, opts.projection)
|
|
158
|
+
// console.log(2, lookups)
|
|
159
|
+
let aggregate = [
|
|
160
|
+
{ $match: opts.query },
|
|
161
|
+
...(util.isDefined(opts.sort) ? [{ $sort: opts.sort }] : []),
|
|
162
|
+
...(util.isDefined(opts.skip) ? [{ $limit: opts.skip }] : []),
|
|
163
|
+
...(util.isDefined(opts.limit) ? [{ $limit: opts.limit }] : []),
|
|
164
|
+
...lookups,
|
|
165
|
+
...(opts.addFields? [{ $addFields: opts.addFields }] : []),
|
|
166
|
+
...(opts.projection? [{ $project: opts.projection }] : []),
|
|
167
|
+
]
|
|
168
|
+
response = await this._aggregate(aggregate)
|
|
169
|
+
this.info('aggregate', JSON.stringify(aggregate))
|
|
170
|
+
}
|
|
157
171
|
|
|
158
|
-
|
|
159
|
-
|
|
172
|
+
// Returning one?
|
|
173
|
+
if (opts._one && util.isArray(response)) response = response[0] || null
|
|
160
174
|
|
|
161
|
-
|
|
162
|
-
|
|
175
|
+
// Process afterFind hooks
|
|
176
|
+
response = await this._processAfterFind(response, opts.projection, opts)
|
|
177
|
+
|
|
178
|
+
// Success
|
|
179
|
+
if (opts.req && opts.respond) opts.req.res.json(response)
|
|
180
|
+
else return Promise.resolve(response)
|
|
181
|
+
|
|
182
|
+
} catch (err) {
|
|
183
|
+
if (opts && opts.req && opts.respond) opts.req.res.error(err)
|
|
184
|
+
else throw err
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
Model.prototype.findOne = async function (opts) {
|
|
189
|
+
return this.find(opts, true)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
Model.prototype.findOneAndUpdate = async function (opts) {
|
|
193
|
+
/**
|
|
194
|
+
* Find and update document(s) with auto population
|
|
195
|
+
* @param {object} opts
|
|
196
|
+
* @param {array|string|false} <opts.blacklist> - augment findBL/updateBL, `false` will remove all blacklisting
|
|
197
|
+
* @param {array} <opts.populate> - find population, see docs
|
|
198
|
+
* @param {array|string} <opts.project> - return only these fields, ignores blacklisting
|
|
199
|
+
* @param {object} <opts.query> - mongodb query object
|
|
200
|
+
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
|
|
201
|
+
* @param {any} <any mongodb option>
|
|
202
|
+
*
|
|
203
|
+
* Update options:
|
|
204
|
+
* @param {object|array} opts.data - mongodb document update object(s)
|
|
205
|
+
* @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
|
|
206
|
+
* @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
|
|
207
|
+
* @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
|
|
208
|
+
* default, but false on update
|
|
209
|
+
* @return promise
|
|
210
|
+
* @this model
|
|
211
|
+
*/
|
|
212
|
+
|
|
213
|
+
if (opts.populate) {
|
|
214
|
+
try {
|
|
215
|
+
// todo: add transaction flag
|
|
216
|
+
delete opts.multi
|
|
217
|
+
let update = await this.update(opts, 'findOneAndUpdate')
|
|
218
|
+
if (update) var response = await this.findOne(opts)
|
|
219
|
+
else response = update
|
|
163
220
|
|
|
164
221
|
// Success
|
|
165
|
-
if (
|
|
166
|
-
else if (opts.req && opts.respond) opts.req.res.json(response)
|
|
222
|
+
if (opts.req && opts.respond) opts.req.res.json(response)
|
|
167
223
|
else return Promise.resolve(response)
|
|
168
224
|
|
|
169
|
-
} catch (
|
|
170
|
-
if (
|
|
171
|
-
else
|
|
172
|
-
else throw err
|
|
225
|
+
} catch (e) {
|
|
226
|
+
if (opts && opts.req && opts.respond) opts.req.res.error(e)
|
|
227
|
+
else throw e
|
|
173
228
|
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
let update = await this.update(opts, null, 'findOneAndUpdate')
|
|
207
|
-
if (update) var response = await this.findOne(opts)
|
|
208
|
-
else response = update
|
|
209
|
-
|
|
210
|
-
// Success
|
|
211
|
-
if (cb) cb(null, response)
|
|
212
|
-
else if (opts.req && opts.respond) opts.req.res.json(response)
|
|
213
|
-
else return Promise.resolve(response)
|
|
214
|
-
|
|
215
|
-
} catch (e) {
|
|
216
|
-
if (cb) cb(e)
|
|
217
|
-
else if (opts && opts.req && opts.respond) opts.req.res.error(e)
|
|
218
|
-
else throw e
|
|
219
|
-
}
|
|
220
|
-
} else {
|
|
221
|
-
return this.update(opts, cb, 'findOneAndUpdate')
|
|
229
|
+
} else {
|
|
230
|
+
return this.update(opts, 'findOneAndUpdate')
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
Model.prototype.update = async function (opts, type='update') {
|
|
235
|
+
/**
|
|
236
|
+
* Updates document(s) after validating data & before hooks.
|
|
237
|
+
* @param {object} opts
|
|
238
|
+
* @param {object|array} opts.data - mongodb document update object(s)
|
|
239
|
+
* @param {array|string|false} <opts.blacklist> - augment schema.updateBL, `false` will remove blacklisting
|
|
240
|
+
* @param {array|string} <opts.project> - return only these fields, ignores blacklisting
|
|
241
|
+
* @param {object} <opts.query> - mongodb query object
|
|
242
|
+
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
|
|
243
|
+
* @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
|
|
244
|
+
* @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
|
|
245
|
+
* @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
|
|
246
|
+
* default, but false on update
|
|
247
|
+
* @param {any} <any mongodb option>
|
|
248
|
+
* @param {function} <type> - 'update', or 'findOneAndUpdate'
|
|
249
|
+
* @return promise(data)
|
|
250
|
+
* @this model
|
|
251
|
+
*/
|
|
252
|
+
try {
|
|
253
|
+
opts = await this._queryObject(opts, type)
|
|
254
|
+
let data = opts.data
|
|
255
|
+
let response = null
|
|
256
|
+
let operators = util.pick(opts, [/^\$/])
|
|
257
|
+
|
|
258
|
+
// Validate
|
|
259
|
+
if (util.isDefined(data)) {
|
|
260
|
+
data = await this.validate(opts.data, opts) // was {...opts}
|
|
222
261
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
* @param {object} opts
|
|
229
|
-
* @param {object|array} opts.data - mongodb document update object(s)
|
|
230
|
-
* @param {array|string|false} <opts.blacklist> - augment schema.updateBL, `false` will remove blacklisting
|
|
231
|
-
* @param {array|string} <opts.project> - return only these fields, ignores blacklisting
|
|
232
|
-
* @param {object} <opts.query> - mongodb query object
|
|
233
|
-
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
|
|
234
|
-
* @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
|
|
235
|
-
* @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
|
|
236
|
-
* @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
|
|
237
|
-
* default, but false on update
|
|
238
|
-
* @param {any} <any mongodb option>
|
|
239
|
-
* @param {function} <cb> - execute cb(err, data) instead of responding
|
|
240
|
-
* @param {function} <type> - 'update', or 'findOneAndUpdate'
|
|
241
|
-
* @return promise(data)
|
|
242
|
-
* @this model
|
|
243
|
-
*/
|
|
244
|
-
if (cb && !util.isFunction(cb)) {
|
|
245
|
-
throw new Error(`The callback passed to ${this.name}.${type}() is not a function`)
|
|
262
|
+
if (!util.isDefined(data) && util.isEmpty(operators)) {
|
|
263
|
+
throw new Error(`Please pass an update operator to ${this.name}.${type}(), e.g. data, $unset, etc`)
|
|
264
|
+
}
|
|
265
|
+
if (util.isDefined(data) && (!data || util.isEmpty(data))) {
|
|
266
|
+
throw new Error(`No valid data passed to ${this.name}.${type}({ data: .. })`)
|
|
246
267
|
}
|
|
247
|
-
try {
|
|
248
|
-
opts = await this._queryObject(opts, type)
|
|
249
|
-
let data = opts.data
|
|
250
|
-
let response = null
|
|
251
|
-
let operators = util.pick(opts, [/^\$/])
|
|
252
|
-
|
|
253
|
-
// Validate
|
|
254
|
-
if (util.isDefined(data)) {
|
|
255
|
-
data = await this.validate(opts.data, opts) // was {...opts}
|
|
256
|
-
}
|
|
257
|
-
if (!util.isDefined(data) && util.isEmpty(operators)) {
|
|
258
|
-
throw new Error(`Please pass an update operator to ${this.name}.${type}(), e.g. data, $unset, etc`)
|
|
259
|
-
}
|
|
260
|
-
if (util.isDefined(data) && (!data || util.isEmpty(data))) {
|
|
261
|
-
throw new Error(`No valid data passed to ${this.name}.${type}({ data: .. })`)
|
|
262
|
-
}
|
|
263
268
|
|
|
264
|
-
|
|
265
|
-
|
|
269
|
+
// Hook: beforeUpdate (has access to original, non-validated opts.data)
|
|
270
|
+
await util.runSeries(this.beforeUpdate.map(f => f.bind(opts, data||{})))
|
|
266
271
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
272
|
+
if (data && operators['$set']) {
|
|
273
|
+
this.info(`'$set' fields take precedence over the data fields for \`${this.name}.${type}()\``)
|
|
274
|
+
}
|
|
275
|
+
if (data || operators['$set']) {
|
|
276
|
+
operators['$set'] = { ...data, ...(operators['$set'] || {}) }
|
|
277
|
+
}
|
|
273
278
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
279
|
+
// findOneAndUpdate, get 'find' projection
|
|
280
|
+
if (type == 'findOneAndUpdate') {
|
|
281
|
+
if (opts.project) opts.projection = this._getProjectionFromProject(opts.project)
|
|
282
|
+
else opts.projection = this._getProjectionFromBlacklist('find', opts.blacklist)
|
|
283
|
+
// Just peform a normal update if we need to populate a findOneAndUpdate
|
|
284
|
+
if (opts.populate) type = 'update'
|
|
285
|
+
}
|
|
281
286
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
287
|
+
// Update
|
|
288
|
+
let update = await this['_' + type](opts.query, operators, util.omit(opts, this._queryOptions))
|
|
289
|
+
if (type == 'findOneAndUpdate') {
|
|
290
|
+
response = update
|
|
291
|
+
} else if (util.isDefined(update.upsertedId)) {
|
|
292
|
+
response = Object.assign(
|
|
286
293
|
Object.create({ _output: update }),
|
|
287
294
|
operators['$set'] || {},
|
|
288
|
-
|
|
295
|
+
update.upsertedId ? { _id: update.upsertedId } : {}
|
|
289
296
|
)
|
|
297
|
+
}
|
|
290
298
|
|
|
291
|
-
|
|
292
|
-
|
|
299
|
+
// Hook: afterUpdate (doesn't have access to validated data)
|
|
300
|
+
if (response) await util.runSeries(this.afterUpdate.map(f => f.bind(opts, response)))
|
|
293
301
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Success
|
|
300
|
-
if (cb) cb(null, response)
|
|
301
|
-
else if (opts.req && opts.respond) opts.req.res.json(response)
|
|
302
|
-
else return response
|
|
303
|
-
|
|
304
|
-
} catch (err) {
|
|
305
|
-
if (cb) cb(err)
|
|
306
|
-
else if (opts && opts.req && opts.respond) opts.req.res.error(err)
|
|
307
|
-
else throw err
|
|
308
|
-
}
|
|
309
|
-
},
|
|
310
|
-
|
|
311
|
-
remove: async function(opts, cb) {
|
|
312
|
-
/**
|
|
313
|
-
* Remove document(s) with monk after before hooks.
|
|
314
|
-
* @param {object} opts
|
|
315
|
-
* @param {object} <opts.query> - mongodb query object
|
|
316
|
-
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
|
|
317
|
-
* @param {boolean=true} <opts.multi> - set to false to limit the deletion to just one document
|
|
318
|
-
* @param {any} <any mongodb option>
|
|
319
|
-
* @param {function} <cb> - execute cb(err, data) instead of responding
|
|
320
|
-
* @return promise
|
|
321
|
-
* @this model
|
|
322
|
-
*/
|
|
323
|
-
if (cb && !util.isFunction(cb)) {
|
|
324
|
-
throw new Error(`The callback passed to ${this.name}.remove() is not a function`)
|
|
302
|
+
// Hook: afterFind if findOneAndUpdate
|
|
303
|
+
if (response && type == 'findOneAndUpdate') {
|
|
304
|
+
response = await this._processAfterFind(response, opts.projection, opts)
|
|
325
305
|
}
|
|
326
|
-
try {
|
|
327
|
-
opts = await this._queryObject(opts, 'remove')
|
|
328
306
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
307
|
+
// Success
|
|
308
|
+
if (opts.req && opts.respond) opts.req.res.json(response)
|
|
309
|
+
else return response
|
|
310
|
+
|
|
311
|
+
} catch (err) {
|
|
312
|
+
if (opts && opts.req && opts.respond) opts.req.res.error(err)
|
|
313
|
+
else throw err
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
|
|
317
|
+
Model.prototype.remove = async function (opts) {
|
|
318
|
+
/**
|
|
319
|
+
* Remove document(s) with before and after hooks.
|
|
320
|
+
* @param {object} opts
|
|
321
|
+
* @param {object} <opts.query> - mongodb query object
|
|
322
|
+
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
|
|
323
|
+
* @param {boolean=true} <opts.multi> - set to false to limit the deletion to just one document
|
|
324
|
+
* @param {any} <any mongodb option>
|
|
325
|
+
* @return promise
|
|
326
|
+
* @this model
|
|
327
|
+
*/
|
|
328
|
+
try {
|
|
329
|
+
opts = await this._queryObject(opts, 'remove')
|
|
330
|
+
|
|
331
|
+
// Remove
|
|
332
|
+
await util.runSeries(this.beforeRemove.map(f => f.bind(opts)))
|
|
333
|
+
let response = await this._remove(opts.query, util.omit(opts, this._queryOptions))
|
|
334
|
+
await util.runSeries(this.afterRemove.map(f => f.bind(response)))
|
|
335
|
+
|
|
336
|
+
// Success
|
|
337
|
+
if (opts.req && opts.respond) opts.req.res.json(response)
|
|
338
|
+
else return Promise.resolve(response)
|
|
339
|
+
|
|
340
|
+
} catch (err) {
|
|
341
|
+
if (opts && opts.req && opts.respond) opts.req.res.error(err)
|
|
342
|
+
else throw err
|
|
343
|
+
}
|
|
344
|
+
}
|
|
338
345
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
346
|
+
Model.prototype._getProjectionFromBlacklist = function (type, customBlacklist) {
|
|
347
|
+
/**
|
|
348
|
+
* Returns an exclusion projection
|
|
349
|
+
*
|
|
350
|
+
* Path collisions are removed
|
|
351
|
+
* E.g. ['pets.dogs', 'pets.dogs.name', '-cat', 'pets.dogs.age'] = { 'pets.dog': 0 }
|
|
352
|
+
*
|
|
353
|
+
* @param {string} type - find, insert, or update
|
|
354
|
+
* @param {array|string|false} customBlacklist - normally passed through options
|
|
355
|
+
* @return {array|undefined} exclusion $project {'pets.name': 0}
|
|
356
|
+
* @this model
|
|
357
|
+
*
|
|
358
|
+
* 1. collate deep-blacklists
|
|
359
|
+
* 2. concatenate the model's blacklist and any custom blacklist
|
|
360
|
+
* 3. create an exclusion projection object from the blacklist, overriding from left to right
|
|
361
|
+
*/
|
|
362
|
+
|
|
363
|
+
let list = []
|
|
364
|
+
let manager = this.manager
|
|
365
|
+
let projection = {}
|
|
366
|
+
if (customBlacklist === false) return
|
|
367
|
+
|
|
368
|
+
// String?
|
|
369
|
+
if (typeof customBlacklist === 'string') {
|
|
370
|
+
customBlacklist = customBlacklist.trim().split(/\s+/)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Concat deep blacklists
|
|
374
|
+
if (type == 'find') {
|
|
375
|
+
util.forEach(this.fieldsFlattened, (schema, path) => {
|
|
376
|
+
if (!schema.model) return
|
|
377
|
+
let deepBL = manager.models[schema.model][`${type}BL`] || []
|
|
378
|
+
let pathWithoutArrays = path.replace(/\.0(\.|$)/, '$1')
|
|
379
|
+
list = list.concat(deepBL.map(o => {
|
|
380
|
+
return `${o.charAt(0) == '-'? '-' : ''}${pathWithoutArrays}.${o.replace(/^-/, '')}`
|
|
381
|
+
}))
|
|
382
|
+
})
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Concat model, and custom blacklists
|
|
386
|
+
list = list.concat([...this[`${type}BL`]]).concat(customBlacklist || [])
|
|
387
|
+
|
|
388
|
+
// Loop blacklists
|
|
389
|
+
for (let _key of list) {
|
|
390
|
+
let key = _key.replace(/^-/, '')
|
|
391
|
+
let whitelisted = _key.match(/^-/)
|
|
392
|
+
|
|
393
|
+
// Remove any child fields. E.g remove { user.token: 0 } = key2 if iterating { user: 0 } = key
|
|
394
|
+
for (let key2 in projection) {
|
|
395
|
+
// todo: need to write a test, testing that this is scoped to \.
|
|
396
|
+
if (key2.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) {
|
|
397
|
+
delete projection[key2]
|
|
398
|
+
}
|
|
371
399
|
}
|
|
372
400
|
|
|
373
|
-
//
|
|
374
|
-
if (
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
}
|
|
401
|
+
// Whitelist
|
|
402
|
+
if (whitelisted) {
|
|
403
|
+
projection[key] = 1
|
|
404
|
+
// Whitelisting a child of a blacklisted field (blacklist expansion)
|
|
405
|
+
// let parent = '' // highest blacklisted parent
|
|
406
|
+
// for (let key2 in projection) {
|
|
407
|
+
// if (key2.length > parent.length && key.match(new RegExp('^' + key2.replace(/\./g, '\\.')))) {
|
|
408
|
+
// parent = key2
|
|
409
|
+
// }
|
|
410
|
+
// }
|
|
411
|
+
|
|
412
|
+
// Blacklist (only if there isn't a parent blacklisted)
|
|
413
|
+
} else {
|
|
414
|
+
let parent
|
|
415
|
+
for (let key2 in projection) { // E.g. [address = key2, addresses.country = key]
|
|
416
|
+
if (projection[key2] == 0 && key.match(new RegExp('^' + key2.replace(/\./g, '\\.') + '\\.'))) {
|
|
417
|
+
parent = key2
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
if (!parent) projection[key] = 0
|
|
383
421
|
}
|
|
422
|
+
}
|
|
384
423
|
|
|
385
|
-
|
|
386
|
-
|
|
424
|
+
// Remove whitelist projections
|
|
425
|
+
for (let key in projection) {
|
|
426
|
+
if (projection[key]) delete projection[key]
|
|
427
|
+
}
|
|
387
428
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
let key = _key.replace(/^-/, '')
|
|
391
|
-
let whitelisted = _key.match(/^-/)
|
|
429
|
+
return util.isEmpty(projection) ? undefined : projection
|
|
430
|
+
}
|
|
392
431
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
432
|
+
Model.prototype._getProjectionFromProject = function (customProject) {
|
|
433
|
+
/**
|
|
434
|
+
* Returns an in/exclusion projection
|
|
435
|
+
* todo: tests
|
|
436
|
+
*
|
|
437
|
+
* @param {object|array|string} customProject - normally passed through options
|
|
438
|
+
* @return {array|undefined} in/exclusion projection {'pets.name': 0}
|
|
439
|
+
* @this model
|
|
440
|
+
*/
|
|
441
|
+
let projection
|
|
442
|
+
if (util.isString(customProject)) {
|
|
443
|
+
customProject = customProject.trim().split(/\s+/)
|
|
444
|
+
}
|
|
445
|
+
if (util.isArray(customProject)) {
|
|
446
|
+
projection = customProject.reduce((o, v) => {
|
|
447
|
+
o[v.replace(/^-/, '')] = v.match(/^-/)? 0 : 1
|
|
448
|
+
return o
|
|
449
|
+
}, {})
|
|
450
|
+
}
|
|
451
|
+
return projection
|
|
452
|
+
}
|
|
400
453
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
}
|
|
454
|
+
Model.prototype._queryObject = async function (opts, type, _one) {
|
|
455
|
+
/**
|
|
456
|
+
* Normalize options
|
|
457
|
+
* @param {MongoId|string|object} opts
|
|
458
|
+
* @param {string} type - insert, update, find, remove, findOneAndUpdate
|
|
459
|
+
* @param {boolean} _one - return one document
|
|
460
|
+
* @return {Promise} opts
|
|
461
|
+
* @this model
|
|
462
|
+
*
|
|
463
|
+
* Query parsing logic:
|
|
464
|
+
* opts == string|MongodId - treated as an id
|
|
465
|
+
* opts == undefined|null|false - throw error
|
|
466
|
+
* opts.query == string|MongodID - treated as an id
|
|
467
|
+
* opts.query == undefined|null|false - throw error
|
|
468
|
+
*/
|
|
469
|
+
|
|
470
|
+
// Query
|
|
471
|
+
if (type != 'insert') {
|
|
472
|
+
let isIdType = (o) => util.isId(o) || util.isString(o)
|
|
473
|
+
if (isIdType(opts)) {
|
|
474
|
+
opts = { query: { _id: opts || '' }}
|
|
422
475
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
for (let key in projection) {
|
|
426
|
-
if (projection[key]) delete projection[key]
|
|
476
|
+
if (isIdType((opts||{}).query)) {
|
|
477
|
+
opts.query = { _id: opts.query || '' }
|
|
427
478
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
},
|
|
431
|
-
|
|
432
|
-
_getProjectionFromProject: function(customProject) {
|
|
433
|
-
/**
|
|
434
|
-
* Returns an in/exclusion projection
|
|
435
|
-
* todo: tests
|
|
436
|
-
*
|
|
437
|
-
* @param {object|array|string} customProject - normally passed through options
|
|
438
|
-
* @return {array|undefined} in/exclusion projection {'pets.name': 0}
|
|
439
|
-
* @this model
|
|
440
|
-
*/
|
|
441
|
-
let projection
|
|
442
|
-
if (util.isString(customProject)) {
|
|
443
|
-
customProject = customProject.trim().split(/\s+/)
|
|
479
|
+
if (!util.isObject(opts) || !util.isObject(opts.query)) {
|
|
480
|
+
throw new Error('Please pass an object or MongoId to options.query')
|
|
444
481
|
}
|
|
445
|
-
if
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
return o
|
|
449
|
-
}, {})
|
|
482
|
+
// For security, if _id is set and undefined, throw an error
|
|
483
|
+
if (typeof opts.query._id == 'undefined' && opts.query.hasOwnProperty('_id')) {
|
|
484
|
+
throw new Error('Please pass an object or MongoId to options.query')
|
|
450
485
|
}
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
* opts == string|MongodId - treated as an id
|
|
465
|
-
* opts == undefined|null|false - throw error
|
|
466
|
-
* opts.query == string|MongodID - treated as an id
|
|
467
|
-
* opts.query == undefined|null|false - throw error
|
|
468
|
-
*/
|
|
469
|
-
|
|
470
|
-
// Query
|
|
471
|
-
if (type != 'insert') {
|
|
472
|
-
let isIdType = (o) => util.isId(o) || util.isString(o)
|
|
473
|
-
if (isIdType(opts)) {
|
|
474
|
-
opts = { query: { _id: opts || '' }}
|
|
475
|
-
}
|
|
476
|
-
if (isIdType((opts||{}).query)) {
|
|
477
|
-
opts.query = { _id: opts.query || '' }
|
|
478
|
-
}
|
|
479
|
-
if (!util.isObject(opts) || !util.isObject(opts.query)) {
|
|
480
|
-
throw new Error('Please pass an object or MongoId to options.query')
|
|
481
|
-
}
|
|
482
|
-
// For security, if _id is set and undefined, throw an error
|
|
483
|
-
if (typeof opts.query._id == 'undefined' && opts.query.hasOwnProperty('_id')) {
|
|
484
|
-
throw new Error('Please pass an object or MongoId to options.query')
|
|
485
|
-
}
|
|
486
|
-
if (util.isId(opts.query._id)) opts.query._id = this.manager.id(opts.query._id)
|
|
487
|
-
if (isIdType(opts.query._id) || one || type == 'findOneAndUpdate') opts.one = true
|
|
488
|
-
opts.query = util.removeUndefined(opts.query)
|
|
489
|
-
|
|
490
|
-
// Query options
|
|
491
|
-
opts.limit = opts.one? 1 : parseInt(opts.limit || this.manager.limit || 0)
|
|
492
|
-
opts.skip = Math.max(0, opts.skip || 0)
|
|
493
|
-
opts.sort = opts.sort || { 'createdAt': -1 }
|
|
494
|
-
if (util.isString(opts.sort)) {
|
|
495
|
-
let name = (opts.sort.match(/([a-z0-9-_]+)/) || [])[0]
|
|
496
|
-
let order = (opts.sort.match(/:(-?[0-9])/) || [])[1]
|
|
497
|
-
opts.sort = { [name]: parseInt(order || 1) }
|
|
498
|
-
}
|
|
486
|
+
if (util.isId(opts.query._id)) opts.query._id = this.manager.id(opts.query._id)
|
|
487
|
+
if (isIdType(opts.query._id) || _one || type == 'findOneAndUpdate') opts._one = true
|
|
488
|
+
opts.query = util.removeUndefined(opts.query)
|
|
489
|
+
|
|
490
|
+
// Query options
|
|
491
|
+
const limit = opts.limit || this.manager.opts.limit
|
|
492
|
+
opts.limit = opts._one ? 1 : (util.isDefined(limit) ? parseInt(limit) : undefined)
|
|
493
|
+
opts.skip = util.isDefined(opts.skip) ? Math.max(0, opts.skip || 0) : undefined
|
|
494
|
+
opts.sort = opts.sort || { 'createdAt': -1 }
|
|
495
|
+
if (util.isString(opts.sort)) {
|
|
496
|
+
let name = (opts.sort.match(/([a-z0-9-_]+)/) || [])[0]
|
|
497
|
+
let order = (opts.sort.match(/:(-?[0-9])/) || [])[1]
|
|
498
|
+
opts.sort = { [name]: parseInt(order || 1) }
|
|
499
499
|
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Data
|
|
503
|
+
if (!opts) opts = {}
|
|
504
|
+
if (!util.isDefined(opts.data) && util.isDefined((opts.req||{}).body)) opts.data = opts.req.body
|
|
505
|
+
if (util.isDefined(opts.data)) opts.data = await util.parseData(opts.data)
|
|
506
|
+
|
|
507
|
+
opts.type = type
|
|
508
|
+
opts[type] = true // still being included in the operation options..
|
|
509
|
+
opts.model = this
|
|
510
|
+
util.removeUndefined(opts)
|
|
511
|
+
return opts
|
|
512
|
+
}
|
|
500
513
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
*/
|
|
521
|
-
for (let key in projection) {
|
|
522
|
-
if (projection[key]) {
|
|
523
|
-
// Inclusion (whitelisted)
|
|
524
|
-
// E.g. pets.color.age == pets.color.age (exact match)
|
|
525
|
-
// E.g. pets.color.age == pets.color (path contains key)
|
|
526
|
-
var inclusion = true
|
|
527
|
-
if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return false
|
|
528
|
-
if (matchDeepWhitelistedKeys) {
|
|
529
|
-
// E.g. pets.color == pets.color.age (key contains path)
|
|
530
|
-
if (key.match(new RegExp('^' + path.replace(/\./g, '\\.') + '\\.'))) return false
|
|
531
|
-
}
|
|
532
|
-
} else {
|
|
533
|
-
// Exclusion (blacklisted)
|
|
534
|
-
// E.g. pets.color.age == pets.color.age (exact match)
|
|
535
|
-
// E.g. pets.color.age == pets.color (path contains key)
|
|
536
|
-
if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return true
|
|
514
|
+
Model.prototype._pathBlacklisted = function (path, projection, matchDeepWhitelistedKeys=true) {
|
|
515
|
+
/**
|
|
516
|
+
* Checks if the path is blacklisted within a inclusion/exclusion projection
|
|
517
|
+
* @param {string} path - path without array brackets e.g. '.[]'
|
|
518
|
+
* @param {object} projection - inclusion/exclusion projection, not mixed
|
|
519
|
+
* @param {boolean} matchDeepWhitelistedKeys - match deep whitelisted keys containing path
|
|
520
|
+
* E.g. pets.color == pets.color.age
|
|
521
|
+
* @return {boolean}
|
|
522
|
+
*/
|
|
523
|
+
for (let key in projection) {
|
|
524
|
+
if (projection[key]) {
|
|
525
|
+
// Inclusion (whitelisted)
|
|
526
|
+
// E.g. pets.color.age == pets.color.age (exact match)
|
|
527
|
+
// E.g. pets.color.age == pets.color (path contains key)
|
|
528
|
+
var inclusion = true
|
|
529
|
+
if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return false
|
|
530
|
+
if (matchDeepWhitelistedKeys) {
|
|
531
|
+
// E.g. pets.color == pets.color.age (key contains path)
|
|
532
|
+
if (key.match(new RegExp('^' + path.replace(/\./g, '\\.') + '\\.'))) return false
|
|
537
533
|
}
|
|
534
|
+
} else {
|
|
535
|
+
// Exclusion (blacklisted)
|
|
536
|
+
// E.g. pets.color.age == pets.color.age (exact match)
|
|
537
|
+
// E.g. pets.color.age == pets.color (path contains key)
|
|
538
|
+
if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return true
|
|
538
539
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
}
|
|
585
|
-
// Collect all of the model's afterFind hooks
|
|
586
|
-
for (let fn of model[item.modelName].afterFind) {
|
|
587
|
-
callbackSeries.push(fn.bind(afterFindContext, item.dataRef))
|
|
540
|
+
}
|
|
541
|
+
return inclusion? true : false
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
Model.prototype._processAfterFind = function (data, projection={}, afterFindContext={}) {
|
|
545
|
+
/**
|
|
546
|
+
* Todo: Maybe make this method public?
|
|
547
|
+
* Recurses through fields that are models and populates missing default-fields and calls model.afterFind([fn,..])
|
|
548
|
+
* Be sure to add any virtual fields to the schema that your populating on,
|
|
549
|
+
* e.g. "nurses": [{ model: 'user' }]
|
|
550
|
+
*
|
|
551
|
+
* @param {object|array|null} data
|
|
552
|
+
* @param {object} projection - opts.projection (== opts.blacklist is merged with all found deep model blacklists)
|
|
553
|
+
* @param {object} afterFindContext - handy context object given to schema.afterFind
|
|
554
|
+
* @return Promise(data)
|
|
555
|
+
* @this model
|
|
556
|
+
*/
|
|
557
|
+
// Recurse down from the parent model, ending with the parent model as the parent afterFind hook may
|
|
558
|
+
// want to manipulate any populated models
|
|
559
|
+
let callbackSeries = []
|
|
560
|
+
let models = this.manager.models
|
|
561
|
+
let parent = util.toArray(data).map(o => ({ dataRef: o, dataPath: '', dataFieldName: '', modelName: this.name }))
|
|
562
|
+
let modelFields = this._recurseAndFindModels('', this.fields, data).concat(parent)
|
|
563
|
+
|
|
564
|
+
// Loop found model/deep-model data objects, and populate missing default-fields and call afterFind on each
|
|
565
|
+
for (let item of modelFields) {
|
|
566
|
+
// Populate missing default fields if data !== null
|
|
567
|
+
// NOTE: maybe only call functions if default is being set.. fine for now
|
|
568
|
+
if (item.dataRef) {
|
|
569
|
+
for (const localSchemaFieldPath in models[item.modelName].fieldsFlattened) {
|
|
570
|
+
const schema = models[item.modelName].fieldsFlattened[localSchemaFieldPath]
|
|
571
|
+
if (!util.isDefined(schema.default) || localSchemaFieldPath.match(/^\.?(createdAt|updatedAt)$/)) continue
|
|
572
|
+
|
|
573
|
+
// const parentPath = item.dataFieldName ? item.dataFieldName + '.' : ''
|
|
574
|
+
const fullPathWithoutArrays = [item.dataPath, localSchemaFieldPath]
|
|
575
|
+
.filter(o => o)
|
|
576
|
+
.join('.')
|
|
577
|
+
.replace(/\.[0-9]+(\.|$)/g, '$1')
|
|
578
|
+
|
|
579
|
+
// Ignore default fields that are blacklisted
|
|
580
|
+
if (this._pathBlacklisted(fullPathWithoutArrays, projection)) continue
|
|
581
|
+
|
|
582
|
+
// Set default value
|
|
583
|
+
const value = util.isFunction(schema.default) ? schema.default(this.manager) : schema.default
|
|
584
|
+
util.setDeepValue(item.dataRef, localSchemaFieldPath.replace(/\.0(\.|$)/g, '.$$$1'), value, true, false, true)
|
|
588
585
|
}
|
|
589
586
|
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
587
|
+
// Collect all of the model's afterFind hooks
|
|
588
|
+
for (let fn of models[item.modelName].afterFind) {
|
|
589
|
+
callbackSeries.push(fn.bind(afterFindContext, item.dataRef))
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return util.runSeries(callbackSeries).then(() => data)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
Model.prototype._recurseAndFindModels = function (parentPath, schemaFields, dataArr) {
|
|
596
|
+
/**
|
|
597
|
+
* Returns a flattened list of models fields
|
|
598
|
+
* @param {string} parentPath
|
|
599
|
+
* @param {object} schemaFields - schema fields object
|
|
600
|
+
* @param {object|array} dataArr
|
|
601
|
+
* @return [{
|
|
602
|
+
* dataRef: { *fields here* },
|
|
603
|
+
* dataPath: 'usersNewCompany',
|
|
604
|
+
* dataFieldName: usersNewCompany,
|
|
605
|
+
* modelName: company
|
|
606
|
+
* },..]
|
|
607
|
+
*/
|
|
608
|
+
let out = []
|
|
609
|
+
for (let data of util.toArray(dataArr)) {
|
|
610
|
+
util.forEach(schemaFields, (field, fieldName) => {
|
|
611
|
+
if (!data || !data[fieldName]) return
|
|
612
|
+
const newParentPath = parentPath ? `${parentPath}.${fieldName}` : fieldName
|
|
613
|
+
// console.log(11, newParentPath, fieldName, field)
|
|
614
|
+
|
|
615
|
+
// Recurse through sub-document fields
|
|
616
|
+
if (util.isObjectAndNotID(data[fieldName]) && util.isSubdocument(field)) {
|
|
617
|
+
out = [...out, ...this._recurseAndFindModels(newParentPath, field, data[fieldName])]
|
|
618
|
+
|
|
619
|
+
// Recurse through array of sub-documents
|
|
620
|
+
} else if (util.isArray(data[fieldName]) && util.isSubdocument((field||[])[0])) {
|
|
621
|
+
for (let i=0, l=data[fieldName].length; i<l; i++) {
|
|
622
|
+
out = [...out, ...this._recurseAndFindModels(newParentPath + '.' + i, field[0], data[fieldName][i])]
|
|
623
|
+
}
|
|
622
624
|
|
|
623
|
-
|
|
624
|
-
|
|
625
|
+
// Single data model (schema field can be either a single or array of models, due to custom $lookup's)
|
|
626
|
+
} else if (util.isObjectAndNotID(data[fieldName]) && (field.model || (field[0]||{}).model)) {
|
|
627
|
+
out.push({
|
|
628
|
+
dataRef: data[fieldName],
|
|
629
|
+
dataPath: newParentPath,
|
|
630
|
+
dataFieldName: fieldName,
|
|
631
|
+
modelName: field.model || field[0].model,
|
|
632
|
+
})
|
|
633
|
+
|
|
634
|
+
// Array of data models (schema field can be either a single or array of models, due to custom $lookup's)
|
|
635
|
+
} else if (util.isObjectAndNotID(data[fieldName][0]) && (field.model || (field[0]||{}).model)) {
|
|
636
|
+
for (let i=0, l=data[fieldName].length; i<l; i++) {
|
|
625
637
|
out.push({
|
|
626
|
-
dataRef: data[fieldName],
|
|
627
|
-
dataPath: newParentPath,
|
|
638
|
+
dataRef: data[fieldName][i],
|
|
639
|
+
dataPath: newParentPath + '.' + i,
|
|
628
640
|
dataFieldName: fieldName,
|
|
629
|
-
modelName: field.model || field[0].model
|
|
641
|
+
modelName: field.model || field[0].model,
|
|
630
642
|
})
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
} else if (util.isObjectAndNotID(data[fieldName][0]) && (field.model || (field[0]||{}).model)) {
|
|
634
|
-
for (let i=0, l=data[fieldName].length; i<l; i++) {
|
|
635
|
-
out.push({
|
|
636
|
-
dataRef: data[fieldName][i],
|
|
637
|
-
dataPath: newParentPath + '.' + i,
|
|
638
|
-
dataFieldName: fieldName,
|
|
639
|
-
modelName: field.model || field[0].model
|
|
640
|
-
})
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
}, this)
|
|
644
|
-
}
|
|
645
|
-
return out
|
|
646
|
-
},
|
|
647
|
-
|
|
648
|
-
_recurseFields: function(fields, path, cb) {
|
|
649
|
-
util.forEach(fields, function(field, fieldName) {
|
|
650
|
-
if (fieldName == 'schema') {
|
|
651
|
-
return
|
|
652
|
-
} else if (util.isArray(field)) {
|
|
653
|
-
this._recurseFields(field, path + fieldName + '.', cb)
|
|
654
|
-
} else if (util.isSubdocument(field)) {
|
|
655
|
-
this._recurseFields(field, path + fieldName + '.', cb)
|
|
656
|
-
} else {
|
|
657
|
-
cb(path + fieldName, field)
|
|
658
|
-
}
|
|
643
|
+
}
|
|
644
|
+
}
|
|
659
645
|
}, this)
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
_queryOptions: [
|
|
663
|
-
// todo: remove type properties
|
|
664
|
-
'blacklist', 'data', 'find', 'findOneAndUpdate', 'insert', 'model', 'one', 'populate', 'project',
|
|
665
|
-
'projectionValidate', 'query', 'remove', 'req', 'respond', 'skipValidation', 'type', 'update',
|
|
666
|
-
'validateUndefined',
|
|
667
|
-
],
|
|
668
|
-
|
|
646
|
+
}
|
|
647
|
+
return out
|
|
669
648
|
}
|
|
649
|
+
|
|
650
|
+
Model.prototype._queryOptions = [
|
|
651
|
+
// todo: remove type properties
|
|
652
|
+
'blacklist', 'data', 'find', 'findOneAndUpdate', 'insert', 'model', '_one', 'populate', 'project',
|
|
653
|
+
'projectionValidate', 'query', 'remove', 'req', 'respond', 'skipValidation', 'type', 'update',
|
|
654
|
+
'validateUndefined',
|
|
655
|
+
]
|