monastery 2.1.1 → 2.2.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/changelog.md +4 -0
- package/docs/readme.md +5 -1
- package/lib/model-crud.js +116 -91
- package/package.json +2 -1
- package/test/crud.js +66 -12
- package/test/populate.js +75 -0
package/changelog.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
+
### [2.2.1](https://github.com/boycce/monastery/compare/2.2.0...2.2.1) (2024-04-07)
|
|
6
|
+
|
|
7
|
+
## [2.2.0](https://github.com/boycce/monastery/compare/2.1.1...2.2.0) (2024-04-07)
|
|
8
|
+
|
|
5
9
|
### [2.1.1](https://github.com/boycce/monastery/compare/2.1.0...2.1.1) (2024-04-05)
|
|
6
10
|
|
|
7
11
|
## [2.1.0](https://github.com/boycce/monastery/compare/1.42.2...2.1.0) (2024-04-05)
|
package/docs/readme.md
CHANGED
|
@@ -89,7 +89,6 @@ Coming soon...
|
|
|
89
89
|
- ~~Add FindOneAndUpdate~~
|
|
90
90
|
- ~~Add beforeInsertUpdate / afterInsertUpdate~~
|
|
91
91
|
- Bug: Setting an object literal on an ID field ('model') saves successfully
|
|
92
|
-
- Population within array items
|
|
93
92
|
- ~~Blacklist false removes all blacklisting~~
|
|
94
93
|
- ~~Add project to insert/update/validate~~
|
|
95
94
|
- ~~Whitelisting a parent will remove any previously blacklisted children~~
|
|
@@ -97,6 +96,8 @@ Coming soon...
|
|
|
97
96
|
- Automatic embedded document ids/createdAt/updatedAt fields
|
|
98
97
|
- ~~Ability to change ACL default on the manager~~
|
|
99
98
|
- ~~Public db.arrayWithSchema method~~
|
|
99
|
+
- ~~Added support for array population~~
|
|
100
|
+
- Change population warnings into errors
|
|
100
101
|
- Global after/before hooks
|
|
101
102
|
- before hooks can receive a data array, remove this
|
|
102
103
|
- docs: Make the implicit ID query conversion more apparent
|
|
@@ -106,6 +107,9 @@ Coming soon...
|
|
|
106
107
|
- double check await db.model.remove({ query: idfromparam }) doesnt cause issues for null, undefined or '', but continue to allow {}
|
|
107
108
|
- ~~can't insert/update model id (maybe we can allow this and add _id to default insert/update blacklists)~~
|
|
108
109
|
- timstamps are blacklisted by default (instead of the `timestamps` opt), and can be switched off via blacklisting
|
|
110
|
+
- Allow rules on image types, e.g. `required`
|
|
111
|
+
- test importing of models
|
|
112
|
+
- Docs: model.methods
|
|
109
113
|
|
|
110
114
|
## Versions
|
|
111
115
|
|
package/lib/model-crud.js
CHANGED
|
@@ -89,13 +89,37 @@ module.exports = {
|
|
|
89
89
|
if (util.isObject(item)) {
|
|
90
90
|
lookups.push({ $lookup: item })
|
|
91
91
|
} else {
|
|
92
|
-
let
|
|
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
|
|
93
105
|
if (!modelName) {
|
|
94
106
|
this.error(
|
|
95
107
|
`The field "${path}" passed to populate is not of type model. You would ` +
|
|
96
108
|
'need to add the field option e.g. { model: \'comment\' } in your schema.'
|
|
97
109
|
)
|
|
98
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
|
|
99
123
|
} else if (!this.manager.model[modelName]) {
|
|
100
124
|
this.error(
|
|
101
125
|
`The field's model defined in your schema does not exist: ${path}: ` +
|
|
@@ -103,8 +127,11 @@ module.exports = {
|
|
|
103
127
|
)
|
|
104
128
|
continue
|
|
105
129
|
}
|
|
106
|
-
//
|
|
107
|
-
(
|
|
130
|
+
// Convert array into a document for non-array targets
|
|
131
|
+
if (!arrayTarget) {
|
|
132
|
+
(opts.addFields = opts.addFields || {})[path] = { '$arrayElemAt': [ '$' + path, 0 ] }
|
|
133
|
+
}
|
|
134
|
+
// Create lookup
|
|
108
135
|
lookups.push({ $lookup: {
|
|
109
136
|
from: modelName,
|
|
110
137
|
localField: path,
|
|
@@ -482,6 +509,36 @@ module.exports = {
|
|
|
482
509
|
return opts
|
|
483
510
|
},
|
|
484
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
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return inclusion? true : false
|
|
540
|
+
},
|
|
541
|
+
|
|
485
542
|
_processAfterFind: function(data, projection={}, afterFindContext={}) {
|
|
486
543
|
/**
|
|
487
544
|
* Todo: Maybe make this method public?
|
|
@@ -490,7 +547,7 @@ module.exports = {
|
|
|
490
547
|
* e.g. "nurses": [{ model: 'user' }]
|
|
491
548
|
*
|
|
492
549
|
* @param {object|array|null} data
|
|
493
|
-
* @param {object} projection -
|
|
550
|
+
* @param {object} projection - opts.projection (== opts.blacklist is merged with all found deep model blacklists)
|
|
494
551
|
* @param {object} afterFindContext - handy context object given to schema.afterFind
|
|
495
552
|
* @return Promise(data)
|
|
496
553
|
* @this model
|
|
@@ -499,25 +556,31 @@ module.exports = {
|
|
|
499
556
|
// want to manipulate any populated models
|
|
500
557
|
let callbackSeries = []
|
|
501
558
|
let model = this.manager.model
|
|
502
|
-
let
|
|
503
|
-
let
|
|
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)
|
|
504
561
|
|
|
505
562
|
// Loop found model/deep-model data objects, and populate missing default-fields and call afterFind on each
|
|
506
|
-
for (let item of
|
|
563
|
+
for (let item of modelFields) {
|
|
507
564
|
// Populate missing default fields if data !== null
|
|
508
565
|
// NOTE: maybe only call functions if default is being set.. fine for now
|
|
509
566
|
if (item.dataRef) {
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
+
|
|
514
577
|
// Ignore default fields that are blacklisted
|
|
515
|
-
if (this._pathBlacklisted(
|
|
516
|
-
|
|
517
|
-
// Set value
|
|
518
|
-
|
|
519
|
-
util.setDeepValue(item.dataRef,
|
|
520
|
-
}
|
|
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
|
+
}
|
|
521
584
|
}
|
|
522
585
|
// Collect all of the model's afterFind hooks
|
|
523
586
|
for (let fn of model[item.modelName].afterFind) {
|
|
@@ -527,94 +590,56 @@ module.exports = {
|
|
|
527
590
|
return util.runSeries(callbackSeries).then(() => data)
|
|
528
591
|
},
|
|
529
592
|
|
|
530
|
-
|
|
593
|
+
_recurseAndFindModels: function(parentPath, schemaFields, dataArr) {
|
|
531
594
|
/**
|
|
532
|
-
*
|
|
533
|
-
* @param {string}
|
|
534
|
-
* @param {object}
|
|
535
|
-
* @param {boolean} matchDeepWhitelistedKeys - match deep whitelisted keys containing path
|
|
536
|
-
* E.g. pets.color == pets.color.age
|
|
537
|
-
* @return {boolean}
|
|
538
|
-
*/
|
|
539
|
-
for (let key in projection) {
|
|
540
|
-
if (projection[key]) {
|
|
541
|
-
// Inclusion (whitelisted)
|
|
542
|
-
// E.g. pets.color.age == pets.color.age (exact match)
|
|
543
|
-
// E.g. pets.color.age == pets.color (path contains key)
|
|
544
|
-
var inclusion = true
|
|
545
|
-
if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return false
|
|
546
|
-
if (matchDeepWhitelistedKeys) {
|
|
547
|
-
// E.g. pets.color == pets.color.age (key contains path)
|
|
548
|
-
if (key.match(new RegExp('^' + path.replace(/\./g, '\\.') + '\\.'))) return false
|
|
549
|
-
}
|
|
550
|
-
} else {
|
|
551
|
-
// Exclusion (blacklisted)
|
|
552
|
-
// E.g. pets.color.age == pets.color.age (exact match)
|
|
553
|
-
// E.g. pets.color.age == pets.color (path contains key)
|
|
554
|
-
if (path.match(new RegExp('^' + key.replace(/\./g, '\\.') + '(\\.|$)'))) return true
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
return inclusion? true : false
|
|
558
|
-
},
|
|
559
|
-
|
|
560
|
-
_recurseAndFindModels: function(fields, dataArr) {
|
|
561
|
-
/**
|
|
562
|
-
* Returns a flattened list of data-objects that are models
|
|
563
|
-
* @param {object} fields
|
|
595
|
+
* Returns a flattened list of models fields
|
|
596
|
+
* @param {string} parentPath
|
|
597
|
+
* @param {object} schemaFields - schema fields object
|
|
564
598
|
* @param {object|array} dataArr
|
|
565
599
|
* @return [{
|
|
566
600
|
* dataRef: { *fields here* },
|
|
567
|
-
*
|
|
601
|
+
* dataPath: 'usersNewCompany',
|
|
602
|
+
* dataFieldName: usersNewCompany,
|
|
568
603
|
* modelName: company
|
|
569
604
|
* },..]
|
|
570
605
|
*/
|
|
571
606
|
let out = []
|
|
572
607
|
for (let data of util.toArray(dataArr)) {
|
|
573
|
-
util.forEach(
|
|
574
|
-
if (!data) return
|
|
575
|
-
|
|
576
|
-
//
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
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
|
+
}
|
|
622
|
+
|
|
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)) {
|
|
583
625
|
out.push({
|
|
584
626
|
dataRef: data[fieldName],
|
|
585
|
-
|
|
627
|
+
dataPath: newParentPath,
|
|
628
|
+
dataFieldName: fieldName,
|
|
586
629
|
modelName: field.model || field[0].model
|
|
587
630
|
})
|
|
588
|
-
|
|
589
|
-
//
|
|
590
|
-
} else if (
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
(util.isArray(field) || field.model) &&
|
|
599
|
-
data[fieldName] &&
|
|
600
|
-
util.isObjectAndNotID(data[fieldName][0])
|
|
601
|
-
) {
|
|
602
|
-
// Valid model object found in array
|
|
603
|
-
// Note that sometimes an array of models are passed instead of single object via a custom populate $lookup
|
|
604
|
-
if (field.model || field[0].model) {
|
|
605
|
-
for (let item of data[fieldName]) {
|
|
606
|
-
out.push({
|
|
607
|
-
dataRef: item,
|
|
608
|
-
fieldName: fieldName,
|
|
609
|
-
modelName: field.model || field[0].model
|
|
610
|
-
})
|
|
611
|
-
}
|
|
612
|
-
// Objects ares sub-documents, recurse through fields
|
|
613
|
-
} else if (!field.model && util.isSubdocument(field[0])) {
|
|
614
|
-
// console.log('101', fieldName, field[0])
|
|
615
|
-
out = [...out, ...this._recurseAndFindModels(field[0], data[fieldName])]
|
|
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
|
+
})
|
|
616
641
|
}
|
|
617
|
-
}
|
|
642
|
+
}
|
|
618
643
|
}, this)
|
|
619
644
|
}
|
|
620
645
|
return out
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "monastery",
|
|
3
3
|
"description": "⛪ A straight forward MongoDB ODM built around Monk",
|
|
4
4
|
"author": "Ricky Boyce",
|
|
5
|
-
"version": "2.
|
|
5
|
+
"version": "2.2.1",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": "github:boycce/monastery",
|
|
8
8
|
"homepage": "https://boycce.github.io/monastery/",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"debug": "4.3.4",
|
|
35
35
|
"file-type": "^16.5.4",
|
|
36
36
|
"monk": "7.3.4",
|
|
37
|
+
"mongodb": "^3.2.3",
|
|
37
38
|
"nanoid": "3.2.0",
|
|
38
39
|
"validator": "13.7.0"
|
|
39
40
|
},
|
package/test/crud.js
CHANGED
|
@@ -460,6 +460,7 @@ module.exports = function(monastery, opendb) {
|
|
|
460
460
|
addresses: [{ city: { type: 'string' }, country: { type: 'string', default: 'Germany' } }],
|
|
461
461
|
address: { country: { type: 'string', default: 'Germany' }},
|
|
462
462
|
pet: { dog: { model: 'dog' }},
|
|
463
|
+
pets: { dog: [{ model: 'dog' }]},
|
|
463
464
|
dogs: [{ model: 'dog' }], // virtual association
|
|
464
465
|
}
|
|
465
466
|
})
|
|
@@ -489,18 +490,20 @@ module.exports = function(monastery, opendb) {
|
|
|
489
490
|
// Note that addresses.1.country shouldn't be overridden
|
|
490
491
|
// Insert documents (without defaults)
|
|
491
492
|
let dog1 = await db.dog._insert({})
|
|
493
|
+
let dog2 = await db.dog._insert({})
|
|
492
494
|
let user1 = await db.user._insert({
|
|
493
495
|
addresses: [
|
|
494
496
|
{ city: 'Frankfurt' },
|
|
495
497
|
{ city: 'Christchurch', country: 'New Zealand' }
|
|
496
498
|
],
|
|
497
|
-
pet: { dog: dog1._id }
|
|
499
|
+
pet: { dog: dog1._id },
|
|
500
|
+
pets: { dog: [dog1._id, dog2._id]},
|
|
498
501
|
})
|
|
499
502
|
await db.dog._update(dog1._id, { $set: { user: user1._id }})
|
|
500
503
|
|
|
501
504
|
let find1 = await db.user.findOne({
|
|
502
505
|
query: user1._id,
|
|
503
|
-
populate: ['pet.dog', {
|
|
506
|
+
populate: ['pet.dog', 'pets.dog', {
|
|
504
507
|
from: 'dog',
|
|
505
508
|
localField: '_id',
|
|
506
509
|
foreignField: 'user',
|
|
@@ -516,19 +519,64 @@ module.exports = function(monastery, opendb) {
|
|
|
516
519
|
],
|
|
517
520
|
address: { country: 'Germany' },
|
|
518
521
|
pet: { dog: { _id: dog1._id, name: 'Scruff', user: user1._id }},
|
|
522
|
+
pets: {
|
|
523
|
+
dog: [
|
|
524
|
+
{ _id: dog1._id, name: 'Scruff', user: user1._id },
|
|
525
|
+
{ _id: dog2._id, name: 'Scruff' },
|
|
526
|
+
]
|
|
527
|
+
},
|
|
519
528
|
dogs: [{ _id: dog1._id, name: 'Scruff', user: user1._id }]
|
|
520
529
|
})
|
|
521
530
|
|
|
522
|
-
|
|
531
|
+
db.close()
|
|
532
|
+
})
|
|
533
|
+
|
|
534
|
+
test('find default field blacklisted', async () => {
|
|
535
|
+
let db = (await opendb(null)).db
|
|
536
|
+
db.model('user', {
|
|
537
|
+
fields: {
|
|
538
|
+
name: { type: 'string', default: 'Martin Luther' },
|
|
539
|
+
addresses: [{ city: { type: 'string' }, country: { type: 'string', default: 'Germany' } }],
|
|
540
|
+
address: { country: { type: 'string', default: 'Germany' }},
|
|
541
|
+
pet: { dog: { model: 'dog' }},
|
|
542
|
+
pets: { dog: [{ model: 'dog' }]},
|
|
543
|
+
dogs: [{ model: 'dog' }], // virtual association
|
|
544
|
+
}
|
|
545
|
+
})
|
|
546
|
+
db.model('dog', {
|
|
547
|
+
fields: {
|
|
548
|
+
age: { type: 'number', default: 12 },
|
|
549
|
+
name: { type: 'string', default: 'Scruff' },
|
|
550
|
+
user: { model: 'user' }
|
|
551
|
+
},
|
|
552
|
+
findBL: ['age']
|
|
553
|
+
})
|
|
554
|
+
let dog1 = await db.dog._insert({})
|
|
555
|
+
let dog2 = await db.dog._insert({})
|
|
556
|
+
let user1 = await db.user._insert({
|
|
557
|
+
addresses: [
|
|
558
|
+
{ city: 'Frankfurt' },
|
|
559
|
+
{ city: 'Christchurch', country: 'New Zealand' }
|
|
560
|
+
],
|
|
561
|
+
pet: { dog: dog1._id },
|
|
562
|
+
pets: { dog: [dog1._id, dog2._id]},
|
|
563
|
+
})
|
|
564
|
+
await db.dog._update(dog1._id, { $set: { user: user1._id }})
|
|
565
|
+
|
|
566
|
+
// Blacklisted direct/populated default fields (should be removed)
|
|
523
567
|
let find2 = await db.user.findOne({
|
|
524
568
|
query: user1._id,
|
|
525
|
-
populate: [
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
569
|
+
populate: [
|
|
570
|
+
'pet.dog',
|
|
571
|
+
'pets.dog',
|
|
572
|
+
{
|
|
573
|
+
from: 'dog',
|
|
574
|
+
localField: '_id',
|
|
575
|
+
foreignField: 'user',
|
|
576
|
+
as: 'dogs',
|
|
577
|
+
}
|
|
578
|
+
],
|
|
579
|
+
blacklist: ['address', 'addresses.country', 'pets.dog.name', 'dogs.name'],
|
|
532
580
|
// ^ great test (address should cancel addresses if not stopping at the .)
|
|
533
581
|
})
|
|
534
582
|
expect(find2).toEqual({
|
|
@@ -536,14 +584,20 @@ module.exports = function(monastery, opendb) {
|
|
|
536
584
|
name: 'Martin Luther',
|
|
537
585
|
addresses: [{ city: 'Frankfurt' }, { city: 'Christchurch' }],
|
|
538
586
|
pet: { dog: { _id: dog1._id, name: 'Scruff', user: user1._id }},
|
|
539
|
-
dogs: [{ _id: dog1._id, user: user1._id }]
|
|
587
|
+
dogs: [{ _id: dog1._id, user: user1._id }],
|
|
588
|
+
pets: {
|
|
589
|
+
dog: [
|
|
590
|
+
{ _id: dog1._id, user: user1._id, /*age, name*/ },
|
|
591
|
+
{ _id: dog2._id, /*age, name*/ },
|
|
592
|
+
]
|
|
593
|
+
},
|
|
540
594
|
})
|
|
541
595
|
|
|
542
596
|
db.close()
|
|
543
597
|
})
|
|
544
598
|
|
|
545
599
|
test('findOneAndUpdate general', async () => {
|
|
546
|
-
// todo: test all findOneAndUpdate options
|
|
600
|
+
// todo: test all findOneAndUpdate options (e.g. array population)
|
|
547
601
|
// todo: test find & update hooks
|
|
548
602
|
let db = (await opendb(null)).db
|
|
549
603
|
let dog = db.model('dog', {
|
package/test/populate.js
CHANGED
|
@@ -82,6 +82,81 @@ module.exports = function(monastery, opendb) {
|
|
|
82
82
|
db.close()
|
|
83
83
|
})
|
|
84
84
|
|
|
85
|
+
test('model populate array', async () => {
|
|
86
|
+
// Setup
|
|
87
|
+
let db = (await opendb(null)).db
|
|
88
|
+
let bird = db.model('bird', { fields: {
|
|
89
|
+
name: { type: 'string' }
|
|
90
|
+
}})
|
|
91
|
+
let user = db.model('user', { fields: {
|
|
92
|
+
birds: [{ model: 'bird' }],
|
|
93
|
+
animal: { birds: [{ model: 'bird' }] },
|
|
94
|
+
animals: [{ bird: { model: 'bird' }, num: { type: 'number' } }],
|
|
95
|
+
}})
|
|
96
|
+
let bird1 = await bird.insert({ data: { name: 'ponyo' }})
|
|
97
|
+
let bird2 = await bird.insert({ data: { name: 'jack' }})
|
|
98
|
+
let bird3 = await bird.insert({ data: { name: 'sophie' }})
|
|
99
|
+
let user1 = await user.insert({ data: {
|
|
100
|
+
birds: [bird1._id, bird2._id],
|
|
101
|
+
animal: { birds: [bird1._id, bird2._id] },
|
|
102
|
+
animals: [{ bird: bird1._id, num: 1 }, { bird: bird3._id, num: 2 }],
|
|
103
|
+
}})
|
|
104
|
+
|
|
105
|
+
// Array
|
|
106
|
+
let find1 = await user.findOne({ query: user1._id, populate: ['birds'] })
|
|
107
|
+
expect(find1).toEqual({
|
|
108
|
+
_id: user1._id,
|
|
109
|
+
birds: [
|
|
110
|
+
{ _id: bird1._id, name: 'ponyo' },
|
|
111
|
+
{ _id: bird2._id, name: 'jack' },
|
|
112
|
+
],
|
|
113
|
+
animal: {
|
|
114
|
+
birds: [bird1._id, bird2._id],
|
|
115
|
+
},
|
|
116
|
+
animals: [
|
|
117
|
+
{ bird: bird1._id, num: 1 },
|
|
118
|
+
{ bird: bird3._id, num: 2 },
|
|
119
|
+
],
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// Nested array
|
|
123
|
+
let find2 = await user.findOne({ query: user1._id, populate: ['animal.birds'] })
|
|
124
|
+
expect(find2).toEqual({
|
|
125
|
+
_id: user1._id,
|
|
126
|
+
birds: [bird1._id, bird2._id],
|
|
127
|
+
animal: {
|
|
128
|
+
birds: [
|
|
129
|
+
{ _id: bird1._id, name: 'ponyo' },
|
|
130
|
+
{ _id: bird2._id, name: 'jack' },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
animals: [
|
|
134
|
+
{ bird: bird1._id, num: 1 },
|
|
135
|
+
{ bird: bird3._id, num: 2 },
|
|
136
|
+
],
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
// modelId within an array of subdocuments (won't populate, but show a debug error)
|
|
140
|
+
user.error = (e) => {/*console.log(e)*/} // hide debug error
|
|
141
|
+
let find3 = await user.findOne({ query: user1._id, populate: ['animals.bird'] })
|
|
142
|
+
expect(find3).toEqual({
|
|
143
|
+
_id: user1._id,
|
|
144
|
+
birds: [bird1._id, bird2._id],
|
|
145
|
+
animal: {
|
|
146
|
+
birds: [
|
|
147
|
+
bird1._id,
|
|
148
|
+
bird2._id
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
animals: [
|
|
152
|
+
{ bird: bird1._id, num: 1 },
|
|
153
|
+
{ bird: bird3._id, num: 2 },
|
|
154
|
+
],
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
db.close()
|
|
158
|
+
})
|
|
159
|
+
|
|
85
160
|
test('model populate type=any', async () => {
|
|
86
161
|
let db = (await opendb(null)).db
|
|
87
162
|
db.model('company', { fields: {
|