monastery 2.2.3 → 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/lib/model-crud.js CHANGED
@@ -1,669 +1,655 @@
1
- let util = require('./util.js')
2
-
3
- module.exports = {
4
-
5
- insert: async function(opts, cb) {
6
- /**
7
- * Inserts document(s) with monk after validating data & before hooks.
8
- * @param {object} opts
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
12
- * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
13
- * @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
14
- * @param {boolean} <opts.timestamps> - whether `createdAt` and `updatedAt` are automatically inserted
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>
18
- * @param {function} <cb> - execute cb(err, data) instead of responding
19
- * @return promise
20
- * @this model
21
- */
22
- if (cb && !util.isFunction(cb)) {
23
- throw new Error(`The callback passed to ${this.name}.insert() is not a function`)
24
- }
25
- try {
26
- opts = await this._queryObject(opts, 'insert')
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
- // Has text search?
74
- // if (opts.query.$text) {
75
- // opts.projection.score = { $meta: 'textScore' }
76
- // opts.sort = { score: { $meta: 'textScore' }}
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
- // Wanting to populate?
80
- if (!opts.populate) {
81
- var response = await this[`_find${opts.one? 'One' : ''}`](opts.query, util.omit(opts, this._queryOptions))
82
- } else {
83
- loop: for (let item of opts.populate) {
84
- let path = util.isObject(item)? item.as : item
85
- // Blacklisted?
86
- if (this._pathBlacklisted(path, opts.projection)) continue loop
87
- // Custom $lookup definition
88
- // https://thecodebarbarian.com/a-nodejs-perspective-on-mongodb-36-lookup-expr
89
- if (util.isObject(item)) {
90
- lookups.push({ $lookup: item })
91
- } else {
92
- let arrayTarget
93
- let arrayCount = 0
94
- let schema = path.split('.').reduce((o, i) => {
95
- if (util.isArray(o[i])) {
96
- arrayCount++
97
- arrayTarget = true
98
- return o[i][0]
99
- } else {
100
- arrayTarget = false
101
- return o[i]
102
- }
103
- }, this.fields)
104
- let modelName = (schema||{}).model
105
- if (!modelName) {
106
- this.error(
107
- `The field "${path}" passed to populate is not of type model. You would ` +
108
- 'need to add the field option e.g. { model: \'comment\' } in your schema.'
109
- )
110
- continue
111
- } else if (arrayCount > 1) {
112
- this.error(
113
- `You cannot populate on array's nested in array's: ${path}: ` +
114
- `{ model: "${modelName}" }`
115
- )
116
- continue
117
- } else if (arrayCount == 1 && !arrayTarget) {
118
- this.error(
119
- `You cannot populate within an array of sub-documents: ${path}: ` +
120
- `{ model: "${modelName}" }`
121
- )
122
- continue
123
- } else if (!this.manager.model[modelName]) {
124
- this.error(
125
- `The field's model defined in your schema does not exist: ${path}: ` +
126
- `{ model: "${modelName}" }`
127
- )
128
- continue
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
- // Create lookup
135
- lookups.push({ $lookup: {
136
- from: modelName,
137
- localField: path,
138
- foreignField: '_id',
139
- as: path
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
- // Returning one?
159
- if (opts.one && util.isArray(response)) response = response[0]
172
+ // Returning one?
173
+ if (opts._one && util.isArray(response)) response = response[0] || null
160
174
 
161
- // Process afterFind hooks
162
- response = await this._processAfterFind(response, opts.projection, opts)
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 (cb) cb(null, response)
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 (err) {
170
- if (cb) cb(err)
171
- else if (opts && opts.req && opts.respond) opts.req.res.error(err)
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
- findOne: async function(opts, cb) {
177
- return this.find(opts, cb, true)
178
- },
179
-
180
- findOneAndUpdate: async function(opts, cb) {
181
- /**
182
- * Find and update document(s) with monk, also auto populates
183
- * @param {object} opts
184
- * @param {array|string|false} <opts.blacklist> - augment findBL/updateBL, `false` will remove all blacklisting
185
- * @param {array} <opts.populate> - find population, see docs
186
- * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
187
- * @param {object} <opts.query> - mongodb query object
188
- * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
189
- * @param {any} <any mongodb option>
190
- *
191
- * Update options:
192
- * @param {object|array} opts.data - mongodb document update object(s)
193
- * @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
194
- * @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
195
- * @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
196
- * default, but false on update
197
- * @param {function} <cb> - execute cb(err, data) instead of responding
198
- * @return promise
199
- * @this model
200
- */
201
-
202
- if (opts.populate) {
203
- try {
204
- // todo: add transaction flag
205
- delete opts.multi
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
- update: async function(opts, cb, type='update') {
226
- /**
227
- * Updates document(s) with monk after validating data & before hooks.
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
- // Hook: beforeUpdate (has access to original, non-validated opts.data)
265
- await util.runSeries(this.beforeUpdate.map(f => f.bind(opts, data||{})))
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
- if (data && operators['$set']) {
268
- this.info(`'$set' fields take precedence over the data fields for \`${this.name}.${type}()\``)
269
- }
270
- if (data || operators['$set']) {
271
- operators['$set'] = { ...data, ...(operators['$set'] || {}) }
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
- // findOneAndUpdate, get 'find' projection
275
- if (type == 'findOneAndUpdate') {
276
- if (opts.project) opts.projection = this._getProjectionFromProject(opts.project)
277
- else opts.projection = this._getProjectionFromBlacklist('find', opts.blacklist)
278
- // Just peform a normal update if we need to populate a findOneAndUpdate
279
- if (opts.populate) type = 'update'
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
- // Update
283
- let update = await this['_' + type](opts.query, operators, util.omit(opts, this._queryOptions))
284
- if (type == 'findOneAndUpdate') response = update
285
- else if (update.n) response = Object.assign(
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
- (update.upserted||[])[0] ? { _id: update.upserted[0]._id } : {},
295
+ update.upsertedId ? { _id: update.upsertedId } : {}
289
296
  )
297
+ }
290
298
 
291
- // Hook: afterUpdate (doesn't have access to validated data)
292
- if (response) await util.runSeries(this.afterUpdate.map(f => f.bind(opts, response)))
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
- // Hook: afterFind if findOneAndUpdate
295
- if (response && type == 'findOneAndUpdate') {
296
- response = await this._processAfterFind(response, opts.projection, opts)
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
- // Remove
330
- await util.runSeries(this.beforeRemove.map(f => f.bind(opts)))
331
- let response = await this._remove(opts.query, util.omit(opts, this._queryOptions))
332
- await util.runSeries(this.afterRemove.map(f => f.bind(response)))
333
-
334
- // Success
335
- if (cb) cb(null, response)
336
- else if (opts.req && opts.respond) opts.req.res.json(response)
337
- else return Promise.resolve(response)
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
- } catch (err) {
340
- if (cb) cb(err)
341
- else if (opts && opts.req && opts.respond) opts.req.res.error(err)
342
- else throw err
343
- }
344
- },
345
-
346
- _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+/)
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
- // Concat deep blacklists
374
- if (type == 'find') {
375
- util.forEach(this.fieldsFlattened, (schema, path) => {
376
- if (!schema.model) return
377
- let deepBL = manager.model[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
- })
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
- // Concat model, and custom blacklists
386
- list = list.concat([...this[`${type}BL`]]).concat(customBlacklist || [])
424
+ // Remove whitelist projections
425
+ for (let key in projection) {
426
+ if (projection[key]) delete projection[key]
427
+ }
387
428
 
388
- // Loop blacklists
389
- for (let _key of list) {
390
- let key = _key.replace(/^-/, '')
391
- let whitelisted = _key.match(/^-/)
429
+ return util.isEmpty(projection) ? undefined : projection
430
+ }
392
431
 
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
- }
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
- // 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
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
- // Remove whitelist projections
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
- return util.isEmpty(projection) ? undefined : projection
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 (util.isArray(customProject)) {
446
- projection = customProject.reduce((o, v) => {
447
- o[v.replace(/^-/, '')] = v.match(/^-/)? 0 : 1
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
- return projection
452
- },
453
-
454
- _queryObject: async function(opts, type, one) {
455
- /**
456
- * Normalise 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 || '' }}
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
- // Data
502
- if (!opts) opts = {}
503
- if (!util.isDefined(opts.data) && util.isDefined((opts.req||{}).body)) opts.data = opts.req.body
504
- if (util.isDefined(opts.data)) opts.data = await util.parseData(opts.data)
505
-
506
- opts.type = type
507
- opts[type] = true // still being included in the operation options..
508
- opts.model = this
509
- return opts
510
- },
511
-
512
- _pathBlacklisted: function(path, projection, matchDeepWhitelistedKeys=true) {
513
- /**
514
- * Checks if the path is blacklisted within a inclusion/exclusion projection
515
- * @param {string} path - path without array brackets e.g. '.[]'
516
- * @param {object} projection - inclusion/exclusion projection, not mixed
517
- * @param {boolean} matchDeepWhitelistedKeys - match deep whitelisted keys containing path
518
- * E.g. pets.color == pets.color.age
519
- * @return {boolean}
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
- return inclusion? true : false
540
- },
541
-
542
- _processAfterFind: function(data, projection={}, afterFindContext={}) {
543
- /**
544
- * Todo: Maybe make this method public?
545
- * Recurses through fields that are models and populates missing default-fields and calls model.afterFind([fn,..])
546
- * Be sure to add any virtual fields to the schema that your populating on,
547
- * e.g. "nurses": [{ model: 'user' }]
548
- *
549
- * @param {object|array|null} data
550
- * @param {object} projection - opts.projection (== opts.blacklist is merged with all found deep model blacklists)
551
- * @param {object} afterFindContext - handy context object given to schema.afterFind
552
- * @return Promise(data)
553
- * @this model
554
- */
555
- // Recurse down from the parent model, ending with the parent model as the parent afterFind hook may
556
- // want to manipulate any populated models
557
- let callbackSeries = []
558
- let model = this.manager.model
559
- let parent = util.toArray(data).map(o => ({ dataRef: o, dataPath: '', dataFieldName: '', modelName: this.name }))
560
- let modelFields = this._recurseAndFindModels('', this.fields, data).concat(parent)
561
-
562
- // Loop found model/deep-model data objects, and populate missing default-fields and call afterFind on each
563
- for (let item of modelFields) {
564
- // Populate missing default fields if data !== null
565
- // NOTE: maybe only call functions if default is being set.. fine for now
566
- if (item.dataRef) {
567
- for (const localSchemaFieldPath in model[item.modelName].fieldsFlattened) {
568
- const schema = model[item.modelName].fieldsFlattened[localSchemaFieldPath]
569
- if (!util.isDefined(schema.default) || localSchemaFieldPath.match(/^\.?(createdAt|updatedAt)$/)) continue
570
-
571
- // const parentPath = item.dataFieldName ? item.dataFieldName + '.' : ''
572
- const fullPathWithoutArrays = [item.dataPath, localSchemaFieldPath]
573
- .filter(o => o)
574
- .join('.')
575
- .replace(/\.[0-9]+(\.|$)/g, '$1')
576
-
577
- // Ignore default fields that are blacklisted
578
- if (this._pathBlacklisted(fullPathWithoutArrays, projection)) continue
579
-
580
- // Set default value
581
- const value = util.isFunction(schema.default) ? schema.default(this.manager) : schema.default
582
- util.setDeepValue(item.dataRef, localSchemaFieldPath.replace(/\.0(\.|$)/g, '.$$$1'), value, true, false, true)
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
- return util.runSeries(callbackSeries).then(() => data)
591
- },
592
-
593
- _recurseAndFindModels: function(parentPath, schemaFields, dataArr) {
594
- /**
595
- * Returns a flattened list of models fields
596
- * @param {string} parentPath
597
- * @param {object} schemaFields - schema fields object
598
- * @param {object|array} dataArr
599
- * @return [{
600
- * dataRef: { *fields here* },
601
- * dataPath: 'usersNewCompany',
602
- * dataFieldName: usersNewCompany,
603
- * modelName: company
604
- * },..]
605
- */
606
- let out = []
607
- for (let data of util.toArray(dataArr)) {
608
- util.forEach(schemaFields, (field, fieldName) => {
609
- if (!data || !data[fieldName]) return
610
- const newParentPath = parentPath ? `${parentPath}.${fieldName}` : fieldName
611
- // console.log(11, newParentPath, fieldName, field)
612
-
613
- // Recurse through sub-document fields
614
- if (util.isObjectAndNotID(data[fieldName]) && util.isSubdocument(field)) {
615
- out = [...out, ...this._recurseAndFindModels(newParentPath, field, data[fieldName])]
616
-
617
- // Recurse through array of sub-documents
618
- } else if (util.isArray(data[fieldName]) && util.isSubdocument((field||[])[0])) {
619
- for (let i=0, l=data[fieldName].length; i<l; i++) {
620
- out = [...out, ...this._recurseAndFindModels(newParentPath + '.' + i, field[0], data[fieldName][i])]
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
- // Single data model (schema field can be either a single or array of models, due to custom $lookup's)
624
- } else if (util.isObjectAndNotID(data[fieldName]) && (field.model || (field[0]||{}).model)) {
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
- // Array of data models (schema field can be either a single or array of models, due to custom $lookup's)
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
+ ]