monastery 3.0.20 → 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,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
+ ### [3.0.22](https://github.com/boycce/monastery/compare/3.0.21...3.0.22) (2024-05-08)
6
+
7
+ ### [3.0.21](https://github.com/boycce/monastery/compare/3.0.20...3.0.21) (2024-05-07)
8
+
5
9
  ### [3.0.20](https://github.com/boycce/monastery/compare/3.0.19...3.0.20) (2024-05-05)
6
10
 
7
11
  ### [3.0.19](https://github.com/boycce/monastery/compare/3.0.18...3.0.19) (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
@@ -193,7 +193,8 @@ module.exports = {
193
193
  * @param {object}
194
194
  * @return promise(data)
195
195
  */
196
- return this.parseFormData(this.parseDotNotation(obj))
196
+ let data = this.parseDotNotation(obj)
197
+ return this.parseFormData(data)
197
198
  },
198
199
 
199
200
  parseDotNotation: function(obj) {
@@ -201,18 +202,20 @@ module.exports = {
201
202
  * Mutates dot notation objects into a deep object
202
203
  * @param {object}
203
204
  */
204
- let original = this.deepCopy(obj)
205
+ if (!Object.keys(obj).find(o => o.indexOf('.') !== -1)) return obj
206
+ let objCopy = this.deepCopy(obj) // maybe convert to JSON.parse(JSON.stringify(obj))
207
+
205
208
  for (let key in obj) {
206
209
  if (key.indexOf('.') !== -1) {
207
- recurse(key, obj[key], obj)
210
+ setup(key, obj[key], obj)
208
211
  } else {
209
212
  // Ordinary values may of been updated by the bracket notation values, we are
210
213
  // reassigning, trying to preserve the order of keys (not always guaranteed in for loops)
211
- obj[key] = original[key]
214
+ obj[key] = objCopy[key]
212
215
  }
213
216
  }
214
217
  return obj
215
- function recurse(str, val, obj) {
218
+ function setup(str, val, obj) {
216
219
  let parentObj = obj
217
220
  let grandparentObj = obj
218
221
  let keys = str.split(/\./)
@@ -233,7 +236,6 @@ module.exports = {
233
236
  },
234
237
 
235
238
  parseFormData: function(obj) {
236
- let original = this.deepCopy(obj)
237
239
  /**
238
240
  * Mutates FormData (bracket notation) objects into a deep object
239
241
  * @param {object}
@@ -242,21 +244,22 @@ module.exports = {
242
244
  * E.g. ['user']['petnames'][0]
243
245
  * E.g. ['users'][0]['name']
244
246
  */
245
- return new Promise(res => {
246
- for (let key in obj) {
247
- if (key.match(/\[\]\[/i)) {
248
- throw `Array items in bracket notation need array indexes "${key}", e.g. users[0][name]`
249
- }
250
- if (key.indexOf('[') !== -1) {
251
- setup(key)
252
- } else {
253
- // Ordinary values may of been updated by the bracket notation values, we are
254
- // reassigning, trying to preserve the order of keys (not always guaranteed in for loops)
255
- obj[key] = original[key]
256
- }
247
+ if (!Object.keys(obj).find(o => o.indexOf('[') !== -1)) return obj
248
+ let objCopy = this.deepCopy(obj) // maybe convert to JSON.parse(JSON.stringify(obj))
249
+
250
+ for (let key in obj) {
251
+ if (key.match(/\[\]\[/i)) {
252
+ throw new Error(`Monastery: Array items in bracket notation need array indexes "${key}", e.g. users[0][name]`)
257
253
  }
258
- res(obj)
259
- })
254
+ if (key.indexOf('[') !== -1) {
255
+ setup(key)
256
+ } else {
257
+ // Ordinary values may of been updated by the bracket notation values, we are
258
+ // reassigning, trying to preserve the order of keys (not always guaranteed in for loops)
259
+ obj[key] = objCopy[key]
260
+ }
261
+ }
262
+ return obj
260
263
  function setup(path) {
261
264
  let parent = obj
262
265
  let grandparent = obj
@@ -308,20 +311,21 @@ module.exports = {
308
311
  return variable
309
312
  },
310
313
 
311
- runSeries: function(tasks, hookName, cb) {
314
+ runSeries: function(tasks, hookName, data) {
312
315
  /*
313
- * Runs functions in series and calls the cb when done
316
+ * Runs functions in series
314
317
  * @param {function(err, result)[]} tasks - array of functions
315
318
  * @param {string} <hookName> - e.g. 'afterFind'
319
+ * @param {any} data - data to pass to the first function
316
320
  * @param {function(err, results[])} <cb>
317
321
  * @return promise
318
322
  * @this Model
319
323
  * @source https://github.com/feross/run-series
320
324
  */
321
325
  let current = 0
322
- let results = []
323
326
  let isSync = true
324
- let caller = hookName == 'afterFind' ? 'afterFind' : this.name + '.' + hookName
327
+ let caller = (this.afterFindName || this.name) + '.' + hookName
328
+ let lastDefinedResult = data
325
329
 
326
330
  return new Promise((res, rej) => {
327
331
  const next = (i, err, result) => { // aka next(err, data)
@@ -330,8 +334,8 @@ module.exports = {
330
334
  return
331
335
  }
332
336
  current++
333
- results.push(result)
334
- if (!err && current < tasks.length) callTask(current)
337
+ if (!err && typeof result !== 'undefined') lastDefinedResult = result
338
+ if (!err && current < tasks.length) callTask(current, lastDefinedResult)
335
339
  else done(err)
336
340
  }
337
341
  const done = (err) => {
@@ -339,13 +343,13 @@ module.exports = {
339
343
  else end(err)
340
344
  }
341
345
  const end = (err) => {
342
- if (cb) cb(err, results)
343
346
  if (err) rej(err)
344
- else res(results)
347
+ else res(lastDefinedResult)
345
348
  }
346
- const callTask = (i) => {
349
+ const callTask = (i, data) => {
347
350
  const next2 = next.bind(null, i)
348
- const res = tasks[i](next2)
351
+ const args = hookName.match(/remove/i)? [next2] : [data, next2]
352
+ const res = tasks[i](...args)
349
353
  if (res instanceof Promise) {
350
354
  res.then((result) => next2(null, result)).catch((e) => next2(e))
351
355
  }
@@ -353,7 +357,7 @@ module.exports = {
353
357
 
354
358
  // Start
355
359
  if (!tasks.length) done(null)
356
- else callTask(current)
360
+ else callTask(current, lastDefinedResult)
357
361
 
358
362
  isSync = false
359
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.20",
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
  })
package/test/util.js CHANGED
@@ -26,8 +26,9 @@ test('util > formdata', async () => {
26
26
  { 'first': 'Bruce', 'last': 'Lee' },
27
27
  ],
28
28
  })
29
- expect(util.parseFormData({ 'users[][\'name\']': 'Martin' })).rejects
30
- .toEqual('Array items in bracket notation need array indexes "users[][\'name\']", e.g. users[0][name]')
29
+ expect(() => util.parseFormData({ 'users[][\'name\']': 'Martin' })).toThrow(
30
+ 'Monastery: Array items in bracket notation need array indexes "users[][\'name\']", e.g. users[0][name]'
31
+ )
31
32
  })
32
33
 
33
34
  test('util > isId', async () => {