monastery 3.0.22 → 3.1.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.js CHANGED
@@ -21,6 +21,11 @@ function Model(name, opts, manager) {
21
21
  } else if (!opts.fields) {
22
22
  throw `We couldn't find ${name}.fields in the model definition, the model maybe setup `
23
23
  + `or exported incorrectly:\n${JSON.stringify(opts, null, 2)}`
24
+ } else if (!util.isSubdocument(opts.fields) && opts.fields.type == 'any') {
25
+ throw `Instead of using { type: 'any' } for ${name}.fields, please use the new 'strict' definition rule` +
26
+ ', e.g. { schema: { strict: false }}'
27
+ } else if (!util.isSubdocument(opts.fields) && !util.isEmpty(opts.fields)) {
28
+ throw `The ${name}.fields object should be a valid document, e.g. { name: { type: 'string' }}`
24
29
  }
25
30
 
26
31
  // Add schema options
@@ -107,8 +112,11 @@ function Model(name, opts, manager) {
107
112
  }, this)
108
113
 
109
114
  // Extend default fields with passed in fields and check for invalid fields
110
- this._setupFields(this.fields = Object.assign({}, this._defaultFields, this.fields))
115
+ this._setupFields(
116
+ this.fields = util.isSchema(this.fields) ? this.fields : Object.assign({}, this._defaultFields, this.fields)
117
+ )
111
118
  this.fieldsFlattened = this._getFieldsFlattened(this.fields, '') // test output?
119
+ this.modelFieldsArray = this._getModelFieldsArray()
112
120
 
113
121
  // Get collection, and extend model with collection methods
114
122
  this.collection = this.manager.get(name, { castIds: false })
