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 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 modelName = (path.split('.').reduce((o,i) => o[i], this.fields) ||{}).model
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
- // Populate model (convert array into document & create lookup)
107
- (opts.addFields = opts.addFields || {})[path] = { '$arrayElemAt': [ '$' + path, 0 ] }
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 - $project object
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 parentModelData = util.toArray(data).map(o => ({ modelName: this.name, dataRef: o }))
503
- let modelData = this._recurseAndFindModels(this.fields, data).concat(parentModelData)
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 modelData) {
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
- util.forEach(model[item.modelName].fieldsFlattened, (schema, path) => {
511
- if (!util.isDefined(schema.default) || path.match(/^\.?(createdAt|updatedAt)$/)) return
512
- let parentPath = item.fieldName? item.fieldName + '.' : ''
513
- let pathWithoutArrays = (parentPath + path).replace(/\.0(\.|$)/, '$1')
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(pathWithoutArrays, projection)) return
516
- // console.log(pathWithoutArrays, path, projection)
517
- // Set value
518
- let value = util.isFunction(schema.default)? schema.default(this.manager) : schema.default
519
- util.setDeepValue(item.dataRef, path.replace(/\.0(\.|$)/g, '.$$$1'), value, true, false, true)
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
- _pathBlacklisted: function(path, projection, matchDeepWhitelistedKeys=true) {
593
+ _recurseAndFindModels: function(parentPath, schemaFields, dataArr) {
531
594
  /**
532
- * Checks if the path is blacklisted within a inclusion/exclusion projection
533
- * @param {string} path - path without array brackets e.g. '.[]'
534
- * @param {object} projection - inclusion/exclusion projection, not mixed
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
- * fieldName: usersNewCompany,
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(fields, (field, fieldName) => {
574
- if (!data) return
575
-
576
- // Valid model object field.
577
- if (
578
- ((util.isArray(field) && field[0].model) || field.model) &&
579
- data[fieldName] &&
580
- util.isObjectAndNotID(data[fieldName])
581
- ) {
582
- // Note that sometimes a single model is passed instead of an array of models via a custom populate $lookup
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
- fieldName: fieldName,
627
+ dataPath: newParentPath,
628
+ dataFieldName: fieldName,
586
629
  modelName: field.model || field[0].model
587
630
  })
588
-
589
- // Recurse through fields that are sub-documents
590
- } else if (
591
- util.isSubdocument(field) &&
592
- util.isObjectAndNotID(data[fieldName])
593
- ) {
594
- out = [...out, ...this._recurseAndFindModels(field, data[fieldName])]
595
-
596
- // Array of sub-documents or models
597
- } else if (
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.1.1",
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
- // Blacklisted default field population test
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: ['pet.dog', {
526
- from: 'dog',
527
- localField: '_id',
528
- foreignField: 'user',
529
- as: 'dogs'
530
- }],
531
- blacklist: ['address', 'addresses.country', 'dogs.name']
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: {