monastery 3.0.14 → 3.0.16

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.16](https://github.com/boycce/monastery/compare/3.0.15...3.0.16) (2024-05-02)
6
+
7
+ ### [3.0.15](https://github.com/boycce/monastery/compare/3.0.14...3.0.15) (2024-05-01)
8
+
5
9
  ### [3.0.14](https://github.com/boycce/monastery/compare/3.0.13...3.0.14) (2024-05-01)
6
10
 
7
11
  ### [3.0.13](https://github.com/boycce/monastery/compare/3.0.12...3.0.13) (2024-05-01)
@@ -58,30 +58,27 @@ The value of the model reference should be an ID, e.g. `myBook = id`
58
58
  ```js
59
59
  {
60
60
  fields: {
61
- myBook: {
62
- model: 'book'
63
- }
61
+ myBook: { model: 'book' },
62
+ myBooks: [{ model: 'books' }], // you can even populate arrays
64
63
  }
65
64
  }
66
65
  ```
67
66
 
68
- You are then able to easily populate returned results via a find operation.
67
+ You are then able to easily populate the returned results in the find method:
69
68
 
70
69
  ```js
71
- user.find({ query: {...}, populate: ['myBook'] })
70
+ user.find({ query: {...}, populate: ['myBook', 'myBooks', ...] })
72
71
  ```
73
72
 
74
- You can also populate within embedded document fields. Although at this time arrays are not supported,
75
- you would need to use the [example below](#populate-multiple-documents-into-virtual-fields).
73
+ You can also populate within embedded document fields.
76
74
  ```js
77
75
  user.find({ query: {...}, populate: ['myBooks.book'] })
78
76
  ```
79
77
 
80
78
  ### Custom Populate Query
81
79
 
82
- If you would like more control you can either use Mongo's `aggregate` method via [`model._aggregate`](./...rawMethods), or simply pass a MongoDB lookup object to populate. When passing a lookup object, the
83
- populated field still needs to be defined in `definition.fields` if you want to call any related hooks,
84
- and prune any blacklisted fields. See the examples below,
80
+ If you would like more control you can either use Mongo's `aggregate` method via [`model._aggregate`](./rawMethods), or simply pass a MongoDB lookup object to `populate`. When passing a lookup object, the
81
+ populated field still needs to be defined in `definition.fields` if you want Monastery to process any related hooks, field blacklisting and default values. See the example belows,
85
82
 
86
83
  #### Populate a single document
87
84
 
@@ -93,37 +90,42 @@ user.find({
93
90
  populate: [{
94
91
  as: 'myBook',
95
92
  from: 'book',
93
+ foreignField: '_id',
96
94
  localField: 'myBook',
97
- foreignField: '_id'
98
95
  }]
99
96
  })
100
97
  ```
101
98
 
102
99
  #### Populate multiple documents into virtual fields
103
100
 
104
- Below populates all books into `user.myBooks` with a `bookOwnerId` equal to the `user._id`. Since `myBooks`
105
- isn't stored on the user, you will need define it as virtual field
101
+ Below populates all books into `user.myBooks` with a `bookOwnerId` equal to the `user._id`. Since `myBooks` isn't stored on the user, you will need define it as virtual field.
106
102
 
107
103
  ```js
108
- {
104
+ db.model('user', {
109
105
  fields: {
110
- myBooks: [{
111
- model: 'book',
112
- virtual: true
113
- }],
106
+ name: { type: 'string' },
107
+ myBooks: [{ model: 'book', virtual: true }],
114
108
  },
115
- }
109
+ })
110
+ db.model('book', {
111
+ fields: {
112
+ bookTitle: { type: 'string' },
113
+ bookOwnerId: { model: 'user' },
114
+ },
115
+ })
116
+
117
+ // books and user inserted here...
116
118
 
117
119
  user.find({
118
120
  query: {...},
119
121
  populate: [{
120
122
  as: 'myBooks',
121
123
  from: 'book',
122
- let: { id: '$_id' },
124
+ let: { userId: '$_id' },
123
125
  pipeline: [{
124
126
  $match: {
125
127
  $expr: {
126
- $eq: ['$bookOwnerId', '$$id']
128
+ $eq: ['$bookOwnerId', '$$userId']
127
129
  }
128
130
  }
129
131
  }]
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)), `${this.name}.beforeInsert`)
51
+ await util.runSeries.call(this, this.beforeInsert.map(f => f.bind(opts, data)), '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)), `${this.name}.afterInsert`)
53
+ await util.runSeries.call(this, this.afterInsert.map(f => f.bind(opts, response)), 'afterInsert')
54
54
 
55
55
  // Success/error
56
56
  if (opts.req && opts.respond) opts.req.res.json(response)
@@ -141,24 +141,40 @@ Model.prototype.find = async function (opts, _one) {
141
141
  )
142
142
  continue
143
143
  }
144
- // Convert array into a document for non-array targets
145
- if (!arrayTarget) {
144
+ if (arrayTarget) {
145
+ // Create lookup
146
+ lookups.push({
147
+ $lookup: {
148
+ as: path,
149
+ from: modelName,
150
+ let: { arraypath: '$' + path }, // host array path
151
+ pipeline: [
152
+ // Populating an array doesn't return sorted in the original sort order
153
+ { $sort: { _id: 1 } },
154
+ { $match: { $expr: { $in: ['$_id', '$$arraypath'] } } },
155
+ ],
156
+ },
157
+ })
158
+ } else {
159
+ // Convert array into a document for non-array targets
146
160
  (opts.addFields = opts.addFields || {})[path] = { '$arrayElemAt': [ '$' + path, 0 ] }
161
+ // Create basic lookup
162
+ lookups.push({
163
+ $lookup: {
164
+ as: path,
165
+ from: modelName,
166
+ foreignField: '_id',
167
+ localField: path,
168
+ },
169
+ })
147
170
  }
148
- // Create lookup
149
- lookups.push({ $lookup: {
150
- from: modelName,
151
- localField: path,
152
- foreignField: '_id',
153
- as: path,
154
- }})
155
171
  }
156
172
  }
157
173
  // console.log(1, opts.projection)
158
174
  // console.log(2, lookups)
159
175
  let aggregate = [
160
176
  { $match: opts.query },
161
- ...(util.isDefined(opts.sort) ? [{ $sort: opts.sort }] : []),
177
+ { $sort: opts.sort },
162
178
  ...(util.isDefined(opts.skip) ? [{ $limit: opts.skip }] : []),
163
179
  ...(util.isDefined(opts.limit) ? [{ $limit: opts.limit }] : []),
164
180
  ...lookups,
@@ -267,7 +283,7 @@ Model.prototype.update = async function (opts, type='update') {
267
283
  }
268
284
 
269
285
  // Hook: beforeUpdate (has access to original, non-validated opts.data)
270
- await util.runSeries(this.beforeUpdate.map(f => f.bind(opts, data||{})), `${this.name}.beforeUpdate`)
286
+ await util.runSeries.call(this, this.beforeUpdate.map(f => f.bind(opts, data||{})), 'beforeUpdate')
271
287
 
272
288
  if (data && operators['$set']) {
273
289
  this.info(`'$set' fields take precedence over the data fields for \`${this.name}.${type}()\``)
@@ -298,7 +314,7 @@ Model.prototype.update = async function (opts, type='update') {
298
314
 
299
315
  // Hook: afterUpdate (doesn't have access to validated data)
300
316
  if (response) {
301
- await util.runSeries(this.afterUpdate.map(f => f.bind(opts, response)), `${this.name}.afterUpdate`)
317
+ await util.runSeries.call(this, this.afterUpdate.map(f => f.bind(opts, response)), 'afterUpdate')
302
318
  }
303
319
 
304
320
  // Hook: afterFind if findOneAndUpdate
@@ -331,9 +347,9 @@ Model.prototype.remove = async function (opts) {
331
347
  opts = await this._queryObject(opts, 'remove')
332
348
 
333
349
  // Remove
334
- await util.runSeries(this.beforeRemove.map(f => f.bind(opts)), `${this.name}.beforeRemove`)
350
+ await util.runSeries.call(this, this.beforeRemove.map(f => f.bind(opts)), 'beforeRemove')
335
351
  let response = await this._remove(opts.query, util.omit(opts, this._queryOptions))
336
- await util.runSeries(this.afterRemove.map(f => f.bind(response)), `${this.name}.afterRemove`)
352
+ await util.runSeries.call(this, this.afterRemove.map(f => f.bind(response)), 'afterRemove')
337
353
 
338
354
  // Success
339
355
  if (opts.req && opts.respond) opts.req.res.json(response)
@@ -591,7 +607,7 @@ Model.prototype._processAfterFind = function (data, projection={}, afterFindCont
591
607
  callbackSeries.push(fn.bind(afterFindContext, item.dataRef))
592
608
  }
593
609
  }
594
- return util.runSeries(callbackSeries, 'afterFind').then(() => data)
610
+ return util.runSeries.call(this, callbackSeries, 'afterFind').then(() => data)
595
611
  }
596
612
 
597
613
  Model.prototype._recurseAndFindModels = function (parentPath, schemaFields, dataArr) {
@@ -30,7 +30,7 @@ Model.prototype.validate = async function (data, opts) {
30
30
 
31
31
  // Hook: beforeValidate
32
32
 
33
- await util.runSeries(this.beforeValidate.map(f => f.bind(opts, data)), `${this.name}.beforeValidate`)
33
+ await util.runSeries.call(this, this.beforeValidate.map(f => f.bind(opts, data)), 'beforeValidate')
34
34
 
35
35
  // Recurse and validate fields
36
36
  let response = util.toArray(data).map(item => {
package/lib/util.js CHANGED
@@ -309,23 +309,25 @@ module.exports = {
309
309
  return variable
310
310
  },
311
311
 
312
- runSeries: function(tasks, info, cb) {
312
+ runSeries: function(tasks, hookName, 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
+ * @param {string} <hookName> - e.g. 'afterFind'
317
317
  * @param {function(err, results[])} <cb>
318
318
  * @return promise
319
+ * @this Model
319
320
  * @source https://github.com/feross/run-series
320
321
  */
321
322
  let current = 0
322
323
  let results = []
323
324
  let isSync = true
325
+ let caller = hookName == 'afterFind' ? 'afterFind' : this.name + '.' + hookName
324
326
 
325
327
  return new Promise((res, rej) => {
326
- function next(i, err, result) { // aka next(err, data)
328
+ const next = (i, err, result) => { // aka next(err, data)
327
329
  if (i !== current) {
328
- console.error(`Monastery ${info} error: you cannot return a promise AND call next()`)
330
+ this.manager.error(`Monastery ${caller} error: you cannot return a promise AND call next()`)
329
331
  return
330
332
  }
331
333
  current++
@@ -333,16 +335,16 @@ module.exports = {
333
335
  if (!err && current < tasks.length) callTask(current)
334
336
  else done(err)
335
337
  }
336
- function done(err) {
338
+ const done = (err) => {
337
339
  if (isSync) process.nextTick(() => end(err))
338
340
  else end(err)
339
341
  }
340
- function end(err) {
342
+ const end = (err) => {
341
343
  if (cb) cb(err, results)
342
344
  if (err) rej(err)
343
345
  else res(results)
344
346
  }
345
- function callTask(i) {
347
+ const callTask = (i) => {
346
348
  const next2 = next.bind(null, i)
347
349
  const res = tasks[i](next2)
348
350
  if (res instanceof Promise) {
@@ -400,6 +402,10 @@ module.exports = {
400
402
  return obj
401
403
  },
402
404
 
405
+ setTimeoutPromise: function(ms) {
406
+ return new Promise(res => setTimeout(res, ms))
407
+ },
408
+
403
409
  toArray: function(variable) {
404
410
  // converts a variable to an array, if not already so
405
411
  if (typeof variable === 'undefined') return []
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.14",
5
+ "version": "3.0.16",
6
6
  "license": "MIT",
7
7
  "repository": "github:boycce/monastery",
8
8
  "homepage": "https://boycce.github.io/monastery/",
@@ -36,8 +36,8 @@
36
36
  "@aws-sdk/lib-storage": "3.549.0",
37
37
  "@aws-sdk/s3-request-presigner": "3.549.0",
38
38
  "debug": "4.3.4",
39
- "file-type": "^16.5.4",
40
- "mongodb": "^5.9.2",
39
+ "file-type": "16.5.4",
40
+ "mongodb": "5.9.2",
41
41
  "nanoid": "3.2.0",
42
42
  "validator": "13.7.0"
43
43
  },
@@ -50,9 +50,10 @@ let plugin = module.exports = {
50
50
  // v3 examples: https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/javascript_s3_code_examples.html
51
51
  this.getS3Client = (useRegion) => {
52
52
  const { S3 } = require('@aws-sdk/client-s3')
53
- const key = useRegion ? '_s3ClientRegional' : '_s3Client'
53
+ const key = '_s3Client'// useRegion ? '_s3ClientRegional' : '_s3Client'
54
54
  return this[key] || (this[key] = new S3({
55
- region: useRegion ? this.awsRegion : undefined,
55
+ // ...(region: useRegion ? this.awsRegion : undefined,
56
+ region: this.awsRegion, // if region is missing it throws an error, but only in production...
56
57
  credentials: {
57
58
  accessKeyId: this.awsAccessKeyId,
58
59
  secretAccessKey: this.awsSecretAccessKey,
package/test/crud.js CHANGED
@@ -859,7 +859,8 @@ test('hooks > async', async () => {
859
859
  })
860
860
 
861
861
  test('hooks > async and next conflict', async () => {
862
- let user1 = db.model('user', {
862
+ const db2 = monastery('127.0.0.1/monastery', { timestamps: false })
863
+ let user1 = db2.model('user', {
863
864
  fields: { age: { type: 'number'} },
864
865
  afterFind: [
865
866
  async (data, next) => {
@@ -881,7 +882,7 @@ test('hooks > async and next conflict', async () => {
881
882
  },
882
883
  ],
883
884
  })
884
- let user2 = db.model('user2', {
885
+ let user2 = db2.model('user2', {
885
886
  fields: { age: { type: 'number'} },
886
887
  afterFind: [
887
888
  async (data, next) => {
@@ -898,7 +899,7 @@ test('hooks > async and next conflict', async () => {
898
899
  },
899
900
  ],
900
901
  })
901
- let user3 = db.model('user3', {
902
+ let user3 = db2.model('user3', {
902
903
  fields: { age: { type: 'number'} },
903
904
  afterFind: [
904
905
  async (data, next) => {
@@ -910,7 +911,7 @@ test('hooks > async and next conflict', async () => {
910
911
  },
911
912
  ],
912
913
  })
913
- let user4 = db.model('user4', {
914
+ let user4 = db2.model('user4', {
914
915
  fields: { age: { type: 'number'} },
915
916
  afterFind: [
916
917
  async (data, next) => {
@@ -928,7 +929,7 @@ test('hooks > async and next conflict', async () => {
928
929
  ],
929
930
  })
930
931
 
931
- let user5 = db.model('user5', {
932
+ let user5 = db2.model('user5', {
932
933
  fields: { age: { type: 'number'} },
933
934
  afterFind: [
934
935
  async (data, next) => {
@@ -949,7 +950,7 @@ test('hooks > async and next conflict', async () => {
949
950
  let user4Doc = await user4.insert({ data: { age: 0 } })
950
951
  let user5Doc = await user5.insert({ data: { age: 0 } })
951
952
 
952
- const logSpy = jest.spyOn(console, 'error').mockImplementation(() => {})
953
+ const logSpy = jest.spyOn(db2, 'error').mockImplementation(() => {})
953
954
 
954
955
  // Only increment twice
955
956
  await expect(user1.find({ query: user1Doc._id })).resolves.toEqual({ _id: expect.any(Object), age: 2 })
@@ -967,4 +968,5 @@ test('hooks > async and next conflict', async () => {
967
968
  await expect(user5.find({ query: user5Doc._id })).rejects.toThrow('An async error occurred with Martin3')
968
969
  expect(logSpy).toHaveBeenCalledWith('Monastery afterFind error: you cannot return a promise AND call next()')
969
970
 
971
+ db2.close()
970
972
  })
package/test/populate.js CHANGED
@@ -83,14 +83,18 @@ test('model populate', async () => {
83
83
  })
84
84
 
85
85
  test('model populate array', async () => {
86
- let bird = db.model('bird', { fields: {
87
- name: { type: 'string' },
88
- }})
89
- let user = db.model('user', { fields: {
90
- birds: [{ model: 'bird' }],
91
- animal: { birds: [{ model: 'bird' }] },
92
- animals: [{ bird: { model: 'bird' }, num: { type: 'number' } }],
93
- }})
86
+ let bird = db.model('bird_pop', {
87
+ fields: {
88
+ name: { type: 'string' },
89
+ },
90
+ })
91
+ let user = db.model('user_pop', {
92
+ fields: {
93
+ birds: [{ model: 'bird_pop' }],
94
+ animal: { birds: [{ model: 'bird_pop' }] },
95
+ animals: [{ bird: { model: 'bird_pop' }, num: { type: 'number' } }],
96
+ },
97
+ })
94
98
  let bird1 = await bird.insert({ data: { name: 'ponyo' }})
95
99
  let bird2 = await bird.insert({ data: { name: 'jack' }})
96
100
  let bird3 = await bird.insert({ data: { name: 'sophie' }})