@@ -138,115 +146,158 @@ function Model(name, opts, manager) {
138
146
 
139
147
  Model.prototype._getFieldsFlattened = function(fields, path) {
140
148
  /**
141
- * Flatten fields
149
+ * Get flattened fields
142
150
  * @param {object|array} fields - can be a nested subdocument or array
143
151
  * @param {string} path
144
152
  * @return {object} e.g. {'name': Schema, 'pets.dog': Schema}
145
153
  */
146
154
  let obj = {}
147
155
  util.forEach(fields, function(field, fieldName) {
148
- let newPath = /*util.isArray(fields)? path : */path + fieldName + '.'
149
- if (fieldName == 'schema') {
150
- return
151
- } else if (util.isArray(field)) {
156
+ const schema = field.schema
157
+ const newPath = /*util.isArray(fields)? path : */path + fieldName + '.'
158
+ if (fieldName == 'schema') return
159
+ if (schema.isArray) {
152
160
  Object.assign(obj, this._getFieldsFlattened(field, newPath))
153
- } else if (util.isSubdocument(field)) {
161
+ } else if (schema.isObject) {
154
162
  Object.assign(obj, this._getFieldsFlattened(field, newPath))
155
163
  } else {
156
- obj[newPath.replace(/\.$/, '')] = field
164
+ obj[newPath.replace(/\.$/, '')] = schema
157
165
  }
158
166
  }, this)
159
167
  return obj
160
168
  }
161
169
 
162
- Model.prototype._setupFields = function(fields) {
170
+ Model.prototype._getModelFieldsArray = function() {
171
+ /**
172
+ * Get all the model fields in an array
173
+ * @return {array} e.g. [{ path: 'pets.0.dog', 'path2': 'pets.dog'}, ...]
174
+ */
175
+ return Object.keys(this.fieldsFlattened).reduce((acc, path) => {
176
+ if (this.fieldsFlattened[path].model) {
177
+ acc.push({ path: path, path2: path.replace(/\.[0-9]+(\.|$)/g, '$1') })
178
+ }
179
+ return acc
180
+ }, [])
181
+ },
182
+
183
+ Model.prototype._setupFields = function(fields, isSub) {
163
184
  /**
164
185
  * Check for invalid rules on a field object, and set field.isType
165
186
  * @param {object|array} fields - subsdocument or array
166
187
  */
188
+ // We need to allow the processing of the root schema object
189
+ if (!isSub) fields = { fields }
190
+
167
191
  util.forEach(fields, function(field, fieldName) {
168
192
  // Schema field
193
+ if (fieldName == 'schema') return
169
194
  if (util.isSchema(field)) {
170
- // No image schema pre-processing done yet by a plugin
171
- if (field.type == 'image' && !field.image) field.image = true, field.type = 'any'
172
- if (field.model) field.type = 'id'
173
- let isType = 'is' + util.ucFirst(field.type)
195
+ const schema = field
196
+ fields[fieldName] = field = { schema }
174
197
 
175
- // No type defined
176
- if (!field.type) {
198
+ // Type 'model'
199
+ if (schema.model) {
200
+ schema.type = 'id'
201
+
202
+ // Type 'image', but no image plugin schema processing done, e.g. image plugin not setup
203
+ } else if (schema.type == 'image' && !schema.image) {
204
+ schema.image = true
205
+ schema.type = 'any'
206
+
207
+ // No type
208
+ } else if (!schema.type) {
177
209
  this.error(`No type defined on "${this.name}" for field "${fieldName}". Defaulting to string.`)
178
- field.type = 'string'
210
+ schema.type = 'string'
211
+ }
179
212
 
180
- // Type doesn't exist
181
- } else if (!this.rules[isType] && !rules[isType]) {
182
- this.error(`Not a valid type "${field.type}" defined on "${this.name}" for field "${fieldName}".
213
+ // Type isn't a rule
214
+ const isType = schema.isType = 'is' + util.ucFirst(schema.type)
215
+ if (!this.rules[isType] && !rules[isType]) {
216
+ this.error(`Not a valid type "${schema.type}" defined on "${this.name}" for field "${fieldName}".
183
217
  Defaulting to string.`)
184
- field.type = 'string'
218
+ schema.type = 'string'
185
219
  }
186
220
 
187
- // Convert type into a is{type} rule
188
- field[isType] = true
221
+ field.schema = {
222
+ ...field.schema,
223
+ [isType]: true, // e.g. isString rule
224
+ isSchema: true,
225
+ }
189
226
 
190
- // Rule doesn't exist
191
- util.forEach(field, (rule, ruleName) => {
192
- if ((this.rules[ruleName] || rules[ruleName]) && this._ignoredRules.indexOf(ruleName) != -1) {
193
- this.error(`The rule name "${ruleName}" for the model "${this.name}" is a reserved keyword, ignoring rule.`)
194
- }
195
- if (!this.rules[ruleName] && !rules[ruleName] && this._ignoredRules.indexOf(ruleName) == -1) {
196
- // console.log(field)
197
- this.error(`No rule "${ruleName}" exists for model "${this.name}", ignoring rule.`)
198
- delete field[ruleName]
199
- }
200
- }, this)
227
+ // Remove invalid rules
228
+ this._removeInvalidRules(field)
201
229
 
202
230
  // Misused schema property
203
- } else if (fieldName == 'schema') {
204
- this.error(`Invalid schema on model "${this.name}", remember 'schema' is a reserverd property, ignoring field.`)
231
+ } else if (fieldName == 'schema' || fieldName == 'isSchema') {
232
+ this.error(`Invalid field '${fieldName}' on model '${this.name}', this is a reserved property, ignoring field.`)
205
233
  delete fields[fieldName]
206
234
 
207
235
  // Fields be an array
208
236
  } else if (util.isArray(field)) {
209
- let arrayDefault = this.manager.opts.defaultObjects? () => [] : undefined
210
- let nullObject = this.manager.opts.nullObjects
211
- let virtual = field.length == 1 && (field[0]||{}).virtual ? true : undefined
212
- field.schema = {
237
+ this._removeInvalidRules(field)
238
+ field.schema = util.removeUndefined({
213
239
  type: 'array',
214
240
  isArray: true,
215
- default: arrayDefault,
216
- nullObject: nullObject,
217
- virtual: virtual,
241
+ isSchema: true,
242
+ isType: 'isArray',
243
+ default: this.manager.opts.defaultObjects? () => [] : undefined,
244
+ nullObject: this.manager.opts.nullObjects,
245
+ virtual: field.length == 1 && (field[0] || {}).virtual ? true : undefined,
218
246
  ...(field.schema || {}),
219
- }
220
- this._setupFields(field)
247
+ })
248
+ this._setupFields(field, true)
221
249
 
222
250
  // Fields can be a subdocument, e.g. user.pet = { name: {}, ..}
223
251
  } else if (util.isSubdocument(field)) {
224
- let objectDefault = this.manager.opts.defaultObjects? () => ({}) : undefined
225
- let nullObject = this.manager.opts.nullObjects
226
252
  let index2dsphere = util.isSubdocument2dsphere(field)
227
253
  field.schema = field.schema || {}
228
254
  if (index2dsphere) {
229
255
  field.schema.index = index2dsphere
230
256
  delete field.index
231
257
  }
232
- field.schema = {
258
+ this._removeInvalidRules(field)
259
+ field.schema = util.removeUndefined({
233
260
  type: 'object',
234
261
  isObject: true,
235
- default: objectDefault,
236
- nullObject: nullObject,
262
+ isSchema: true,
263
+ isType: 'isObject',
264
+ default: this.manager.opts.defaultObjects? () => ({}) : undefined,
265
+ nullObject: this.manager.opts.nullObjects,
237
266
  ...field.schema,
238
- }
239
- this._setupFields(field)
267
+ })
268
+ this._setupFields(field, true)
240
269
  }
241
270
  }, this)
242
271
  },
243
272
 
273
+ Model.prototype._removeInvalidRules = function(field) {
274
+ /**
275
+ * Remove invalid rules on a field object
276
+ * @param {object} field
277
+ * @return {object} field
278
+ **/
279
+ for (let ruleName in (field||{}).schema) {
280
+ const ruleFn = this.rules[ruleName] || rules[ruleName]
281
+ // Rule doesn't exist
282
+ if (!ruleFn && this._ignoredRules.indexOf(ruleName) == -1) {
283
+ // console.log(field.schema)
284
+ this.error(`No rule "${ruleName}" exists for model "${this.name}", ignoring rule.`)
285
+ delete field.schema[ruleName]
286
+ }
287
+ // Reserved rule
288
+ if (this.rules[ruleName] && this._ignoredRules.indexOf(ruleName) != -1) {
289
+ this.error(`The rule "${ruleName}" for the model "${this.name}" is a reserved keyword, ignoring custom rule function.`)
290
+ }
291
+ }
292
+ return field
293
+ },
294
+
244
295
  Model.prototype._setupIndexes = async function(fields, opts={}) {
245
296
  /**
246
297
  * Creates indexes for the model (multikey, and sub-document supported)
247
298
  * Note: the collection be created beforehand???
248
299
  * Note: only one text index per model(collection) is allowed due to mongodb limitations
249
- * @param {object} <fields>
300
+ * @param {object} <fields> - processed or unprocessed fields, e.g. {schema: {name: {index}}} or {name: {index}}
250
301
  * @return Promise( {array} indexes ensured ) || error
251
302
  *
252
303
  * MongoDB index structures = [
@@ -267,8 +318,16 @@ Model.prototype._setupIndexes = async function(fields, opts={}) {
267
318
  throw new Error(`Skipping createIndex on the '${model.name||''}' model, no mongodb connection found.`)
268
319
  }
269
320
 
321
+ // Process custom 'unprocessed' fields
322
+ if (fields && !(fields[Object.keys(fields)[0]].schema||{}).isSchema) {
323
+ fields = util.deepCopy(fields)
324
+ this._setupFields(fields)
325
+ // console.dir(fields, { depth: null })
326
+ }
327
+
270
328
  // Find all indexes
271
329
  recurseFields(fields || model.fields, '')
330
+
272
331
  // console.log(2, indexes, fields)
273
332
  if (hasTextIndex) indexes.push(textIndex)
274
333
  if (opts.dryRun) return indexes || []
@@ -313,14 +372,19 @@ Model.prototype._setupIndexes = async function(fields, opts={}) {
313
372
  model.info('db index(s) created for ' + model.name)
314
373
  return indexes
315
374
 
316
- function recurseFields(fields, parentPath) {
317
- util.forEach(fields, (field, name) => {
318
- let index = field.index
375
+ function recurseFields(schemaFields, parentPath) {
376
+ /**
377
+ * Recursively find fields with an index property
378
+ * @param {object} schemaFields
379
+ */
380
+ util.forEach(schemaFields, (field, fieldName) => {
381
+ const index = (field.schema||{}).index
382
+ if (fieldName == 'schema') return
319
383
  if (index) {
320
384
  let options = util.isObject(index)? util.omit(index, ['type']) : {}
321
385
  let type = util.isObject(index)? index.type : index
322
- let path = name == 'schema'? parentPath.slice(0, -1) : parentPath + name
323
- let path2 = path.replace(/(^|\.)[0-9]+(\.|$)/g, '$2') // no numirical keys, e.g. pets.1.name
386
+ let path = parentPath + fieldName
387
+ let path2 = path.replace(/(^|\.)[0-9]+(\.|$)/g, '$2') // no numirical keys, e.g. pets.1.fieldName
324
388
  if (type === true) type = 1
325
389
  if (type == 'text') {
326
390
  hasTextIndex = textIndex.key[path2] = 'text'
@@ -331,10 +395,11 @@ Model.prototype._setupIndexes = async function(fields, opts={}) {
331
395
  indexes.push({ name: `${path2}_1`, key: { [path2]: 1 }, unique: true, ...options })
332
396
  }
333
397
  }
334
- if (util.isSubdocument(field)) {
335
- recurseFields(field, parentPath + name + '.')
336
- } else if (util.isArray(field)) {
337
- recurseFields(field, parentPath + name + '.')
398
+ if (field.schema.isObject) {
399
+ recurseFields(field, parentPath + fieldName + '.')
400
+
401
+ } else if (field.schema.isArray) {
402
+ recurseFields(field, parentPath + fieldName + '.')
338
403
  }
339
404
  })
340
405
  }
package/lib/util.js CHANGED
@@ -35,8 +35,7 @@ module.exports = {
35
35
  let obj2 = Array.isArray(obj)? [] : {}
36
36
  for (let key in obj) {
37
37
  let v = obj[key]
38
- if (this.isId(v)) obj2[key] = v//.toString()
39
- else obj2[key] = (typeof v === 'object')? this.deepCopy(v) : v
38
+ obj2[key] = (typeof v === 'object' && !this.isIdFast(v))? this.deepCopy(v) : v
40
39
  }
41
40
  return obj2
42
41
  },
@@ -114,6 +113,17 @@ module.exports = {
114
113
  else return false
115
114
  },
116
115
 
116
+ isIdFast: function(value) {
117
+ // Check quickly if the value is an ObjectId. We can use db.isId() but this may be slower
118
+ // console.log(isId('66333b1b3343d7e3b200005b')) = true
119
+ // console.log(isId(db.id())) = true
120
+ // console.log(isId(null)) = undefined
121
+ // console.log(isId('qwefqwefqwef')) = undefined
122
+ // console.log(isId({})) = undefined
123
+ // console.log(isId(['66333b1b3343d7e3b200005b'])) = undefined
124
+ if ((value||'').toString()?.match(/^[0-9a-fA-F]{24}$/) && !this.isArray(value)) return true
125
+ },
126
+
117
127
  isNumber: function(value) {
118
128
  return !isNaN(parseFloat(value)) && isFinite(value)
119
129
  },
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "monastery",
3
3
  "description": "⛪ A simple, straightforward MongoDB ODM",
4
4
  "author": "Ricky Boyce",
5
- "version": "3.0.22",
5
+ "version": "3.1.0",
6
6
  "license": "MIT",
7
7
  "repository": "github:boycce/monastery",
8
8
  "homepage": "https://boycce.github.io/monastery/",
@@ -482,27 +482,27 @@ let plugin = module.exports = {
482
482
  })).then(() => validFiles)
483
483
  },
484
484
 
485
- _findAndTransformImageFields: function(fields, path) {
485
+ _findAndTransformImageFields: function(unprocessedFields, path) {
486
486
  /**
487
- * Returns a list of valid image fields
488
- * @param {object|array} fields
487
+ * Returns a list of valid image field schemas
488
+ * @param {object|array} unprocessedFields - fields not yet setup
489
489
  * @param {string} path
490
490
  * @return [{}, ...]
491
491
  * @this plugin
492
492
  */
493
493
  let list = []
494
494
  let that = this
495
- util.forEach(fields, (field, fieldName) => {
495
+ util.forEach(unprocessedFields, (field, fieldName) => {
496
496
  let path2 = `${path}.${fieldName}`.replace(/^\./, '')
497
- // let schema = field.schema || {}
497
+ if (fieldName == 'schema') return
498
498
 
499
499
  // Subdocument field
500
- if (util.isSubdocument(field)) {//schema.isObject
500
+ if (util.isSubdocument(field)) {
501
501
  // log(`Recurse 1: ${path2}`)
502
502
  list = list.concat(plugin._findAndTransformImageFields(field, path2))
503
503
 
504
504
  // Array field
505
- } else if (util.isArray(field)) {//schema.isArray
505
+ } else if (util.isArray(field)) {
506
506
  // log(`Recurse 2: ${path2}`)
507
507
  list = list.concat(plugin._findAndTransformImageFields(field, path2))
508
508
 
@@ -532,15 +532,15 @@ let plugin = module.exports = {
532
532
  params: field.params ? util.deepCopy(field.params) : undefined,
533
533
  })
534
534
  // Convert image field to subdocument
535
- fields[fieldName] = {
535
+ unprocessedFields[fieldName] = {
536
536
  bucket: { type: 'string' },
537
537
  date: { type: 'number' },
538
538
  filename: { type: 'string' },
539
539
  filesize: { type: 'number' },
540
540
  metadata: { type: 'any' },
541
541
  path: { type: 'string' },
542
- schema: { image: true, nullObject: true, isImageObject: true },
543
542
  uid: { type: 'string' },
543
+ schema: { image: true, isImageObject: true, nullObject: true },
544
544
  }
545
545
  }
546
546
  })
@@ -328,7 +328,8 @@ test('find project population', async () => {
328
328
  expect(find3).toEqual(customProject)
329
329
  })
330
330
 
331
- test('insert blacklisting (validate)', async () => {
331
+ test('insert blacklisting validate', async () => {
332
+ // todo: isolated model._pathBlacklisted test
332
333
  let user = db.model('user', {
333
334
  fields: {
334
335
  list: [{ type: 'number' }],
package/test/crud.js CHANGED
@@ -571,6 +571,68 @@ test('update mixing formData', async() => {
571
571
  })
572
572
  })
573
573
 
574
+ test('update large document', async () => {
575
+ // todo: sereach util.deepCopy
576
+ // todo: check castIds and any other recursive functions
577
+ // todo: move default fields to before validate
578
+ db.model('a', { fields: {} })
579
+ db.model('b', { fields: {} })
580
+ db.model('c', { fields: {} })
581
+ db.model('d', { fields: {} })
582
+ db.model('e', { fields: {} })
583
+ try {
584
+ var large = db.model('large', require('../resources/fixtures/large-definition.js'))
585
+ var largePayload = require('../resources/fixtures/large-payload.json')
586
+ } catch (e) {
587
+ // ignore publicly for now
588
+ return
589
+ }
590
+ // Insert
591
+ let inserted = await large._insert({})
592
+ // Update
593
+ // console.time('update large document')
594
+ let update = await large.update({
595
+ query: inserted._id,
596
+ data: largePayload,
597
+ })
598
+ // console.timeEnd('update large document')
599
+ // Check
600
+ await expect(update).toEqual(removePrunedProperties(largePayload))
601
+ // Find
602
+ // console.time('find large document')
603
+ // await large.findOne({
604
+ // query: inserted._id,
605
+ // })
606
+ // console.timeEnd('find large document')
607
+
608
+ function removePrunedProperties(entity) {
609
+ for (let entitiesKey of [
610
+ 'components', 'connections', 'bridges', 'openings', 'spaces', 'elements', 'elementTypes',
611
+ 'categories', 'typologies',
612
+ ]) {
613
+ if (entity[entitiesKey]) {
614
+ for (let i=0, l=entity[entitiesKey].length; i<l; i++) {
615
+ entity[entitiesKey][i] = removePrunedProperties(entity[entitiesKey][i])
616
+ }
617
+ }
618
+ }
619
+ // remove actually keys
620
+ if (entity.metrics) {
621
+ for (let key in entity.metrics) {
622
+ delete entity.metrics[key].actually
623
+ }
624
+ }
625
+ if (entity.code?.match(/^(ELEM|CAT)/)) {
626
+ delete entity.name
627
+ }
628
+ // // convert _id to ObjectId
629
+ // if (entity._id) {
630
+ // entity._id = db.id(entity._id)
631
+ // }
632
+ return entity
633
+ }
634
+ })
635
+
574
636
  test('findOneAndUpdate general', async () => {
575
637
  // todo: test all findOneAndUpdate options (e.g. array population)
576
638
  // todo: test find & update hooks
package/test/manager.js CHANGED
@@ -8,7 +8,7 @@ test('manager > basics', async () => {
8
8
  // Basic find command
9
9
  expect(await manager.db.collection('non-collection').findOne({})).toEqual(null)
10
10
  // Raw MongoDB ping command
11
- expect(await manager.command({ ping: 1 })).toEqual({ ok: 1 })
11
+ expect(await manager.command({ ping: 1 })).toMatchObject({ ok: 1 }) // cluster connections return extra fields
12
12
  manager.close()
13
13
  })
14
14