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 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, data)), 'beforeInsert')
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, response)), 'afterInsert')
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
- await util.runSeries.call(this, this.beforeUpdate.map(f => f.bind(opts, data||{})), 'beforeUpdate')
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, response)), 'afterUpdate')
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 callbackSeries = []
580
+ let seriesGroups = []
580
581
  let models = this.manager.models
581
- let parent = util.toArray(data).map(o => ({ dataRef: o, dataPath: '', dataFieldName: '', modelName: this.name }))
582
- let modelFields = this._recurseAndFindModels(data).concat(parent)
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 (item.dataRef) {
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(item.dataRef, localSchemaFieldPath.replace(/\.0(\.|$)/g, '.$$$1'), value, true, false, true)
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
- for (let fn of models[item.modelName].afterFind) {
609
- callbackSeries.push(fn.bind(afterFindContext, item.dataRef))
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
- return util.runSeries.call(this, callbackSeries, 'afterFind').then(() => data)
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
- * dataRef: { *fields here* },
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
- dataRef: data[key],
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
- dataRef: data[key][i],
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
 
@@ -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, cb) {
314
+ runSeries: function(tasks, hookName, data) {
315
315
  /*
316
- * Runs functions in series and calls the cb when done
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 = hookName == 'afterFind' ? 'afterFind' : this.name + '.' + hookName
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
- results.push(result)
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(results)
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 res = tasks[i](next2)
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.21",
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
  })