monastery 3.0.4 → 3.0.6

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/.eslintrc.json CHANGED
@@ -9,6 +9,7 @@
9
9
  ],
10
10
  "globals": {
11
11
  // jest globals
12
+ "jest": true,
12
13
  "test": true,
13
14
  "expect": true,
14
15
  "afterAll": true,
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.6](https://github.com/boycce/monastery/compare/3.0.5...3.0.6) (2024-04-29)
6
+
7
+ ### [3.0.5](https://github.com/boycce/monastery/compare/3.0.4...3.0.5) (2024-04-29)
8
+
5
9
  ### [3.0.4](https://github.com/boycce/monastery/compare/3.0.3...3.0.4) (2024-04-29)
6
10
 
7
11
  ### [3.0.3](https://github.com/boycce/monastery/compare/3.0.2...3.0.3) (2024-04-28)
package/docs/readme.md CHANGED
@@ -87,9 +87,11 @@ You can view MongoDB's [compatibility table here](https://www.mongodb.com/docs/d
87
87
  - Removed callback functions on all model methods, you can use the returned promise instead
88
88
  - model.update() now returns the following _update property: `{ acknowledged: true, matchedCount: 1, modifiedCount: 1, upsertedCount: 0, upsertedId: null }` instead of `{ n: 1, nModified: 1, ok: 1 }`
89
89
  - model.remove() now returns `{ acknowledged: true, deletedCount: 1 }`, instead of `{ results: {n:1, ok:1} }`
90
- - Models are now added to db.models instead of db.model, e.g. db.models.user
91
- - MongoDB connection can be found here db.db changed from db._db
92
90
  - model._indexes() now returns collection._indexes() not collection._indexInformation()
91
+ - db.model.* moved to db.models.*
92
+ - db._client moved to db.client
93
+ - db._db moved to db.db
94
+ - db.catch/then() moved to db.onError/db.onOpen()
93
95
 
94
96
  ## Roadmap
95
97
 
package/lib/index.js CHANGED
@@ -215,7 +215,7 @@ Manager.prototype.onError = function(fn) {
215
215
  resolve(err)
216
216
  })
217
217
  }).then((err) => {
218
- fn(err)
218
+ return fn(err)
219
219
  })
220
220
  }
221
221
 
@@ -226,6 +226,9 @@ Manager.prototype.onOpen = function(fn) {
226
226
  * @return {Promise(manager)}
227
227
  */
228
228
  return new Promise((resolve, reject) => {
229
+ if (this._state == 'open') {
230
+ resolve(this)
231
+ }
229
232
  this.on('open', () => {
230
233
  resolve(this)
231
234
  })
@@ -233,7 +236,7 @@ Manager.prototype.onOpen = function(fn) {
233
236
  reject(err)
234
237
  })
235
238
  }).then((err) => { // If `then` is not chained, the error will be thrown, detached!
236
- fn(err)
239
+ return fn(err)
237
240
  })
238
241
  }
239
242
 
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(this.beforeInsert.map(f => f.bind(opts, data)))
51
+ await util.runSeries(this.beforeInsert.map(f => f.bind(opts, data)), `${this.name}.beforeInsert`)
52
52
  let response = await this._insert(data, util.omit(opts, this._queryOptions))
53
- await util.runSeries(this.afterInsert.map(f => f.bind(opts, response)))
53
+ await util.runSeries(this.afterInsert.map(f => f.bind(opts, response)), `${this.name}.afterInsert`)
54
54
 
55
55
  // Success/error
56
56
  if (opts.req && opts.respond) opts.req.res.json(response)
@@ -267,7 +267,7 @@ Model.prototype.update = async function (opts, type='update') {
267
267
  }
268
268
 
269
269
  // Hook: beforeUpdate (has access to original, non-validated opts.data)
270
- await util.runSeries(this.beforeUpdate.map(f => f.bind(opts, data||{})))
270
+ await util.runSeries(this.beforeUpdate.map(f => f.bind(opts, data||{})), `${this.name}.beforeUpdate`)
271
271
 
