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 +4 -0
- package/docs/model/find.md +23 -21
- package/lib/model-crud.js +33 -17
- package/lib/model-validate.js +1 -1
- package/lib/util.js +13 -7
- package/package.json +3 -3
- package/plugins/images/index.js +3 -2
- package/test/crud.js +8 -6
- package/test/populate.js +12 -8
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)
|
package/docs/model/find.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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.
|
|
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`](
|
|
83
|
-
populated field still needs to be defined in `definition.fields` if you want to
|
|
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
|
-
|
|
111
|
-
|
|
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: {
|
|
124
|
+
let: { userId: '$_id' },
|
|
123
125
|
pipeline: [{
|
|
124
126
|
$match: {
|
|
125
127
|
$expr: {
|
|
126
|
-
$eq: ['$bookOwnerId', '$$
|
|
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)),
|
|
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)),
|
|
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
|
-
|
|
145
|
-
|
|
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
|
-
|
|
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||{})),
|
|
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)),
|
|
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)),
|
|
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)),
|
|
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) {
|
package/lib/model-validate.js
CHANGED
|
@@ -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)),
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
328
|
+
const next = (i, err, result) => { // aka next(err, data)
|
|
327
329
|
if (i !== current) {
|
|
328
|
-
|
|
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
|
-
|
|
338
|
+
const done = (err) => {
|
|
337
339
|
if (isSync) process.nextTick(() => end(err))
|
|
338
340
|
else end(err)
|
|
339
341
|
}
|
|
340
|
-
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
40
|
-
"mongodb": "
|
|
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
|
},
|
package/plugins/images/index.js
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
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('
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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' }})
|