monastery 1.28.4 → 1.30.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.eslintrc.json CHANGED
@@ -1,12 +1,38 @@
1
1
  {
2
+ "env": {
3
+ "browser": true,
4
+ "es2021": true,
5
+ "node": true
6
+ },
7
+ "extends": [
8
+ "eslint:recommended"
9
+ ],
10
+ "globals": {
11
+ "test": true,
12
+ "expect": true
13
+ },
2
14
  "parserOptions": {
3
- "ecmaVersion": 2018,
4
- "sourceType": "module",
5
15
  "ecmaFeatures": {
6
16
  "jsx": true
7
- }
17
+ },
18
+ "ecmaVersion": "latest",
19
+ "sourceType": "module"
8
20
  },
21
+ "plugins": [],
9
22
  "rules": {
10
- //"quotes": ["error", "double"]
23
+ "brace-style": ["error", "1tbs", { "allowSingleLine": true }],
24
+ "max-len": ["error", { "code": 120, "ignorePattern": "^\\s*<(rect|path|line)\\s" }],
25
+ "no-prototype-builtins": "off",
26
+ "no-unused-vars": ["error", { "args": "none" }],
27
+ "object-shorthand": ["error", "consistent"],
28
+ // "no-restricted-syntax": [
29
+ // "error",
30
+ // {
31
+ // "selector": "ObjectPattern > Property[shorthand=false]",
32
+ // "message": "Renaming properties within object deconstructions is not allowed."
33
+ // }
34
+ // ],
35
+ "quotes": ["error", "single"],
36
+ "semi": ["error", "never"]
11
37
  }
12
38
  }
package/docs/errors.md CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/docs/readme.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ![](./assets/imgs/monastery.jpg)
2
2
 