272
272
  if (data && operators['$set']) {
273
273
  this.info(`'$set' fields take precedence over the data fields for \`${this.name}.${type}()\``)
@@ -297,7 +297,9 @@ Model.prototype.update = async function (opts, type='update') {
297
297
  }
298
298
 
299
299
  // Hook: afterUpdate (doesn't have access to validated data)
300
- if (response) await util.runSeries(this.afterUpdate.map(f => f.bind(opts, response)))
300
+ if (response) {
301
+ await util.runSeries(this.afterUpdate.map(f => f.bind(opts, response)), `${this.name}.afterUpdate`)
302
+ }
301
303
 
302
304
  // Hook: afterFind if findOneAndUpdate
303
305
  if (response && type == 'findOneAndUpdate') {
@@ -329,9 +331,9 @@ Model.prototype.remove = async function (opts) {
329
331
  opts = await this._queryObject(opts, 'remove')
330
332
 
331
333
  // Remove
332
- await util.runSeries(this.beforeRemove.map(f => f.bind(opts)))
334
+ await util.runSeries(this.beforeRemove.map(f => f.bind(opts)), `${this.name}.beforeRemove`)
333
335
  let response = await this._remove(opts.query, util.omit(opts, this._queryOptions))
334
- await util.runSeries(this.afterRemove.map(f => f.bind(response)))
336
+ await util.runSeries(this.afterRemove.map(f => f.bind(response)), `${this.name}.afterRemove`)
335
337
 
336
338
  // Success
337
339
  if (opts.req && opts.respond) opts.req.res.json(response)
@@ -589,7 +591,7 @@ Model.prototype._processAfterFind = function (data, projection={}, afterFindCont
589
591
  callbackSeries.push(fn.bind(afterFindContext, item.dataRef))
590
592
  }
591
593
  }
592
- return util.runSeries(callbackSeries).then(() => data)
594
+ return util.runSeries(callbackSeries, 'afterFind').then(() => data)
593
595
  }
594
596
 
595
597
  Model.prototype._recurseAndFindModels = function (parentPath, schemaFields, dataArr) {
@@ -29,7 +29,8 @@ 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
- await util.runSeries(this.beforeValidate.map(f => f.bind(opts, data)))
32
+
33
+ await util.runSeries(this.beforeValidate.map(f => f.bind(opts, data)), `${this.name}.beforeValidate`)
33
34
 
34
35
  // Recurse and validate fields
35
36
  let response = util.toArray(data).map(item => {
package/lib/util.js CHANGED
@@ -309,10 +309,11 @@ module.exports = {
309
309
  return variable
310
310
  },
311
311
 
312
- runSeries: function(tasks, cb) {
312
+ runSeries: function(tasks, info, cb) {
313
313
  /*
314
314
  * Runs functions in series and calls the cb when done
315
315
  * @param {function(err, result)[]} tasks - array of functions
316
+ * @param {object} <info> - data to pass to the error
316
317
  * @param {function(err, results[])} <cb>
317
318
  * @return promise
318
319
  * @source https://github.com/feross/run-series
@@ -322,9 +323,10 @@ module.exports = {
322
323
  let isSync = true
323
324
 
324
325
  return new Promise((res, rej) => {
325
- function each(i, err, result) { // aka next(err, data)
326
- if (i == current + 1) {
327
- throw new Error('Hook error: you cannot return a promise and call next()')
326
+ function next(i, err, result) { // aka next(err, data)
327
+ if (i !== current) {
328
+ console.error(`Monastery ${info} error: you cannot return a promise AND call next()`)
329
+ return
328
330
  }
329
331
  results.push(result)
330
332
  if (!err && ++current < tasks.length) callTask(current)
@@ -340,10 +342,10 @@ module.exports = {
340
342
  else res(results)
341
343
  }
342
344
  function callTask(i) {
343
- const each2 = each.bind(null, i)
344
- const res = tasks[i](each2)
345
+ const next2 = next.bind(null, i)
346
+ const res = tasks[i](next2)
345
347
  if (res instanceof Promise) {
346
- res.then((result) => each2(null, result)).catch(each2)
348
+ res.then((result) => next2(null, result)).catch(next2)
347
349
  }
348
350
  }
349
351
 
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.4",
5
+ "version": "3.0.6",
6
6
  "license": "MIT",
7
7
  "repository": "github:boycce/monastery",
8
8
  "homepage": "https://boycce.github.io/monastery/",
package/test/crud.js CHANGED
@@ -856,4 +856,94 @@ test('hooks > async', async () => {
856
856
  // Third async hook throws an error
857
857
  await expect(user.find({ query: user3Doc._id }))
858
858
  .rejects.toThrow('An async error occurred with Martin3')
859
+ })
860
+
861
+ test('hooks > async and next conflict', async () => {
862
+ let user1 = db.model('user', {
863
+ fields: { age: { type: 'number'} },
864
+ afterFind: [
865
+ async (data, next) => {
866
+ const promise = await new Promise((resolve, reject) => {
867
+ if (data.age === 0) {
868
+ setTimeout(() => {
869
+ data.age = data.age + 1
870
+ resolve(data)
871
+ }, 100)
872
+ } else {
873
+ resolve(data)
874
+ }
875
+ })
876
+ next() // should console an error after waiting for the promise to finish
877
+ return promise
878
+ },
879
+ async (data, next) => {
880
+ data.age = data.age + 1
881
+ },
882
+ ],
883
+ })
884
+ let user2 = db.model('user2', {
885
+ fields: { age: { type: 'number'} },
886
+ afterFind: [
887
+ async (data, next) => {
888
+ await new Promise((resolve, reject) => {
889
+ setTimeout(() => {
890
+ data.age = data.age + 1
891
+ resolve(data)
892
+ }, 100)
893
+ })
894
+ next() // should console an error after waiting for the promise to finish, without returning a promise
895
+ },
896
+ async (data, next) => {
897
+ data.age = data.age + 1
898
+ },
899
+ ],
900
+ })
901
+ let user3 = db.model('user3', {
902
+ fields: { age: { type: 'number'} },
903
+ afterFind: [
904
+ async (data, next) => {
905
+ next() // should console an error for empty promise
906
+ },
907
+ async (data, next) => {
908
+ data.age = data.age + 1
909
+ },
910
+ ],
911
+ })
912
+ let user4 = db.model('user3', {
913
+ fields: { age: { type: 'number'} },
914
+ afterFind: [
915
+ async (data, next) => {
916
+ return await new Promise((resolve, reject) => {
917
+ setTimeout(() => {
918
+ data.age = data.age + 1
919
+ next() // should console an error
920
+ resolve(data)
921
+ }, 100)
922
+ })
923
+ },
924
+ async (data, next) => {
925
+ data.age = data.age + 1
926
+ },
927
+ ],
928
+ })
929
+ let user1Doc = await user1.insert({ data: { age: 0 } })
930
+ let user2Doc = await user2.insert({ data: { age: 0 } })
931
+ let user3Doc = await user3.insert({ data: { age: 0 } })
932
+ let user4Doc = await user4.insert({ data: { age: 0 } })
933
+
934
+ const logSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
935
+
936
+ // Only increment twice
937
+ await expect(user1.find({ query: user1Doc._id })).resolves.toEqual({ _id: expect.any(Object), age: 2 })
938
+ expect(logSpy).toHaveBeenCalledWith('Monastery afterFind error: you cannot return a promise AND call next()')
939
+
940
+ await expect(user2.find({ query: user2Doc._id })).resolves.toEqual({ _id: expect.any(Object), age: 2 })
941
+ expect(logSpy).toHaveBeenCalledWith('Monastery afterFind error: you cannot return a promise AND call next()')
942
+
943
+ await expect(user3.find({ query: user3Doc._id })).resolves.toEqual({ _id: expect.any(Object), age: 2 })
944
+ expect(logSpy).toHaveBeenCalledWith('Monastery afterFind error: you cannot return a promise AND call next()')
945
+
946
+ await expect(user4.find({ query: user4Doc._id })).resolves.toEqual({ _id: expect.any(Object), age: 2 })
947
+ expect(logSpy).toHaveBeenCalledWith('Monastery afterFind error: you cannot return a promise AND call next()')
948
+
859
949
  })
package/test/manager.js CHANGED
@@ -18,18 +18,34 @@ test('manager > uri error', async () => {
18
18
 
19
19
  test('manager > onError', async () => {
20
20
  // Bad port (thrown by MongoDB)
21
- let error
22
21
  const db = monastery('localhost:1234/monastery', { serverSelectionTimeoutMS: 500 })
23
- await db.onError((res) => { error = res.message })
22
+ let error, isAPromise
23
+ await db.onError((res) => { error = res.message }).then(() => { isAPromise = true })
24
24
  expect(error).toEqual('connect ECONNREFUSED 127.0.0.1:1234')
25
+ expect(isAPromise).toEqual(true)
25
26
  db.close()
26
27
  })
27
28
 
28
29
  test('manager > onOpen', async () => {
29
- let manager
30
30
  const db = monastery('localhost/monastery', { serverSelectionTimeoutMS: 500 })
31
- await db.onOpen((res) => { manager = res })
32
- expect(manager.open).toEqual(expect.any(Function))
31
+
32
+ let manager1
33
+ await db.onOpen((res) => { manager1 = res })
34
+ expect(manager1.open).toEqual(expect.any(Function))
35
+
36
+ // Wait until after the client has been connected
37
+ await new Promise((resolve, reject) => {
38
+ setTimeout(() => {
39
+ resolve()
40
+ }, 1000)
41
+ })
42
+
43
+ // This should still run after the client has been connected
44
+ let manager2
45
+ let isAPromise
46
+ await db.onOpen((res) => { manager2 = res }).then(() => { isAPromise = true })
47
+ expect(manager2.open).toEqual(expect.any(Function))
48
+ expect(isAPromise).toEqual(true)
33
49
  db.close()
34
50
  })
35
51