monastery 3.0.22 → 3.0.23
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/changelog.md +2 -0
- package/lib/model-crud.js +39 -36
- package/lib/model-validate.js +90 -65
- package/lib/model.js +90 -50
- package/lib/util.js +12 -2
- package/package.json +1 -1
- package/plugins/images/index.js +9 -9
- package/test/blacklisting.js +2 -1
- package/test/crud.js +62 -0
- package/test/manager.js +1 -1
- package/test/model.js +125 -47
- package/test/plugin-images.js +38 -16
- package/test/virtuals.js +2 -2
package/lib/model.js
CHANGED
|
@@ -109,6 +109,7 @@ function Model(name, opts, manager) {
|
|
|
109
109
|
// Extend default fields with passed in fields and check for invalid fields
|
|
110
110
|
this._setupFields(this.fields = Object.assign({}, this._defaultFields, this.fields))
|
|
111
111
|
this.fieldsFlattened = this._getFieldsFlattened(this.fields, '') // test output?
|
|
112
|
+
this.modelFieldsArray = this._getModelFieldsArray()
|
|
112
113
|
|
|
113
114
|
// Get collection, and extend model with collection methods
|
|
114
115
|
this.collection = this.manager.get(name, { castIds: false })
|
|
@@ -138,27 +139,40 @@ function Model(name, opts, manager) {
|
|
|
138
139
|
|
|
139
140
|
Model.prototype._getFieldsFlattened = function(fields, path) {
|
|
140
141
|
/**
|
|
141
|
-
*
|
|
142
|
+
* Get flattened fields
|
|
142
143
|
* @param {object|array} fields - can be a nested subdocument or array
|
|
143
144
|
* @param {string} path
|
|
144
145
|
* @return {object} e.g. {'name': Schema, 'pets.dog': Schema}
|
|
145
146
|
*/
|
|
146
147
|
let obj = {}
|
|
147
148
|
util.forEach(fields, function(field, fieldName) {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
const schema = field.schema
|
|
150
|
+
const newPath = /*util.isArray(fields)? path : */path + fieldName + '.'
|
|
151
|
+
if (fieldName == 'schema') return
|
|
152
|
+
if (schema.isArray) {
|
|
152
153
|
Object.assign(obj, this._getFieldsFlattened(field, newPath))
|
|
153
|
-
} else if (
|
|
154
|
+
} else if (schema.isObject) {
|
|
154
155
|
Object.assign(obj, this._getFieldsFlattened(field, newPath))
|
|
155
156
|
} else {
|
|
156
|
-
obj[newPath.replace(/\.$/, '')] =
|
|
157
|
+
obj[newPath.replace(/\.$/, '')] = schema
|
|
157
158
|
}
|
|
158
159
|
}, this)
|
|
159
160
|
return obj
|
|
160
161
|
}
|
|
161
162
|
|
|
163
|
+
Model.prototype._getModelFieldsArray = function() {
|
|
164
|
+
/**
|
|
165
|
+
* Get all the model fields in an array
|
|
166
|
+
* @return {array} e.g. [{ path: 'pets.0.dog', 'path2': 'pets.dog'}, ...]
|
|
167
|
+
*/
|
|
168
|
+
return Object.keys(this.fieldsFlattened).reduce((acc, path) => {
|
|
169
|
+
if (this.fieldsFlattened[path].model) {
|
|
170
|
+
acc.push({ path: path, path2: path.replace(/\.[0-9]+(\.|$)/g, '$1') })
|
|
171
|
+
}
|
|
172
|
+
return acc
|
|
173
|
+
}, [])
|
|
174
|
+
},
|
|
175
|
+
|
|
162
176
|
Model.prototype._setupFields = function(fields) {
|
|
163
177
|
/**
|
|
164
178
|
* Check for invalid rules on a field object, and set field.isType
|
|
@@ -166,76 +180,88 @@ Model.prototype._setupFields = function(fields) {
|
|
|
166
180
|
*/
|
|
167
181
|
util.forEach(fields, function(field, fieldName) {
|
|
168
182
|
// Schema field
|
|
183
|
+
if (fieldName == 'schema') return
|
|
169
184
|
if (util.isSchema(field)) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
185
|
+
const schema = field
|
|
186
|
+
fields[fieldName] = field = { schema }
|
|
187
|
+
|
|
188
|
+
// Type 'model'
|
|
189
|
+
if (schema.model) {
|
|
190
|
+
schema.type = 'id'
|
|
174
191
|
|
|
175
|
-
//
|
|
176
|
-
if (!
|
|
192
|
+
// Type 'image', but no image plugin schema processing done, e.g. image plugin not setup
|
|
193
|
+
} else if (schema.type == 'image' && !schema.image) {
|
|
194
|
+
schema.image = true
|
|
195
|
+
schema.type = 'any'
|
|
196
|
+
|
|
197
|
+
// No type
|
|
198
|
+
} else if (!schema.type) {
|
|
177
199
|
this.error(`No type defined on "${this.name}" for field "${fieldName}". Defaulting to string.`)
|
|
178
|
-
|
|
200
|
+
schema.type = 'string'
|
|
201
|
+
}
|
|
179
202
|
|
|
180
|
-
// Type
|
|
181
|
-
|
|
182
|
-
|
|
203
|
+
// Type isn't a rule
|
|
204
|
+
const isType = schema.isType = 'is' + util.ucFirst(schema.type)
|
|
205
|
+
if (!this.rules[isType] && !rules[isType]) {
|
|
206
|
+
this.error(`Not a valid type "${schema.type}" defined on "${this.name}" for field "${fieldName}".
|
|
183
207
|
Defaulting to string.`)
|
|
184
|
-
|
|
208
|
+
schema.type = 'string'
|
|
185
209
|
}
|
|
186
210
|
|
|
187
|
-
|
|
188
|
-
|
|
211
|
+
field.schema = {
|
|
212
|
+
...field.schema,
|
|
213
|
+
[isType]: true, // e.g. isString rule
|
|
214
|
+
isSchema: true,
|
|
215
|
+
}
|
|
189
216
|
|
|
190
217
|
// Rule doesn't exist
|
|
191
|
-
|
|
218
|
+
for (let ruleName in field.schema) {
|
|
192
219
|
if ((this.rules[ruleName] || rules[ruleName]) && this._ignoredRules.indexOf(ruleName) != -1) {
|
|
193
220
|
this.error(`The rule name "${ruleName}" for the model "${this.name}" is a reserved keyword, ignoring rule.`)
|
|
194
221
|
}
|
|
195
222
|
if (!this.rules[ruleName] && !rules[ruleName] && this._ignoredRules.indexOf(ruleName) == -1) {
|
|
196
|
-
// console.log(field)
|
|
223
|
+
// console.log(field.schema)
|
|
197
224
|
this.error(`No rule "${ruleName}" exists for model "${this.name}", ignoring rule.`)
|
|
198
|
-
delete field[ruleName]
|
|
225
|
+
delete field.schema[ruleName]
|
|
199
226
|
}
|
|
200
|
-
}
|
|
227
|
+
}
|
|
201
228
|
|
|
202
229
|
// Misused schema property
|
|
203
|
-
} else if (fieldName == 'schema') {
|
|
204
|
-
this.error(`Invalid
|
|
230
|
+
} else if (fieldName == 'schema' || fieldName == 'isSchema') {
|
|
231
|
+
this.error(`Invalid field '${fieldName}' on model '${this.name}', this is a reserved property, ignoring field.`)
|
|
205
232
|
delete fields[fieldName]
|
|
206
233
|
|
|
207
234
|
// Fields be an array
|
|
208
235
|
} else if (util.isArray(field)) {
|
|
209
|
-
|
|
210
|
-
let nullObject = this.manager.opts.nullObjects
|
|
211
|
-
let virtual = field.length == 1 && (field[0]||{}).virtual ? true : undefined
|
|
212
|
-
field.schema = {
|
|
236
|
+
field.schema = util.removeUndefined({
|
|
213
237
|
type: 'array',
|
|
214
238
|
isArray: true,
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
239
|
+
isSchema: true,
|
|
240
|
+
isType: 'isArray',
|
|
241
|
+
default: this.manager.opts.defaultObjects? () => [] : undefined,
|
|
242
|
+
nullObject: this.manager.opts.nullObjects,
|
|
243
|
+
virtual: field.length == 1 && (field[0] || {}).virtual ? true : undefined,
|
|
218
244
|
...(field.schema || {}),
|
|
219
|
-
}
|
|
245
|
+
})
|
|
220
246
|
this._setupFields(field)
|
|
221
247
|
|
|
222
248
|
// Fields can be a subdocument, e.g. user.pet = { name: {}, ..}
|
|
223
249
|
} else if (util.isSubdocument(field)) {
|
|
224
|
-
let objectDefault = this.manager.opts.defaultObjects? () => ({}) : undefined
|
|
225
|
-
let nullObject = this.manager.opts.nullObjects
|
|
226
250
|
let index2dsphere = util.isSubdocument2dsphere(field)
|
|
227
251
|
field.schema = field.schema || {}
|
|
228
252
|
if (index2dsphere) {
|
|
229
253
|
field.schema.index = index2dsphere
|
|
230
254
|
delete field.index
|
|
231
255
|
}
|
|
232
|
-
field.schema = {
|
|
256
|
+
field.schema = util.removeUndefined({
|
|
233
257
|
type: 'object',
|
|
234
258
|
isObject: true,
|
|
235
|
-
|
|
236
|
-
|
|
259
|
+
isSchema: true,
|
|
260
|
+
isType: 'isObject',
|
|
261
|
+
default: this.manager.opts.defaultObjects? () => ({}) : undefined,
|
|
262
|
+
nullObject: this.manager.opts.nullObjects,
|
|
237
263
|
...field.schema,
|
|
238
|
-
}
|
|
264
|
+
})
|
|
239
265
|
this._setupFields(field)
|
|
240
266
|
}
|
|
241
267
|
}, this)
|
|
@@ -246,7 +272,7 @@ Model.prototype._setupIndexes = async function(fields, opts={}) {
|
|
|
246
272
|
* Creates indexes for the model (multikey, and sub-document supported)
|
|
247
273
|
* Note: the collection be created beforehand???
|
|
248
274
|
* Note: only one text index per model(collection) is allowed due to mongodb limitations
|
|
249
|
-
* @param {object} <fields>
|
|
275
|
+
* @param {object} <fields> - processed or unprocessed fields, e.g. {schema: {name: {index}}} or {name: {index}}
|
|
250
276
|
* @return Promise( {array} indexes ensured ) || error
|
|
251
277
|
*
|
|
252
278
|
* MongoDB index structures = [
|
|
@@ -267,8 +293,16 @@ Model.prototype._setupIndexes = async function(fields, opts={}) {
|
|
|
267
293
|
throw new Error(`Skipping createIndex on the '${model.name||''}' model, no mongodb connection found.`)
|
|
268
294
|
}
|
|
269
295
|
|
|
296
|
+
// Process custom 'unprocessed' fields
|
|
297
|
+
if (fields && !fields[Object.keys(fields)[0]].schema) {
|
|
298
|
+
fields = util.deepCopy(fields)
|
|
299
|
+
this._setupFields(fields)
|
|
300
|
+
// console.dir(fields, { depth: null })
|
|
301
|
+
}
|
|
302
|
+
|
|
270
303
|
// Find all indexes
|
|
271
304
|
recurseFields(fields || model.fields, '')
|
|
305
|
+
|
|
272
306
|
// console.log(2, indexes, fields)
|
|
273
307
|
if (hasTextIndex) indexes.push(textIndex)
|
|
274
308
|
if (opts.dryRun) return indexes || []
|
|
@@ -313,14 +347,19 @@ Model.prototype._setupIndexes = async function(fields, opts={}) {
|
|
|
313
347
|
model.info('db index(s) created for ' + model.name)
|
|
314
348
|
return indexes
|
|
315
349
|
|
|
316
|
-
function recurseFields(
|
|
317
|
-
|
|
318
|
-
|
|
350
|
+
function recurseFields(schemaFields, parentPath) {
|
|
351
|
+
/**
|
|
352
|
+
* Recursively find fields with an index property
|
|
353
|
+
* @param {object} schemaFields
|
|
354
|
+
*/
|
|
355
|
+
util.forEach(schemaFields, (field, fieldName) => {
|
|
356
|
+
const index = (field.schema||{}).index
|
|
357
|
+
if (fieldName == 'schema') return
|
|
319
358
|
if (index) {
|
|
320
359
|
let options = util.isObject(index)? util.omit(index, ['type']) : {}
|
|
321
360
|
let type = util.isObject(index)? index.type : index
|
|
322
|
-
let path =
|
|
323
|
-
let path2 = path.replace(/(^|\.)[0-9]+(\.|$)/g, '$2') // no numirical keys, e.g. pets.1.
|
|
361
|
+
let path = parentPath + fieldName
|
|
362
|
+
let path2 = path.replace(/(^|\.)[0-9]+(\.|$)/g, '$2') // no numirical keys, e.g. pets.1.fieldName
|
|
324
363
|
if (type === true) type = 1
|
|
325
364
|
if (type == 'text') {
|
|
326
365
|
hasTextIndex = textIndex.key[path2] = 'text'
|
|
@@ -331,10 +370,11 @@ Model.prototype._setupIndexes = async function(fields, opts={}) {
|
|
|
331
370
|
indexes.push({ name: `${path2}_1`, key: { [path2]: 1 }, unique: true, ...options })
|
|
332
371
|
}
|
|
333
372
|
}
|
|
334
|
-
if (
|
|
335
|
-
recurseFields(field, parentPath +
|
|
336
|
-
|
|
337
|
-
|
|
373
|
+
if (field.schema.isObject) {
|
|
374
|
+
recurseFields(field, parentPath + fieldName + '.')
|
|
375
|
+
|
|
376
|
+
} else if (field.schema.isArray) {
|
|
377
|
+
recurseFields(field, parentPath + fieldName + '.')
|
|
338
378
|
}
|
|
339
379
|
})
|
|
340
380
|
}
|
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
|
-
|
|
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.
|
|
5
|
+
"version": "3.0.23",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": "github:boycce/monastery",
|
|
8
8
|
"homepage": "https://boycce.github.io/monastery/",
|
package/plugins/images/index.js
CHANGED
|
@@ -482,27 +482,27 @@ let plugin = module.exports = {
|
|
|
482
482
|
})).then(() => validFiles)
|
|
483
483
|
},
|
|
484
484
|
|
|
485
|
-
_findAndTransformImageFields: function(
|
|
485
|
+
_findAndTransformImageFields: function(unprocessedFields, path) {
|
|
486
486
|
/**
|
|
487
|
-
* Returns a list of valid image
|
|
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(
|
|
495
|
+
util.forEach(unprocessedFields, (field, fieldName) => {
|
|
496
496
|
let path2 = `${path}.${fieldName}`.replace(/^\./, '')
|
|
497
|
-
|
|
497
|
+
if (fieldName == 'schema') return
|
|
498
498
|
|
|
499
499
|
// Subdocument field
|
|
500
|
-
if (util.isSubdocument(field)) {
|
|
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)) {
|
|
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
|
-
|
|
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
|
})
|
package/test/blacklisting.js
CHANGED
|
@@ -328,7 +328,8 @@ test('find project population', async () => {
|
|
|
328
328
|
expect(find3).toEqual(customProject)
|
|
329
329
|
})
|
|
330
330
|
|
|
331
|
-
test('insert blacklisting
|
|
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 })).
|
|
11
|
+
expect(await manager.command({ ping: 1 })).toMatchObject({ ok: 1 }) // cluster connections return extra fields
|
|
12
12
|
manager.close()
|
|
13
13
|
})
|
|
14
14
|
|