3
- [![NPM](https://img.shields.io/npm/v/monastery.svg)](https://www.npmjs.com/package/monastery) [![Build Status](https://travis-ci.com/boycce/monastery.svg?branch=master)](https://travis-ci.com/boycce/monastery)
3
+ [![NPM](https://img.shields.io/npm/v/monastery.svg)](https://www.npmjs.com/package/monastery) [![Build Status](https://travis-ci.com/boycce/monastery.svg?branch=master)](https://app.travis-ci.com/github/boycce/monastery)
4
4
 
5
5
  ## Features
6
6
 
package/docs/rules.md CHANGED
File without changes
package/lib/index.js CHANGED
@@ -1,5 +1,4 @@
1
1
  let util = require('./util')
2
- let imagePlugin = require('./util')
3
2
  let monk = require('monk')
4
3
  let debug = require('debug')
5
4
 
package/lib/model-crud.js CHANGED
@@ -9,7 +9,8 @@ module.exports = {
9
9
  * @param {object|array} <opts.data> - documents to insert
10
10
  * @param {array|string|false} <opts.blacklist> - augment schema.insertBL, `false` will remove all blacklisting
11
11
  * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
12
- * @param {array|string|true} ignoreUndefined - ignore all required fields during insert, or undefined subdocument required fields that have a defined parent/grandparent during update
12
+ * @param {array|string|false} validateUndefined - ignore all required fields during insert, or
13
+ * undefined subdocument required fields that have a defined parent/grandparent during update
13
14
  * @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
14
15
  * @param {boolean} <opts.timestamps> - whether `createdAt` and `updatedAt` are automatically inserted
15
16
  * @param {any} <opts.any> - any mongodb option
@@ -21,7 +22,9 @@ module.exports = {
21
22
  opts.insert = true
22
23
  opts.model = this
23
24
  let data = opts.data = opts.data || (opts.req? opts.req.body : {})
24
- let options = util.omit(opts, ['data', 'insert', 'model', 'respond', 'ignoreUndefined', 'skipValidation', 'blacklist'])
25
+ let options = util.omit(opts, [
26
+ 'data', 'insert', 'model', 'respond', 'validateUndefined', 'skipValidation', 'blacklist'
27
+ ])
25
28
  if (cb && !util.isFunction(cb)) {
26
29
  throw new Error(`The callback passed to ${this.name}.insert() is not a function`)
27
30
  }
@@ -75,7 +78,7 @@ module.exports = {
75
78
  opts.one = one || opts.one
76
79
  // Operation options
77
80
  options = util.omit(opts, ['blacklist', 'one', 'populate', 'project', 'query', 'respond'])
78
- options.sort = options.sort || { "createdAt": -1 }
81
+ options.sort = options.sort || { 'createdAt': -1 }
79
82
  options.skip = Math.max(0, options.skip || 0)
80
83
  options.limit = opts.one? 1 : parseInt(options.limit || this.manager.limit || 0)
81
84
  options.addFields = options.addFields || {}
@@ -99,8 +102,8 @@ module.exports = {
99
102
  }
100
103
  // Has text search?
101
104
  // if (opts.query.$text) {
102
- // options.projection.score = { $meta: "textScore" }
103
- // options.sort = { score: { $meta: "textScore" }}
105
+ // options.projection.score = { $meta: 'textScore' }
106
+ // options.sort = { score: { $meta: 'textScore' }}
104
107
  // }
105
108
  // Sort string passed
106
109
  if (util.isString(options.sort)) {
@@ -130,7 +133,7 @@ module.exports = {
130
133
  continue
131
134
  }
132
135
  // Populate model (convert array into document & create lookup)
133
- options.addFields[path] = { "$arrayElemAt": [ "$" + path, 0 ] }
136
+ options.addFields[path] = { '$arrayElemAt': [ '$' + path, 0 ] }
134
137
  lookups.push({ $lookup: {
135
138
  from: modelName,
136
139
  localField: path,
@@ -191,7 +194,8 @@ module.exports = {
191
194
  * @param {object} <opts.query> - mongodb query object
192
195
  * @param {object|array} <opts.data> - mongodb document update object(s)
193
196
  * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
194
- * @param {array|string|true} ignoreUndefined - ignore all required fields during insert, or undefined subdocument required fields that have a defined parent/grandparent during update
197
+ * @param {array|string|false} validateUndefined - ignore all required fields during insert, or
198
+ * undefined subdocument required fields that have a defined parent/grandparent during update
195
199
  * @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
196
200
  * @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
197
201
  * @param {array|string|false} <opts.blacklist> - augment schema.updateBL, `false` will remove all blacklisting
@@ -211,8 +215,8 @@ module.exports = {
211
215
  data = opts.data = opts.data || (opts.req? opts.req.body : null)
212
216
  operators = util.pluck(opts, [/^\$/])
213
217
  // Operation options
214
- options = util.omit(opts, ['data', 'query', 'respond', 'ignoreUndefined', 'skipValidation', 'blacklist'])
215
- options.sort = options.sort || { "createdAt": -1 }
218
+ options = util.omit(opts, ['data', 'query', 'respond', 'validateUndefined', 'skipValidation', 'blacklist'])
219
+ options.sort = options.sort || { 'createdAt': -1 }
216
220
  options.limit = parseInt(options.limit || 0)
217
221
  // Sort string passed
218
222
  if (util.isString(options.sort)) {
@@ -283,7 +287,7 @@ module.exports = {
283
287
  if (util.isEmpty(opts.query)) throw new Error('Please specify opts.query')
284
288
  // Operation options
285
289
  options = util.omit(opts, ['query', 'respond'])
286
- options.sort = options.sort || { "createdAt": -1 }
290
+ options.sort = options.sort || { 'createdAt': -1 }
287
291
  options.limit = parseInt(options.limit || 1)
288
292
  // Sort string passed
289
293
  if (util.isString(options.sort)) {
@@ -397,7 +401,7 @@ module.exports = {
397
401
  let paths = (populate||[]).map(o => o && o.as? o.as : o)
398
402
 
399
403
  if (!paths.length) return blacklistProjection
400
- this._recurseFields(model.fields, "", function(path, field) {
404
+ this._recurseFields(model.fields, '', function(path, field) {
401
405
  // Remove array indexes from the path e.g. '0.'
402
406
  path = path.replace(/(\.[0-9]+)(\.|$)/, '$2')
403
407
  if (!field.model || !paths.includes(path)) return
@@ -420,7 +424,8 @@ module.exports = {
420
424
  /**
421
425
  * Merge blacklist in
422
426
  * @param {object} blacklistProjection
423
- * @param {array} paths - e.g. ['password', '-email'] - email will be whitelisted / removed from exlcusion projection
427
+ * @param {array} paths - e.g. ['password', '-email'] - email will be whitelisted / removed from
428
+ * exlcusion projection
424
429
  * @return {object} exclusion blacklist
425
430
  * @this model
426
431
  */
@@ -461,7 +466,7 @@ module.exports = {
461
466
  let findWL = [ '_id', ...this.findWL ]
462
467
  let model = this.manager.model
463
468
 
464
- this._recurseFields(this.fields, "", function(path, field) {
469
+ this._recurseFields(this.fields, '', function(path, field) {
465
470
  // Remove array indexes from the path e.g. '0.'
466
471
  path = path.replace(/(\.[0-9]+)(\.|$)/, '$2')
467
472
  //if (field.type == 'any') findWL.push(path) //exclude.push(path)
@@ -529,7 +534,7 @@ module.exports = {
529
534
  }
530
535
 
531
536
  for (let doc of util.toArray(data)) {
532
- recurseAndDeleteData(doc, "")
537
+ recurseAndDeleteData(doc, '')
533
538
  }
534
539
  return data
535
540
  },
@@ -586,7 +591,11 @@ module.exports = {
586
591
  if (!data) return
587
592
 
588
593
  // Valid model object field.
589
- if (((util.isArray(field) && field[0].model) || field.model) && data[fieldName] && (util.isObjectAndNotID(data[fieldName]))) {
594
+ if (
595
+ ((util.isArray(field) && field[0].model) || field.model) &&
596
+ data[fieldName] &&
597
+ util.isObjectAndNotID(data[fieldName])
598
+ ) {
590
599
  // Note that sometimes a single model is passed instead of an array of models via a custom populate $lookup
591
600
  out.push({
592
601
  dataRef: data[fieldName],
@@ -595,11 +604,18 @@ module.exports = {
595
604
  })
596
605
 
597
606
  // Recurse through fields that are sub-documents
598
- } else if (util.isSubdocument(field) && util.isObjectAndNotID(data[fieldName])) {
607
+ } else if (
608
+ util.isSubdocument(field) &&
609
+ util.isObjectAndNotID(data[fieldName])
610
+ ) {
599
611
  out = [...out, ...this._recurseAndFindModels(field, data[fieldName])]
600
612
 
601
613
  // Array of sub-documents or models
602
- } else if ((util.isArray(field) || field.model) && data[fieldName] && util.isObjectAndNotID(data[fieldName][0])) {
614
+ } else if (
615
+ (util.isArray(field) || field.model) &&
616
+ data[fieldName] &&
617
+ util.isObjectAndNotID(data[fieldName][0])
618
+ ) {
603
619
  // Valid model object found in array
604
620
  // Note that sometimes an array of models are passed instead of single object via a custom populate $lookup
605
621
  if (field.model || field[0].model) {
@@ -12,9 +12,11 @@ module.exports = {
12
12
  * @param {boolean(false)} update - are we validating for insert or update?
13
13
  * @param {array|string|false} blacklist - augment schema blacklist, `false` will remove all blacklisting
14
14
  * @param {array|string} projection - only return these fields, ignores blacklist
15
- * @param {array|string|true} ignoreUndefined - ignore all required fields during insert, or undefined subdocument required fields that have a defined parent/grandparent during update
15
+ * @param {array|string|false} validateUndefined - ignore all required fields during insert, or undefined
16
+ * subdocument required fields that have a defined parent/grandparent during update
16
17
  * @param {array|string|true} skipValidation - skip validation on these fields
17
- * @param {boolean} timestamps - whether `createdAt` and `updatedAt` are inserted, or `updatedAt` is updated, depending on the `options.update` value
18
+ * @param {boolean} timestamps - whether `createdAt` and `updatedAt` are inserted, or `updatedAt` is
19
+ * updated, depending on the `options.update` value
18
20
  * @param {function} <cb> - instead of returning a promise
19
21
  * @this model
20
22
 
@@ -56,7 +58,7 @@ module.exports = {
56
58
  else continue
57
59
  if (whitelist.includes(split.join())) {
58
60
  blacklist.splice(i, 1)
59
- break;
61
+ break
60
62
  }
61
63
  }
62
64
  }
@@ -175,7 +177,11 @@ module.exports = {
175
177
  // Object schema errors
176
178
  errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path2)))
177
179
  // Recurse if data value is a subdocument, or when inserting, or when updating deep properties (non-root)
178
- if (util.isObject(value) || opts.insert || ((path2||'').match(/\./) && !opts.ignoreUndefined)) {
180
+ if (
181
+ util.isObject(value) ||
182
+ opts.insert ||
183
+ ((path2||'').match(/\./) && (util.isDefined(opts.validateUndefined) ? opts.validateUndefined : true))
184
+ ) {
179
185
  var res = this._validateFields(dataRoot, field, value, opts, path2)
180
186
  errors.push(...res[0])
181
187
  }
@@ -189,11 +195,11 @@ module.exports = {
189
195
  errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path2)))
190
196
  // Data value is array too
191
197
  if (util.isArray(value)) {
192
- var res = this._validateFields(dataRoot, field, value, opts, path2)
193
- errors.push(...res[0])
198
+ var res2 = this._validateFields(dataRoot, field, value, opts, path2)
199
+ errors.push(...res2[0])
194
200
  }
195
201
  if (util.isDefined(value) && !verrors.length) {
196
- data2[indexOrFieldName] = res? res[1] : value
202
+ data2[indexOrFieldName] = res2? res2[1] : value
197
203
  }
198
204
  }
199
205
  }, this)
@@ -211,6 +217,7 @@ module.exports = {
211
217
  * @param {object} dataRoot - data
212
218
  * @param {object} field - field schema
213
219
  * @param {string} path - full field path
220
+ * @param {object} opts - original validate() options
214
221
  * @this model
215
222
  * @return {array} errors
216
223
  */
@@ -234,44 +241,38 @@ module.exports = {
234
241
  for (let i=0, l=skippedFieldChunks.length; i<l; i++) {
235
242
  if (skippedFieldChunks[i] == '$') skippedFieldChunks[i] = '[0-9]+'
236
243
  }
237
- if (path.match(new RegExp('^' + skippedFieldChunks.join('.') + '(\.|$)'))) return []
244
+ if (path.match(new RegExp('^' + skippedFieldChunks.join('.') + '(.|$)'))) return []
238
245
  }
239
246
  }
240
247
 
241
248
  for (let ruleName in field) {
242
249
  if (this._ignoredRules.indexOf(ruleName) > -1) continue
243
- // ignore undefined updated root properties, or
244
- if (util.isUndefined(value) && ((opts.update && !path.match(/\./)) || opts.ignoreUndefined)) continue
245
- let error = this._validateRule(dataRoot, ruleName, field, field[ruleName], value, path)
250
+ let error = this._validateRule(dataRoot, ruleName, field, field[ruleName], value, opts, path)
246
251
  if (error && ruleName == 'required') return [error] // only show the required error
247
252
  if (error) errors.push(error)
248
253
  }
249
254
  return errors
250
255
  },
251
256
 
252
- _validateRule: function(dataRoot, ruleName, field, ruleArg, value, path) {
253
- //this.debug(path, field, ruleName, ruleArg, value)
257
+ _validateRule: function(dataRoot, ruleName, field, ruleArg, value, opts, path) {
258
+ // this.debug(path, field, ruleName, ruleArg, value)
254
259
  // Remove [] from the message path, and simply ignore non-numeric children to test for all array items
255
260
  ruleArg = ruleArg === true? undefined : ruleArg
256
261
  let rule = this.rules[ruleName] || rules[ruleName]
257
- let fieldName = path.match(/[^\.]+$/)[0]
262
+ let fieldName = path.match(/[^.]+$/)[0]
258
263
  let ruleMessageKey = this._getMostSpecificKeyMatchingPath(this.messages, path)
259
264
  let ruleMessage = ruleMessageKey && this.messages[ruleMessageKey][ruleName]
265
+ let validateUndefined = util.isDefined(opts.validateUndefined) ? opts.validateUndefined : rule.validateUndefined
260
266
  if (!ruleMessage) ruleMessage = rule.message
261
267
 
262
- if (ruleName !== 'required') {
263
- // Ignore undefined when not testing 'required'
264
- if (typeof value === 'undefined') return
268
+ // Ignore undefined (if updated root property, or ignoring)
269
+ if ((!validateUndefined || (opts.update && !path.match(/\./))) && typeof value === 'undefined') return
265
270
 
266
- // Ignore null if not testing required
267
- if (value === null && !field.isObject && !field.isArray) return
268
-
269
- // Ignore null if nullObject is set on objects or arrays
270
- if (value === null && field.nullObject) return
271
- }
271
+ // Ignore null (if nullObject is set on objects or arrays) (todo: change to ignoreNull)
272
+ if (field.nullObject && (field.isObject || field.isArray) && value === null) return
272
273
 
273
274
  // Ignore empty strings
274
- if (value === '' && rule.ignoreEmptyString) return
275
+ if (!rule.validateEmptyString && value === '') return
275
276
 
276
277
  // Rule failed
277
278
  if (!rule.fn.call(dataRoot, value, ruleArg, path, this)) return {
package/lib/model.js CHANGED
@@ -61,6 +61,7 @@ let Model = module.exports = function(name, opts, manager) {
61
61
  // Update with formatted rule
62
62
  let formattedRule = util.isObject(rule)? rule : { fn: rule }
63
63
  if (!formattedRule.message) formattedRule.message = `Invalid data property for rule "${ruleName}".`
64
+ if (typeof formattedRule.validateEmptyString == 'undefined') formattedRule.validateEmptyString = true
64
65
  this.rules[ruleName] = formattedRule
65
66
  }
66
67
  }, this)
@@ -203,7 +204,12 @@ Model.prototype._setupFields = function(fields) {
203
204
  let nullObject = this.manager.nullObjects
204
205
  let virtual = field.length == 1 && (field[0]||{}).virtual ? true : undefined
205
206
  field.schema = {
206
- type: 'array', isArray: true, default: arrayDefault, nullObject: nullObject, virtual: virtual, ...(field.schema || {})
207
+ type: 'array',
208
+ isArray: true,
209
+ default: arrayDefault,
210
+ nullObject: nullObject,
211
+ virtual: virtual,
212
+ ...(field.schema || {})
207
213
  }
208
214
  this._setupFields(field)
209
215
 
@@ -236,11 +242,10 @@ Model.prototype._setupFieldsAndWhitelists = function(fields, path) {
236
242
  this.findBLProject = this.findBL.reduce((o, v) => { (o[v] = 0); return o }, {})
237
243
  },
238
244
 
239
- Model.prototype._setupIndexes = function(fields) {
245
+ Model.prototype._setupIndexes = function(fields, opts={}) {
240
246
  /**
241
- * Creates indexes for the model
247
+ * Creates indexes for the model (multikey, and sub-document supported)
242
248
  * Note: only one text index per model(collection) is allowed due to mongodb limitations
243
- * Note: we and currently don't support indexes on sub-collections, but sub-documents yes!
244
249
  * @link https://docs.mongodb.com/manual/reference/command/createIndexes/
245
250
  * @link https://mongodb.github.io/node-mongodb-native/2.1/api/Collection.html#createIndexes
246
251
  * @param {object} <fields>
@@ -262,7 +267,10 @@ Model.prototype._setupIndexes = function(fields) {
262
267
 
263
268
  // No db defined
264
269
  if (!(model.manager._state || '').match(/^open/)) {
265
- let error = { type: 'info', detail: `Skipping createIndex on the '${model.name}' model, no mongodb connection found.` }
270
+ let error = {
271
+ type: 'info',
272
+ detail: `Skipping createIndex on the '${model.name}' model, no mongodb connection found.`
273
+ }
266
274
  return Promise.reject(error)
267
275
  }
268
276
 
@@ -270,6 +278,7 @@ Model.prototype._setupIndexes = function(fields) {
270
278
  recurseFields(fields || model.fields, '')
271
279
  // console.log(2, indexes, fields)
272
280
  if (hasTextIndex) indexes.push(textIndex)
281
+ if (opts.dryRun) return Promise.resolve(indexes || [])
273
282
  if (!indexes.length) return Promise.resolve([]) // No indexes defined
274
283
 
275
284
  // Create indexes
@@ -322,22 +331,24 @@ Model.prototype._setupIndexes = function(fields) {
322
331
  util.forEach(fields, (field, name) => {
323
332
  let index = field.index
324
333
  if (index) {
325
- let path = name == 'schema'? parentPath.slice(0, -1) : parentPath + name
326
334
  let options = util.isObject(index)? util.omit(index, ['type']) : {}
327
335
  let type = util.isObject(index)? index.type : index
336
+ let path = name == 'schema'? parentPath.slice(0, -1) : parentPath + name
337
+ let path2 = path.replace(/(^|\.)[0-9]+(\.|$)/g, '$2') // no numirical keys, e.g. pets.1.name
328
338
  if (type === true) type = 1
329
-
330
339
  if (type == 'text') {
331
- hasTextIndex = textIndex.key[path] = 'text'
340
+ hasTextIndex = textIndex.key[path2] = 'text'
332
341
  Object.assign(textIndex, options)
333
342
  } else if (type == '1' || type == '-1' || type == '2dsphere') {
334
- indexes.push({ name: `${path}_${type}`, key: { [path]: type }, ...options })
343
+ indexes.push({ name: `${path2}_${type}`, key: { [path2]: type }, ...options })
335
344
  } else if (type == 'unique') {
336
- indexes.push({ name: `${path}_1`, key: { [path]: 1 }, unique: true, ...options })
345
+ indexes.push({ name: `${path2}_1`, key: { [path2]: 1 }, unique: true, ...options })
337
346
  }
338
347
  }
339
348
  if (util.isSubdocument(field)) {
340
349
  recurseFields(field, parentPath + name + '.')
350
+ } else if (util.isArray(field)) {
351
+ recurseFields(field, parentPath + name + '.')
341
352
  }
342
353
  })
343
354
  }
@@ -361,10 +372,10 @@ Model.prototype._timestampFields = {
361
372
  }
362
373
  }
363
374
 
364
- for (var key in crud) {
375
+ for (let key in crud) {
365
376
  Model.prototype[key] = crud[key]
366
377
  }
367
378
 
368
- for (var key in validate) {
379
+ for (let key in validate) {
369
380
  Model.prototype[key] = validate[key]
370
381
  }