monastery 3.0.21 → 3.0.22
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 +45 -17
- package/lib/model-validate.js +1 -2
- package/lib/util.js +12 -11
- package/package.json +1 -1
- package/test/crud.js +123 -6
package/changelog.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
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
|
+
### [3.0.22](https://github.com/boycce/monastery/compare/3.0.21...3.0.22) (2024-05-08)
|
|
6
|
+
|
|
5
7
|
### [3.0.21](https://github.com/boycce/monastery/compare/3.0.20...3.0.21) (2024-05-07)
|
|
6
8
|
|
|
7
9
|
### [3.0.20](https://github.com/boycce/monastery/compare/3.0.19...3.0.20) (2024-05-05)
|
package/lib/model-crud.js
CHANGED
|
@@ -48,9 +48,9 @@ Model.prototype.insert = async function (opts) {
|
|
|
48
48
|
let data = await this.validate(opts.data || {}, opts) // was { ...opts }
|
|
49
49
|
|
|
50
50
|
// Insert
|
|
51
|
-
await util.runSeries.call(this, this.beforeInsert.map(f => f.bind(opts
|
|
51
|
+
data = await util.runSeries.call(this, this.beforeInsert.map(f => f.bind(opts)), 'beforeInsert', data)
|
|
52
52
|
let response = await this._insert(data, util.omit(opts, this._queryOptions))
|
|
53
|
-
await util.runSeries.call(this, this.afterInsert.map(f => f.bind(opts
|
|
53
|
+
response = await util.runSeries.call(this, this.afterInsert.map(f => f.bind(opts)), 'afterInsert', response)
|
|
54
54
|
|
|
55
55
|
// Success/error
|
|
56
56
|
if (opts.req && opts.respond) opts.req.res.json(response)
|
|
@@ -283,7 +283,8 @@ Model.prototype.update = async function (opts, type='update') {
|
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
// Hook: beforeUpdate (has access to original, non-validated opts.data)
|
|
286
|
-
|
|
286
|
+
data = data || {}
|
|
287
|
+
data = await util.runSeries.call(this, this.beforeUpdate.map(f => f.bind(opts)), 'beforeUpdate', data)
|
|
287
288
|
|
|
288
289
|
if (data && operators['$set']) {
|
|
289
290
|
this.info(`'$set' fields take precedence over the data fields for \`${this.name}.${type}()\``)
|
|
@@ -314,7 +315,7 @@ Model.prototype.update = async function (opts, type='update') {
|
|
|
314
315
|
|
|
315
316
|
// Hook: afterUpdate (doesn't have access to validated data)
|
|
316
317
|
if (response) {
|
|
317
|
-
await util.runSeries.call(this, this.afterUpdate.map(f => f.bind(opts
|
|
318
|
+
response = await util.runSeries.call(this, this.afterUpdate.map(f => f.bind(opts)), 'afterUpdate', response)
|
|
318
319
|
}
|
|
319
320
|
|
|
320
321
|
// Hook: afterFind if findOneAndUpdate
|
|
@@ -561,7 +562,7 @@ Model.prototype._pathBlacklisted = function (path, projection, matchDeepWhitelis
|
|
|
561
562
|
return inclusion? true : false
|
|
562
563
|
}
|
|
563
564
|
|
|
564
|
-
Model.prototype._processAfterFind = function (data, projection={}, afterFindContext={}) {
|
|
565
|
+
Model.prototype._processAfterFind = async function (data, projection={}, afterFindContext={}) {
|
|
565
566
|
/**
|
|
566
567
|
* Todo: Maybe make this method public?
|
|
567
568
|
* Recurses through fields that are models and populates missing default-fields and calls model.afterFind([fn,..])
|
|
@@ -576,16 +577,26 @@ Model.prototype._processAfterFind = function (data, projection={}, afterFindCont
|
|
|
576
577
|
*/
|
|
577
578
|
// Recurse down from the parent model, ending with the parent model as the parent afterFind hook may
|
|
578
579
|
// want to manipulate any populated models
|
|
579
|
-
let
|
|
580
|
+
let seriesGroups = []
|
|
580
581
|
let models = this.manager.models
|
|
581
|
-
let
|
|
582
|
-
|
|
582
|
+
let isArray = util.isArray(data)
|
|
583
|
+
if (!isArray) data = [data]
|
|
584
|
+
let modelFields = this
|
|
585
|
+
._recurseAndFindModels(data)
|
|
586
|
+
.concat(data.map((o, i) => ({
|
|
587
|
+
dataRefParent: data,
|
|
588
|
+
dataRefKey: i,
|
|
589
|
+
dataPath: '',
|
|
590
|
+
dataFieldName: '',
|
|
591
|
+
modelName: this.name,
|
|
592
|
+
})))
|
|
583
593
|
|
|
584
594
|
// Loop found model/deep-model data objects, and populate missing default-fields and call afterFind on each
|
|
585
595
|
for (let item of modelFields) {
|
|
596
|
+
const dataRef = item.dataRefParent[item.dataRefKey]
|
|
586
597
|
// Populate missing default fields if data !== null
|
|
587
598
|
// NOTE: maybe only call functions if default is being set.. fine for now
|
|
588
|
-
if (
|
|
599
|
+
if (dataRef) {
|
|
589
600
|
for (const localSchemaFieldPath in models[item.modelName].fieldsFlattened) {
|
|
590
601
|
const schema = models[item.modelName].fieldsFlattened[localSchemaFieldPath]
|
|
591
602
|
if (!util.isDefined(schema.default) || localSchemaFieldPath.match(/^\.?(createdAt|updatedAt)$/)) continue
|
|
@@ -601,25 +612,37 @@ Model.prototype._processAfterFind = function (data, projection={}, afterFindCont
|
|
|
601
612
|
|
|
602
613
|
// Set default value
|
|
603
614
|
const value = util.isFunction(schema.default) ? schema.default(this.manager) : schema.default
|
|
604
|
-
util.setDeepValue(
|
|
615
|
+
util.setDeepValue(dataRef, localSchemaFieldPath.replace(/\.0(\.|$)/g, '.$$$1'), value, true, false, true)
|
|
605
616
|
}
|
|
606
617
|
}
|
|
607
618
|
// Collect all of the model's afterFind hooks
|
|
608
|
-
|
|
609
|
-
|
|
619
|
+
if (models[item.modelName].afterFind.length) {
|
|
620
|
+
seriesGroups.push(
|
|
621
|
+
(async (_item) => {
|
|
622
|
+
const _modelName = _item.modelName
|
|
623
|
+
const _model = models[_modelName]
|
|
624
|
+
const _opts = { ...afterFindContext, afterFindName: _modelName }
|
|
625
|
+
const _dataRef = _item.dataRefParent[_item.dataRefKey]
|
|
626
|
+
_item.dataRefParent[_item.dataRefKey] = (
|
|
627
|
+
await util.runSeries.call(_model, _model.afterFind.map(f => f.bind(_opts)), 'afterFind', _dataRef)
|
|
628
|
+
)
|
|
629
|
+
}).bind(null, item)
|
|
630
|
+
)
|
|
610
631
|
}
|
|
611
632
|
}
|
|
612
|
-
|
|
633
|
+
for (let item of seriesGroups) await item()
|
|
634
|
+
return isArray ? data : data[0]
|
|
613
635
|
}
|
|
614
636
|
|
|
615
637
|
Model.prototype._recurseAndFindModels = function (dataArr, dataParentPath='', modelPaths) {
|
|
616
638
|
/**
|
|
617
|
-
* Returns a flattened list of models fields
|
|
639
|
+
* Returns a flattened list of models fields, sorted by depth
|
|
618
640
|
* @param {object|array} dataArr
|
|
619
641
|
* @param {string} <dataParentPath>
|
|
620
642
|
* @this Model
|
|
621
643
|
* @return [{
|
|
622
|
-
*
|
|
644
|
+
* dataRefParent: { *fields here* },
|
|
645
|
+
* dataRefKey: 'fieldName',
|
|
623
646
|
* dataPath: 'usersNewCompany',
|
|
624
647
|
* dataFieldName: usersNewCompany,
|
|
625
648
|
* modelName: company
|
|
@@ -667,7 +690,8 @@ Model.prototype._recurseAndFindModels = function (dataArr, dataParentPath='', mo
|
|
|
667
690
|
// Array of data models (schema field can be either a single or array of models, due to custom $lookup's)
|
|
668
691
|
} else if (pathMatch && util.isObject(data[key])) {
|
|
669
692
|
out.push({
|
|
670
|
-
|
|
693
|
+
dataRefParent: data,
|
|
694
|
+
dataRefKey: key,
|
|
671
695
|
dataPath: dataPath,
|
|
672
696
|
dataFieldName: key,
|
|
673
697
|
modelName: this.fieldsFlattened[modelPath.k].model,
|
|
@@ -677,7 +701,8 @@ Model.prototype._recurseAndFindModels = function (dataArr, dataParentPath='', mo
|
|
|
677
701
|
} else if (pathMatch && util.isObject(data[key][0])) {
|
|
678
702
|
for (let i=0, l=data[key].length; i<l; i++) {
|
|
679
703
|
out.push({
|
|
680
|
-
|
|
704
|
+
dataRefParent: data[key],
|
|
705
|
+
dataRefKey: i,
|
|
681
706
|
dataPath: dataPath + '.' + i,
|
|
682
707
|
dataFieldName: key,
|
|
683
708
|
modelName: this.fieldsFlattened[modelPath.k].model,
|
|
@@ -687,6 +712,9 @@ Model.prototype._recurseAndFindModels = function (dataArr, dataParentPath='', mo
|
|
|
687
712
|
}
|
|
688
713
|
}
|
|
689
714
|
|
|
715
|
+
// Sort by dataPath length so that nested models are processed first
|
|
716
|
+
// note: this shouldn't matter anyway since we can't currently have nested populated models
|
|
717
|
+
out = out.sort((a, b) => b.dataPath.split('.').length - a.dataPath.split('.').length)
|
|
690
718
|
return out
|
|
691
719
|
}
|
|
692
720
|
|
package/lib/model-validate.js
CHANGED
|
@@ -29,8 +29,7 @@ Model.prototype.validate = async function (data, opts) {
|
|
|
29
29
|
else opts.projectionValidate = this._getProjectionFromBlacklist(opts.update ? 'update' : 'insert', opts.blacklist)
|
|
30
30
|
|
|
31
31
|
// Hook: beforeValidate
|
|
32
|
-
|
|
33
|
-
await util.runSeries.call(this, this.beforeValidate.map(f => f.bind(opts, data)), 'beforeValidate')
|
|
32
|
+
data = await util.runSeries.call(this, this.beforeValidate.map(f => f.bind(opts)), 'beforeValidate', data)
|
|
34
33
|
|
|
35
34
|
// Recurse and validate fields
|
|
36
35
|
let response = util.toArray(data).map(item => {
|
package/lib/util.js
CHANGED
|
@@ -311,20 +311,21 @@ module.exports = {
|
|
|
311
311
|
return variable
|
|
312
312
|
},
|
|
313
313
|
|
|
314
|
-
runSeries: function(tasks, hookName,
|
|
314
|
+
runSeries: function(tasks, hookName, data) {
|
|
315
315
|
/*
|
|
316
|
-
* Runs functions in series
|
|
316
|
+
* Runs functions in series
|
|
317
317
|
* @param {function(err, result)[]} tasks - array of functions
|
|
318
318
|
* @param {string} <hookName> - e.g. 'afterFind'
|
|
319
|
+
* @param {any} data - data to pass to the first function
|
|
319
320
|
* @param {function(err, results[])} <cb>
|
|
320
321
|
* @return promise
|
|
321
322
|
* @this Model
|
|
322
323
|
* @source https://github.com/feross/run-series
|
|
323
324
|
*/
|
|
324
325
|
let current = 0
|
|
325
|
-
let results = []
|
|
326
326
|
let isSync = true
|
|
327
|
-
let caller =
|
|
327
|
+
let caller = (this.afterFindName || this.name) + '.' + hookName
|
|
328
|
+
let lastDefinedResult = data
|
|
328
329
|
|
|
329
330
|
return new Promise((res, rej) => {
|
|
330
331
|
const next = (i, err, result) => { // aka next(err, data)
|
|
@@ -333,8 +334,8 @@ module.exports = {
|
|
|
333
334
|
return
|
|
334
335
|
}
|
|
335
336
|
current++
|
|
336
|
-
|
|
337
|
-
if (!err && current < tasks.length) callTask(current)
|
|
337
|
+
if (!err && typeof result !== 'undefined') lastDefinedResult = result
|
|
338
|
+
if (!err && current < tasks.length) callTask(current, lastDefinedResult)
|
|
338
339
|
else done(err)
|
|
339
340
|
}
|
|
340
341
|
const done = (err) => {
|
|
@@ -342,13 +343,13 @@ module.exports = {
|
|
|
342
343
|
else end(err)
|
|
343
344
|
}
|
|
344
345
|
const end = (err) => {
|
|
345
|
-
if (cb) cb(err, results)
|
|
346
346
|
if (err) rej(err)
|
|
347
|
-
else res(
|
|
347
|
+
else res(lastDefinedResult)
|
|
348
348
|
}
|
|
349
|
-
const callTask = (i) => {
|
|
349
|
+
const callTask = (i, data) => {
|
|
350
350
|
const next2 = next.bind(null, i)
|
|
351
|
-
const
|
|
351
|
+
const args = hookName.match(/remove/i)? [next2] : [data, next2]
|
|
352
|
+
const res = tasks[i](...args)
|
|
352
353
|
if (res instanceof Promise) {
|
|
353
354
|
res.then((result) => next2(null, result)).catch((e) => next2(e))
|
|
354
355
|
}
|
|
@@ -356,7 +357,7 @@ module.exports = {
|
|
|
356
357
|
|
|
357
358
|
// Start
|
|
358
359
|
if (!tasks.length) done(null)
|
|
359
|
-
else callTask(current)
|
|
360
|
+
else callTask(current, lastDefinedResult)
|
|
360
361
|
|
|
361
362
|
isSync = false
|
|
362
363
|
})
|
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.22",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": "github:boycce/monastery",
|
|
8
8
|
"homepage": "https://boycce.github.io/monastery/",
|
package/test/crud.js
CHANGED
|
@@ -687,7 +687,7 @@ test('count defaults', async () => {
|
|
|
687
687
|
.resolves.toEqual(2)
|
|
688
688
|
})
|
|
689
689
|
|
|
690
|
-
test('hooks', async () => {
|
|
690
|
+
test('hooks > basic', async () => {
|
|
691
691
|
let user = db.model('user', {
|
|
692
692
|
fields: {
|
|
693
693
|
first: { type: 'string'},
|
|
@@ -774,6 +774,123 @@ test('hooks', async () => {
|
|
|
774
774
|
await expect(user2.update({ query: userDoc2._id, data: { first: 'M', bad: true } })).rejects.toThrow('error2')
|
|
775
775
|
})
|
|
776
776
|
|
|
777
|
+
test('hooks > chained values', async () => {
|
|
778
|
+
let bookCount = 0
|
|
779
|
+
const afterInsertAsync = [
|
|
780
|
+
async (data) => {
|
|
781
|
+
return // ignored
|
|
782
|
+
},
|
|
783
|
+
async (data) => {
|
|
784
|
+
data.first = 'Martin11'
|
|
785
|
+
},
|
|
786
|
+
async (data) => {
|
|
787
|
+
expect(data.first).toEqual('Martin11')
|
|
788
|
+
return { ...data, first: 'Martin' }
|
|
789
|
+
},
|
|
790
|
+
async (data) => {
|
|
791
|
+
expect(data.first).toEqual('Martin')
|
|
792
|
+
return // ignored
|
|
793
|
+
},
|
|
794
|
+
]
|
|
795
|
+
const afterFindAsync = [
|
|
796
|
+
async (data) => {
|
|
797
|
+
return // ignored
|
|
798
|
+
},
|
|
799
|
+
async (data) => {
|
|
800
|
+
bookCount++
|
|
801
|
+
if (data.bookNumber) data.bookNumber += (1 + bookCount)
|
|
802
|
+
else data.first = 'Martin2'
|
|
803
|
+
},
|
|
804
|
+
async (data) => {
|
|
805
|
+
if (data.bookNumber) {
|
|
806
|
+
expect(data).toEqual({ _id: expect.any(Object), bookNumber: 11 + bookCount })
|
|
807
|
+
return { _id: 1, bookNumber: 11 + bookCount }
|
|
808
|
+
} else {
|
|
809
|
+
expect(data).toEqual({ _id: expect.any(Object), first: 'Martin2', books: data.books })
|
|
810
|
+
return { _id: 2, books: data.books }
|
|
811
|
+
}
|
|
812
|
+
},
|
|
813
|
+
async (data) => {
|
|
814
|
+
if (data._id == 1) expect(data).toEqual({ _id: 1, bookNumber: 11 + bookCount })
|
|
815
|
+
else expect(data).toEqual({ _id: 2, books: data.books })
|
|
816
|
+
return // ignored
|
|
817
|
+
},
|
|
818
|
+
]
|
|
819
|
+
const afterFindCallback = [
|
|
820
|
+
(data, next) => {
|
|
821
|
+
next() // ignored
|
|
822
|
+
},
|
|
823
|
+
(data, next) => {
|
|
824
|
+
bookCount++
|
|
825
|
+
if (data.bookNumber) data.bookNumber += (1 + bookCount)
|
|
826
|
+
else data.first = 'Martin2'
|
|
827
|
+
next()
|
|
828
|
+
},
|
|
829
|
+
(data, next) => {
|
|
830
|
+
if (data.bookNumber) {
|
|
831
|
+
expect(data).toEqual({ _id: expect.any(Object), bookNumber: 11 + bookCount })
|
|
832
|
+
next(null, { _id: 1, bookNumber: 11 + bookCount })
|
|
833
|
+
} else {
|
|
834
|
+
expect(data).toEqual({ _id: expect.any(Object), first: 'Martin2', books: data.books })
|
|
835
|
+
next(null, { _id: 2, books: data.books })
|
|
836
|
+
}
|
|
837
|
+
},
|
|
838
|
+
(data, next) => {
|
|
839
|
+
if (data._id == 1) expect(data).toEqual({ _id: 1, bookNumber: 11 + bookCount })
|
|
840
|
+
else expect(data).toEqual({ _id: 2, books: data.books })
|
|
841
|
+
next() // ignored
|
|
842
|
+
},
|
|
843
|
+
]
|
|
844
|
+
|
|
845
|
+
// Async
|
|
846
|
+
db.model('book', {
|
|
847
|
+
fields: { bookNumber: { type: 'number'} },
|
|
848
|
+
afterFind: afterFindAsync,
|
|
849
|
+
})
|
|
850
|
+
db.model('user', {
|
|
851
|
+
fields: { first: { type: 'string'}, books: [{ model: 'book' }] },
|
|
852
|
+
afterInsert: afterInsertAsync,
|
|
853
|
+
afterFind: afterFindAsync,
|
|
854
|
+
})
|
|
855
|
+
let bookDoc = await db.book.insert({ data: { bookNumber: 10 }})
|
|
856
|
+
let bookDoc2 = await db.book.insert({ data: { bookNumber: 10 }})
|
|
857
|
+
let userDoc = await db.user.insert({ data: { first: 'Martin0', books: [bookDoc._id, bookDoc2._id]}})
|
|
858
|
+
|
|
859
|
+
// AfterInsert async
|
|
860
|
+
expect(userDoc).toEqual({
|
|
861
|
+
_id: expect.any(Object),
|
|
862
|
+
first: 'Martin',
|
|
863
|
+
books: [bookDoc._id, bookDoc2._id],
|
|
864
|
+
})
|
|
865
|
+
|
|
866
|
+
// AfterFind async
|
|
867
|
+
await expect(db.user.find({ query: userDoc._id, populate: ['books'] })).resolves.toEqual({
|
|
868
|
+
_id: 2,
|
|
869
|
+
books: [
|
|
870
|
+
{ _id: 1, bookNumber: 12 },
|
|
871
|
+
{ _id: 1, bookNumber: 13 },
|
|
872
|
+
],
|
|
873
|
+
})
|
|
874
|
+
|
|
875
|
+
// AfterFind callback/next
|
|
876
|
+
db.model('book', {
|
|
877
|
+
fields: { bookNumber: { type: 'number'} },
|
|
878
|
+
afterFind: afterFindCallback,
|
|
879
|
+
})
|
|
880
|
+
db.model('user', {
|
|
881
|
+
fields: { first: { type: 'string'}, books: [{ model: 'book' }] },
|
|
882
|
+
afterFind: afterFindCallback,
|
|
883
|
+
})
|
|
884
|
+
await expect(db.user.find({ query: userDoc._id, populate: ['books'] })).resolves.toEqual({
|
|
885
|
+
_id: 2,
|
|
886
|
+
books: [
|
|
887
|
+
{ _id: 1, bookNumber: 15 },
|
|
888
|
+
{ _id: 1, bookNumber: 16 },
|
|
889
|
+
],
|
|
890
|
+
})
|
|
891
|
+
|
|
892
|
+
})
|
|
893
|
+
|
|
777
894
|
test('hooks > async', async () => {
|
|
778
895
|
let user = db.model('user', {
|
|
779
896
|
fields: {
|
|
@@ -954,19 +1071,19 @@ test('hooks > async and next conflict', async () => {
|
|
|
954
1071
|
|
|
955
1072
|
// Only increment twice
|
|
956
1073
|
await expect(user1.find({ query: user1Doc._id })).resolves.toEqual({ _id: expect.any(Object), age: 2 })
|
|
957
|
-
expect(logSpy).toHaveBeenCalledWith('Monastery afterFind error: you cannot return a promise AND call next()')
|
|
1074
|
+
expect(logSpy).toHaveBeenCalledWith('Monastery user.afterFind error: you cannot return a promise AND call next()')
|
|
958
1075
|
|
|
959
1076
|
await expect(user2.find({ query: user2Doc._id })).resolves.toEqual({ _id: expect.any(Object), age: 2 })
|
|
960
|
-
expect(logSpy).toHaveBeenCalledWith('Monastery afterFind error: you cannot return a promise AND call next()')
|
|
1077
|
+
expect(logSpy).toHaveBeenCalledWith('Monastery user.afterFind error: you cannot return a promise AND call next()')
|
|
961
1078
|
|
|
962
1079
|
await expect(user3.find({ query: user3Doc._id })).resolves.toEqual({ _id: expect.any(Object), age: 2 })
|
|
963
|
-
expect(logSpy).toHaveBeenCalledWith('Monastery afterFind error: you cannot return a promise AND call next()')
|
|
1080
|
+
expect(logSpy).toHaveBeenCalledWith('Monastery user.afterFind error: you cannot return a promise AND call next()')
|
|
964
1081
|
|
|
965
1082
|
await expect(user4.find({ query: user4Doc._id })).resolves.toEqual({ _id: expect.any(Object), age: 2 })
|
|
966
|
-
expect(logSpy).toHaveBeenCalledWith('Monastery afterFind error: you cannot return a promise AND call next()')
|
|
1083
|
+
expect(logSpy).toHaveBeenCalledWith('Monastery user.afterFind error: you cannot return a promise AND call next()')
|
|
967
1084
|
|
|
968
1085
|
await expect(user5.find({ query: user5Doc._id })).rejects.toThrow('An async error occurred with Martin3')
|
|
969
|
-
expect(logSpy).toHaveBeenCalledWith('Monastery afterFind error: you cannot return a promise AND call next()')
|
|
1086
|
+
expect(logSpy).toHaveBeenCalledWith('Monastery user.afterFind error: you cannot return a promise AND call next()')
|
|
970
1087
|
|
|
971
1088
|
db2.close()
|
|
972
1089
|
})
|