monastery 1.31.4 → 1.32.0
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 +30 -0
- package/docs/Gemfile +3 -9
- package/docs/_config.yml +1 -0
- package/docs/image-plugin.md +6 -4
- package/docs/model/find.md +1 -0
- package/docs/{schema.md → schema/index.md} +19 -0
- package/docs/{rules.md → schema/rules.md} +1 -0
- package/lib/model-crud.js +140 -175
- package/lib/model-validate.js +12 -11
- package/lib/model.js +4 -1
- package/package.json +2 -2
- package/plugins/images/index.js +53 -10
- package/test/crud.js +34 -0
- package/test/model.js +72 -0
- package/test/plugin-images.js +55 -0
- package/test/validate.js +104 -9
package/changelog.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
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
|
+
## [1.32.0](https://github.com/boycce/monastery/compare/1.31.7...1.32.0) (2022-02-28)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* added getSignedUrl(s) ([3552a4d](https://github.com/boycce/monastery/commit/3552a4d0b21c192a256a590e3ac1cb48b31c6564))
|
|
11
|
+
* added image optiosn filename, and params ([353b2f0](https://github.com/boycce/monastery/commit/353b2f09ed429a5cd8d74a3b2e94493650fb52e4))
|
|
12
|
+
|
|
13
|
+
### [1.31.7](https://github.com/boycce/monastery/compare/1.31.6...1.31.7) (2022-02-28)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* refactored crud ops ([e7f3f78](https://github.com/boycce/monastery/commit/e7f3f784e123e4a66586a4d9e733d5cac477b98b))
|
|
19
|
+
|
|
20
|
+
### [1.31.6](https://github.com/boycce/monastery/compare/1.31.5...1.31.6) (2022-02-25)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
### Bug Fixes
|
|
24
|
+
|
|
25
|
+
* added partial unqiue index tests ([ff6f193](https://github.com/boycce/monastery/commit/ff6f1938e333407ee17895873d2b42fa5263d7e3))
|
|
26
|
+
* scripts ([bc32680](https://github.com/boycce/monastery/commit/bc326809098ae24686158a0386fbbd6671d86c98))
|
|
27
|
+
|
|
28
|
+
### [1.31.5](https://github.com/boycce/monastery/compare/1.31.4...1.31.5) (2022-02-15)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
### Bug Fixes
|
|
32
|
+
|
|
33
|
+
* scripts ([417ba13](https://github.com/boycce/monastery/commit/417ba13c1a0862f76fadf97d6d6d063a74e196bd))
|
|
34
|
+
|
|
5
35
|
### 1.31.4 (2022-02-15)
|
|
6
36
|
|
|
7
37
|
|
package/docs/Gemfile
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
# Old
|
|
2
|
-
# source 'https://rubygems.org'
|
|
3
|
-
# gem 'github-pages', group: :jekyll_plugins
|
|
4
|
-
|
|
5
|
-
# Below pulls the latest remote_theme in development
|
|
6
1
|
source 'https://rubygems.org'
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
end
|
|
2
|
+
|
|
3
|
+
# Development (strict gem versions used)
|
|
4
|
+
gem 'github-pages', group: :jekyll_plugins
|
package/docs/_config.yml
CHANGED
package/docs/image-plugin.md
CHANGED
|
@@ -26,9 +26,12 @@ Then in your model schema, e.g.
|
|
|
26
26
|
```js
|
|
27
27
|
let user = db.model('user', { fields: {
|
|
28
28
|
logo: {
|
|
29
|
-
type: 'image',
|
|
30
|
-
formats: ['bmp', 'gif', 'jpg', 'jpeg', 'png', 'tiff'],
|
|
31
|
-
|
|
29
|
+
type: 'image', // required
|
|
30
|
+
formats: ['bmp', 'gif', 'jpg', 'jpeg', 'png', 'tiff'],
|
|
31
|
+
filename: 'avatar',
|
|
32
|
+
filesize: 1000 * 1000 * 5, // max size in bytes
|
|
33
|
+
getSignedUrl: true, // get a s3 signed url by default after `find()`
|
|
34
|
+
params: {}, // upload params, https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#upload-property
|
|
32
35
|
},
|
|
33
36
|
logos: [{
|
|
34
37
|
type: 'image'
|
|
@@ -61,4 +64,3 @@ user.update({
|
|
|
61
64
|
Due to known limitations, we are inaccurately able to validate non-binary file types (e.g. txt, svg) before uploading to S3, and rely on their file processing to remove any malicious files.
|
|
62
65
|
|
|
63
66
|
...to be continued
|
|
64
|
-
|
package/docs/model/find.md
CHANGED
|
@@ -15,6 +15,7 @@ Find document(s) in a collection and call related hook: `schema.afterFind`
|
|
|
15
15
|
- [[`options.populate`](#populate)] *(array)*
|
|
16
16
|
- [`options.sort`] *(string\|array\|object)*: same as the mongodb option, but allows string parsing e.g. 'name', 'name:1'
|
|
17
17
|
- [`options.blacklist`] *(array\|string\|false)*: augment `schema.findBL`. `false` will remove all blacklisting
|
|
18
|
+
- [`options.getSignedUrls`] *(boolean)*: get signed urls for all image objects
|
|
18
19
|
- [`options.project`] *(string\|array\|object)*: return only these fields, ignores blacklisting
|
|
19
20
|
- [[`any mongodb option`](http://mongodb.github.io/node-mongodb-native/3.2/api/Collection.html#find)] *(any)*
|
|
20
21
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Schema
|
|
3
3
|
nav_order: 4
|
|
4
|
+
has_children: true
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
# Schema
|
|
@@ -166,6 +167,24 @@ await db.user.insert({
|
|
|
166
167
|
}
|
|
167
168
|
```
|
|
168
169
|
|
|
170
|
+
Since unique indexes by default don't allow mutliple documents with `null`, you use a partial index (less performant), e.g.
|
|
171
|
+
|
|
172
|
+
```js
|
|
173
|
+
|
|
174
|
+
schema.fields = {
|
|
175
|
+
index: {
|
|
176
|
+
name: {
|
|
177
|
+
type: 'string',
|
|
178
|
+
index: {
|
|
179
|
+
type: 'unique',
|
|
180
|
+
partialFilterExpression: {
|
|
181
|
+
email: { $type: 'string' }
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
169
188
|
### Custom validation rules
|
|
170
189
|
|
|
171
190
|
You are able to define custom validation rules to use. (`this` will refer to the data object passed in)
|
package/lib/model-crud.js
CHANGED
|
@@ -2,15 +2,15 @@ let util = require('./util')
|
|
|
2
2
|
|
|
3
3
|
module.exports = {
|
|
4
4
|
|
|
5
|
-
insert: function(opts, cb) {
|
|
5
|
+
insert: async function(opts, cb) {
|
|
6
6
|
/**
|
|
7
7
|
* Inserts document(s) with monk after validating data & before hooks.
|
|
8
8
|
* @param {object} opts
|
|
9
9
|
* @param {object|array} <opts.data> - documents to insert
|
|
10
10
|
* @param {array|string|false} <opts.blacklist> - augment schema.insertBL, `false` will remove all blacklisting
|
|
11
11
|
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
|
|
12
|
-
* @param {array|string|false} validateUndefined -
|
|
13
|
-
*
|
|
12
|
+
* @param {array|string|false} validateUndefined - validates all 'required' undefined fields, true by
|
|
13
|
+
* default, but false on update
|
|
14
14
|
* @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
|
|
15
15
|
* @param {boolean} <opts.timestamps> - whether `createdAt` and `updatedAt` are automatically inserted
|
|
16
16
|
* @param {any} <opts.any> - any mongodb option
|
|
@@ -18,44 +18,36 @@ module.exports = {
|
|
|
18
18
|
* @this model
|
|
19
19
|
* @return promise
|
|
20
20
|
*/
|
|
21
|
-
opts = opts || {}
|
|
22
|
-
opts.insert = true
|
|
23
|
-
opts.model = this
|
|
24
|
-
let req = opts.req
|
|
25
|
-
let data = opts.data = util.isDefined(opts.data)? opts.data : util.isDefined((req||{}).body) ? req.body : {}
|
|
26
|
-
let options = util.omit(opts, [
|
|
27
|
-
'data', 'insert', 'model', 'respond', 'validateUndefined', 'skipValidation', 'blacklist'
|
|
28
|
-
])
|
|
29
21
|
if (cb && !util.isFunction(cb)) {
|
|
30
22
|
throw new Error(`The callback passed to ${this.name}.insert() is not a function`)
|
|
31
23
|
}
|
|
32
|
-
|
|
33
|
-
opts
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (cb) cb(null,
|
|
48
|
-
else if (opts.req && opts.respond) opts.req.res.json(
|
|
49
|
-
else return Promise.resolve(
|
|
50
|
-
|
|
51
|
-
}
|
|
24
|
+
try {
|
|
25
|
+
opts = await this._queryObject(opts, 'insert')
|
|
26
|
+
let options = util.omit(opts, [
|
|
27
|
+
'data', 'insert', 'model', 'respond', 'validateUndefined', 'skipValidation', 'blacklist'
|
|
28
|
+
])
|
|
29
|
+
|
|
30
|
+
// Validate
|
|
31
|
+
opts.data = await this.validate(opts.data||{}, { ...opts })
|
|
32
|
+
|
|
33
|
+
// Insert
|
|
34
|
+
await util.runSeries(this.beforeInsert.map(f => f.bind(opts, opts.data)))
|
|
35
|
+
let response = await this._insert(opts.data, options)
|
|
36
|
+
await util.runSeries(this.afterInsert.map(f => f.bind(opts, response)))
|
|
37
|
+
|
|
38
|
+
// Success/error
|
|
39
|
+
if (cb) cb(null, response)
|
|
40
|
+
else if (opts.req && opts.respond) opts.req.res.json(response)
|
|
41
|
+
else return Promise.resolve(response)
|
|
42
|
+
|
|
43
|
+
} catch (err) {
|
|
52
44
|
if (cb) cb(err)
|
|
53
45
|
else if (opts && opts.req && opts.respond) opts.req.res.error(err)
|
|
54
46
|
else throw err
|
|
55
|
-
}
|
|
47
|
+
}
|
|
56
48
|
},
|
|
57
49
|
|
|
58
|
-
find: function(opts, cb, one) {
|
|
50
|
+
find: async function(opts, cb, one) {
|
|
59
51
|
/**
|
|
60
52
|
* Finds document(s) with monk, also auto populates
|
|
61
53
|
* @param {object} opts
|
|
@@ -69,19 +61,15 @@ module.exports = {
|
|
|
69
61
|
* @this model
|
|
70
62
|
* @return promise
|
|
71
63
|
*/
|
|
72
|
-
let options, lookups = []
|
|
73
64
|
if (cb && !util.isFunction(cb)) {
|
|
74
65
|
throw new Error(`The callback passed to ${this.name}.find() is not a function`)
|
|
75
66
|
}
|
|
76
|
-
|
|
77
|
-
opts = this._queryObject(opts)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
options = util.omit(opts, ['blacklist', 'one', 'populate', 'project', 'query', 'respond'])
|
|
81
|
-
options.sort = options.sort || { 'createdAt': -1 }
|
|
82
|
-
options.skip = Math.max(0, options.skip || 0)
|
|
83
|
-
options.limit = opts.one? 1 : parseInt(options.limit || this.manager.limit || 0)
|
|
67
|
+
try {
|
|
68
|
+
opts = await this._queryObject(opts, 'find', one)
|
|
69
|
+
let lookups = []
|
|
70
|
+
let options = util.omit(opts, ['blacklist', 'one', 'populate', 'project', 'query', 'respond'])
|
|
84
71
|
options.addFields = options.addFields || {}
|
|
72
|
+
|
|
85
73
|
// Project, or use blacklisting
|
|
86
74
|
if (opts.project) {
|
|
87
75
|
// Can be an inclusion or exclusion projection
|
|
@@ -105,14 +93,10 @@ module.exports = {
|
|
|
105
93
|
// options.projection.score = { $meta: 'textScore' }
|
|
106
94
|
// options.sort = { score: { $meta: 'textScore' }}
|
|
107
95
|
// }
|
|
108
|
-
// Sort string passed
|
|
109
|
-
if (util.isString(options.sort)) {
|
|
110
|
-
let name = (options.sort.match(/([a-z0-9-_]+)/) || [])[0]
|
|
111
|
-
let order = (options.sort.match(/:(-?[0-9])/) || [])[1]
|
|
112
|
-
options.sort = { [name]: parseInt(order || 1) }
|
|
113
|
-
}
|
|
114
96
|
// Wanting to populate?
|
|
115
|
-
if (opts.populate) {
|
|
97
|
+
if (!opts.populate) {
|
|
98
|
+
var response = await this[`_find${opts.one? 'One' : ''}`](opts.query, options)
|
|
99
|
+
} else {
|
|
116
100
|
loop: for (let item of opts.populate) {
|
|
117
101
|
let path = util.isObject(item)? item.as : item
|
|
118
102
|
// Blacklisted?
|
|
@@ -148,7 +132,7 @@ module.exports = {
|
|
|
148
132
|
}
|
|
149
133
|
// console.log(1, options.projection)
|
|
150
134
|
// console.log(2, lookups)
|
|
151
|
-
let
|
|
135
|
+
let aggregate = [
|
|
152
136
|
{ $match: opts.query },
|
|
153
137
|
{ $sort: options.sort },
|
|
154
138
|
{ $skip: options.skip },
|
|
@@ -157,49 +141,45 @@ module.exports = {
|
|
|
157
141
|
...util.isEmpty(options.addFields)? [] : [{ $addFields: options.addFields }],
|
|
158
142
|
{ $project: options.projection }
|
|
159
143
|
]
|
|
160
|
-
|
|
161
|
-
this.info('aggregate', JSON.stringify(
|
|
162
|
-
|
|
163
|
-
// Normal operation
|
|
164
|
-
} else {
|
|
165
|
-
operation = this[opts.one? '_findOne' : '_find'].bind(this._collection, opts.query, options)
|
|
144
|
+
response = await this._aggregate(aggregate)
|
|
145
|
+
this.info('aggregate', JSON.stringify(aggregate))
|
|
166
146
|
}
|
|
167
|
-
operation()
|
|
168
|
-
.then(data => resolve(data))
|
|
169
|
-
.catch(err => reject(err))
|
|
170
147
|
|
|
171
|
-
|
|
172
|
-
if (opts.one && util.isArray(data)) data = data[0]
|
|
148
|
+
if (opts.one && util.isArray(response)) response = response[0]
|
|
173
149
|
// (Not using) Project works with lookup.
|
|
174
150
|
// Remove blacklisted properties from joined models, because subpiplines with 'project' are slower
|
|
175
151
|
// if (opts.populate) this._depreciated_removeBlacklisted(data, opts.populate, options.projection)
|
|
176
|
-
|
|
152
|
+
response = await this._processAfterFind(response, options.projection, opts)
|
|
177
153
|
|
|
178
|
-
|
|
179
|
-
if (cb) cb(null,
|
|
180
|
-
else if (opts.req && opts.respond) opts.req.res.json(
|
|
181
|
-
else return Promise.resolve(
|
|
154
|
+
// Success
|
|
155
|
+
if (cb) cb(null, response)
|
|
156
|
+
else if (opts.req && opts.respond) opts.req.res.json(response)
|
|
157
|
+
else return Promise.resolve(response)
|
|
182
158
|
|
|
183
|
-
}
|
|
159
|
+
} catch (err) {
|
|
184
160
|
if (cb) cb(err)
|
|
185
161
|
else if (opts && opts.req && opts.respond) opts.req.res.error(err)
|
|
186
162
|
else throw err
|
|
187
|
-
}
|
|
163
|
+
}
|
|
188
164
|
},
|
|
189
165
|
|
|
190
|
-
findOne: function(opts, cb) {
|
|
166
|
+
findOne: async function(opts, cb) {
|
|
191
167
|
return this.find(opts, cb, true)
|
|
192
168
|
},
|
|
193
169
|
|
|
194
|
-
|
|
170
|
+
// findOneAndUpdate: function(opts, cb) {
|
|
171
|
+
// return this._findOneAndUpdate(opts, cb)
|
|
172
|
+
// },
|
|
173
|
+
|
|
174
|
+
update: async function(opts, cb) {
|
|
195
175
|
/**
|
|
196
176
|
* Updates document(s) with monk after validating data & before hooks.
|
|
197
177
|
* @param {object} opts
|
|
198
178
|
* @param {object} <opts.query> - mongodb query object
|
|
199
179
|
* @param {object|array} <opts.data> - mongodb document update object(s)
|
|
200
180
|
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
|
|
201
|
-
* @param {array|string|false} validateUndefined -
|
|
202
|
-
*
|
|
181
|
+
* @param {array|string|false} validateUndefined - validates all 'required' undefined fields, true by
|
|
182
|
+
* default, but false on update
|
|
203
183
|
* @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
|
|
204
184
|
* @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
|
|
205
185
|
* @param {array|string|false} <opts.blacklist> - augment schema.updateBL, `false` will remove all blacklisting
|
|
@@ -208,155 +188,140 @@ module.exports = {
|
|
|
208
188
|
* @this model
|
|
209
189
|
* @return promise(data)
|
|
210
190
|
*/
|
|
211
|
-
let data, options, operators
|
|
212
191
|
if (cb && !util.isFunction(cb)) {
|
|
213
192
|
throw new Error(`The callback passed to ${this.name}.update() is not a function`)
|
|
214
193
|
}
|
|
215
|
-
|
|
216
|
-
opts = this._queryObject(opts)
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
let
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
options.limit = parseInt(options.limit || 0)
|
|
226
|
-
// Sort string passed
|
|
227
|
-
if (util.isString(options.sort)) {
|
|
228
|
-
let name = (options.sort.match(/([a-z0-9-_]+)/) || [])[0]
|
|
229
|
-
let order = (options.sort.match(/:(-?[0-9])/) || [])[1]
|
|
230
|
-
options.sort = { [name]: parseInt(order || 1) }
|
|
194
|
+
try {
|
|
195
|
+
opts = await this._queryObject(opts, 'update')
|
|
196
|
+
let response = null
|
|
197
|
+
let operators = util.pluck(opts, [/^\$/])
|
|
198
|
+
let options = util.omit(opts, ['data', 'query', 'respond', 'validateUndefined', 'skipValidation', 'blacklist'])
|
|
199
|
+
|
|
200
|
+
// Validate
|
|
201
|
+
if (util.isDefined(opts.data)) opts.data = await this.validate(opts.data, { ...opts })
|
|
202
|
+
if (!util.isDefined(opts.data) && util.isEmpty(operators)) {
|
|
203
|
+
throw new Error(`Please pass an update operator to ${this.name}.update(), e.g. data, $unset, etc`)
|
|
231
204
|
}
|
|
232
|
-
util.
|
|
233
|
-
|
|
234
|
-
}).then(data => {
|
|
235
|
-
opts.data = data
|
|
236
|
-
if (util.isDefined(data)) return this.validate(data, { ...opts })
|
|
237
|
-
else return Promise.resolve(data)
|
|
238
|
-
|
|
239
|
-
}).then(data => {
|
|
240
|
-
if (util.isDefined(data) && (!data || util.isEmpty(data))) {
|
|
205
|
+
if (util.isDefined(opts.data) && (!opts.data || util.isEmpty(opts.data))) {
|
|
241
206
|
throw new Error(`No valid data passed to ${this.name}.update({ data: .. })`)
|
|
242
207
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
return util.runSeries(this.beforeUpdate.map(f => f.bind(opts, data||{}))).then(() => data)
|
|
247
|
-
|
|
248
|
-
}).then(data => {
|
|
249
|
-
if (data && operators['$set']) {
|
|
208
|
+
// Hook: beforeUpdate
|
|
209
|
+
await util.runSeries(this.beforeUpdate.map(f => f.bind(opts, opts.data||{})))
|
|
210
|
+
if (opts.data && operators['$set']) {
|
|
250
211
|
this.warn(`'$set' fields take precedence over the data fields for \`${this.name}.update()\``)
|
|
251
212
|
}
|
|
252
|
-
if (data || operators['$set']) {
|
|
253
|
-
operators['$set'] = {
|
|
254
|
-
...data,
|
|
255
|
-
...(operators['$set'] || {}),
|
|
256
|
-
}
|
|
213
|
+
if (opts.data || operators['$set']) {
|
|
214
|
+
operators['$set'] = { ...opts.data, ...(operators['$set'] || {}) }
|
|
257
215
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
output = data = null // Cleanup (just incase)
|
|
262
|
-
return response
|
|
263
|
-
})
|
|
216
|
+
// Update
|
|
217
|
+
let update = await this._update(opts.query, operators, options)
|
|
218
|
+
if (update.n) response = Object.assign(Object.create({ _output: update }), operators['$set']||{})
|
|
264
219
|
|
|
265
|
-
|
|
266
|
-
if (
|
|
267
|
-
return util.runSeries(this.afterUpdate.map(f => f.bind(opts, data))).then(() => data)
|
|
220
|
+
// Hook: afterUpdate
|
|
221
|
+
if (update.n) await util.runSeries(this.afterUpdate.map(f => f.bind(opts, response)))
|
|
268
222
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (
|
|
272
|
-
else
|
|
273
|
-
else return Promise.resolve(data)
|
|
223
|
+
// Success
|
|
224
|
+
if (cb) cb(null, response)
|
|
225
|
+
else if (opts.req && opts.respond) opts.req.res.json(response)
|
|
226
|
+
else return response
|
|
274
227
|
|
|
275
|
-
}
|
|
228
|
+
} catch (err) {
|
|
276
229
|
if (cb) cb(err)
|
|
277
230
|
else if (opts && opts.req && opts.respond) opts.req.res.error(err)
|
|
278
231
|
else throw err
|
|
279
|
-
}
|
|
232
|
+
}
|
|
280
233
|
},
|
|
281
234
|
|
|
282
|
-
remove: function(opts, cb) {
|
|
235
|
+
remove: async function(opts, cb) {
|
|
283
236
|
/**
|
|
284
237
|
* Remove document(s) with monk after before hooks.
|
|
285
238
|
* @param {object} opts
|
|
286
239
|
* @param {object} <opts.query> - mongodb query object
|
|
287
240
|
* @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
|
|
241
|
+
* @param {boolean=true} <opts.multi> - set to false to limit the deletion to just one document
|
|
288
242
|
* @param {any} <opts.any> - any mongodb option
|
|
289
243
|
* @param {function} <cb> - execute cb(err, data) instead of responding
|
|
290
244
|
* @this model
|
|
291
245
|
* @return promise
|
|
292
246
|
*/
|
|
293
|
-
let options
|
|
294
247
|
if (cb && !util.isFunction(cb)) {
|
|
295
248
|
throw new Error(`The callback passed to ${this.name}.remove() is not a function`)
|
|
296
249
|
}
|
|
297
|
-
|
|
298
|
-
opts = this._queryObject(opts)
|
|
299
|
-
|
|
300
|
-
opts.model = this
|
|
250
|
+
try {
|
|
251
|
+
opts = await this._queryObject(opts, 'remove')
|
|
252
|
+
let options = util.omit(opts, ['query', 'respond'])
|
|
301
253
|
if (util.isEmpty(opts.query)) throw new Error('Please specify opts.query')
|
|
302
|
-
// Operation options
|
|
303
|
-
options = util.omit(opts, ['query', 'respond'])
|
|
304
|
-
options.sort = options.sort || { 'createdAt': -1 }
|
|
305
|
-
options.limit = parseInt(options.limit || 1)
|
|
306
|
-
// Sort string passed
|
|
307
|
-
if (util.isString(options.sort)) {
|
|
308
|
-
let name = (options.sort.match(/([a-z0-9-_]+)/) || [])[0]
|
|
309
|
-
let order = (options.sort.match(/:(-?[0-9])/) || [])[1]
|
|
310
|
-
options.sort = { [name]: parseInt(order || 1) }
|
|
311
|
-
}
|
|
312
|
-
util.runSeries(this.beforeRemove.map(f => f.bind(opts)))
|
|
313
|
-
.then(() => resolve())
|
|
314
|
-
.catch(e => reject(e))
|
|
315
|
-
|
|
316
|
-
}).then(() => {
|
|
317
|
-
return this._remove(opts.query, options)
|
|
318
254
|
|
|
319
|
-
|
|
320
|
-
|
|
255
|
+
// Remove
|
|
256
|
+
await util.runSeries(this.beforeRemove.map(f => f.bind(opts)))
|
|
257
|
+
let response = await this._remove(opts.query, options)
|
|
258
|
+
await util.runSeries(this.afterRemove.map(f => f.bind(response)))
|
|
321
259
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if (
|
|
325
|
-
else
|
|
326
|
-
else return Promise.resolve(data)
|
|
260
|
+
// Success
|
|
261
|
+
if (cb) cb(null, response)
|
|
262
|
+
else if (opts.req && opts.respond) opts.req.res.json(response)
|
|
263
|
+
else return Promise.resolve(response)
|
|
327
264
|
|
|
328
|
-
}
|
|
265
|
+
} catch (err) {
|
|
329
266
|
if (cb) cb(err)
|
|
330
267
|
else if (opts && opts.req && opts.respond) opts.req.res.error(err)
|
|
331
268
|
else throw err
|
|
332
|
-
}
|
|
269
|
+
}
|
|
333
270
|
},
|
|
334
271
|
|
|
335
|
-
_queryObject: function(opts) {
|
|
272
|
+
_queryObject: async function(opts, type, one) {
|
|
336
273
|
/**
|
|
337
|
-
*
|
|
338
|
-
* @param {MongoId|
|
|
339
|
-
* @
|
|
274
|
+
* Normalise options
|
|
275
|
+
* @param {MongoId|string|object} opts
|
|
276
|
+
* @param {string} type - operation type
|
|
277
|
+
* @param {boolean} one - return one document
|
|
278
|
+
* @this model
|
|
279
|
+
* @return {Promise} opts
|
|
340
280
|
*
|
|
341
|
-
* opts == string|
|
|
281
|
+
* opts == string|MongodId - treated as an id
|
|
342
282
|
* opts == undefined|null|false - throw error
|
|
343
283
|
* opts.query == string|MongodID - treated as an id
|
|
344
284
|
* opts.query == undefined|null|false - throw error
|
|
345
285
|
*/
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
if (
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
286
|
+
|
|
287
|
+
// Query
|
|
288
|
+
if (type != 'insert') {
|
|
289
|
+
let isIdType = (o) => util.isId(o) || util.isString(o)
|
|
290
|
+
if (isIdType(opts)) {
|
|
291
|
+
opts = { query: { _id: opts || '' }}
|
|
292
|
+
}
|
|
293
|
+
if (isIdType((opts||{}).query)) {
|
|
294
|
+
opts.query = { _id: opts.query || '' }
|
|
295
|
+
}
|
|
296
|
+
if (!util.isObject(opts) || !util.isObject(opts.query)) {
|
|
297
|
+
throw new Error('Please pass an object or MongoId to options.query')
|
|
298
|
+
}
|
|
299
|
+
// For security, if _id is set and undefined, throw an error
|
|
300
|
+
if (typeof opts.query._id == 'undefined' && opts.query.hasOwnProperty('_id')) {
|
|
301
|
+
throw new Error('Please pass an object or MongoId to options.query')
|
|
302
|
+
}
|
|
303
|
+
if (util.isId(opts.query._id)) opts.query._id = this.manager.id(opts.query._id)
|
|
304
|
+
if (isIdType(opts.query._id) || one) opts.one = true
|
|
305
|
+
opts.query = util.removeUndefined(opts.query)
|
|
306
|
+
|
|
307
|
+
// Query options
|
|
308
|
+
opts.limit = opts.one? 1 : parseInt(opts.limit || this.manager.limit || 0)
|
|
309
|
+
opts.skip = Math.max(0, opts.skip || 0)
|
|
310
|
+
opts.sort = opts.sort || { 'createdAt': -1 }
|
|
311
|
+
if (util.isString(opts.sort)) {
|
|
312
|
+
let name = (opts.sort.match(/([a-z0-9-_]+)/) || [])[0]
|
|
313
|
+
let order = (opts.sort.match(/:(-?[0-9])/) || [])[1]
|
|
314
|
+
opts.sort = { [name]: parseInt(order || 1) }
|
|
315
|
+
}
|
|
355
316
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (
|
|
359
|
-
if (
|
|
317
|
+
|
|
318
|
+
// Data
|
|
319
|
+
if (!opts) opts = {}
|
|
320
|
+
if (!util.isDefined(opts.data) && util.isDefined((opts.req||{}).body)) opts.data = opts.req.body
|
|
321
|
+
if (util.isDefined(opts.data)) opts.data = await util.parseData(opts.data)
|
|
322
|
+
|
|
323
|
+
opts[type] = true
|
|
324
|
+
opts.model = this
|
|
360
325
|
return opts
|
|
361
326
|
},
|
|
362
327
|
|
package/lib/model-validate.js
CHANGED
|
@@ -12,8 +12,8 @@ module.exports = {
|
|
|
12
12
|
* @param {boolean(false)} update - are we validating for insert or update?
|
|
13
13
|
* @param {array|string|false} blacklist - augment schema blacklist, `false` will remove all blacklisting
|
|
14
14
|
* @param {array|string} projection - only return these fields, ignores blacklist
|
|
15
|
-
* @param {array|string|false} validateUndefined -
|
|
16
|
-
*
|
|
15
|
+
* @param {array|string|false} validateUndefined - validates all 'required' undefined fields, true by
|
|
16
|
+
* default, but false on update
|
|
17
17
|
* @param {array|string|true} skipValidation - skip validation on these fields
|
|
18
18
|
* @param {boolean} timestamps - whether `createdAt` and `updatedAt` are inserted, or `updatedAt` is
|
|
19
19
|
* updated, depending on the `options.update` value
|
|
@@ -176,11 +176,11 @@ module.exports = {
|
|
|
176
176
|
} else if (util.isSubdocument(field)) {
|
|
177
177
|
// Object schema errors
|
|
178
178
|
errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path2)))
|
|
179
|
-
// Recurse if
|
|
179
|
+
// Recurse if inserting, value is a subdocument, or a deep property (todo: not dot-notation)
|
|
180
180
|
if (
|
|
181
|
-
util.isObject(value) ||
|
|
182
181
|
opts.insert ||
|
|
183
|
-
|
|
182
|
+
util.isObject(value) ||
|
|
183
|
+
(util.isDefined(opts.validateUndefined) ? opts.validateUndefined : (path2||'').match(/\./))
|
|
184
184
|
) {
|
|
185
185
|
var res = this._validateFields(dataRoot, field, value, opts, path2)
|
|
186
186
|
errors.push(...res[0])
|
|
@@ -260,13 +260,14 @@ module.exports = {
|
|
|
260
260
|
ruleArg = ruleArg === true? undefined : ruleArg
|
|
261
261
|
let rule = this.rules[ruleName] || rules[ruleName]
|
|
262
262
|
let fieldName = path.match(/[^.]+$/)[0]
|
|
263
|
+
let isDeepProp = path.match(/\./) // todo: not dot-notation
|
|
263
264
|
let ruleMessageKey = this._getMostSpecificKeyMatchingPath(this.messages, path)
|
|
264
265
|
let ruleMessage = ruleMessageKey && this.messages[ruleMessageKey][ruleName]
|
|
265
|
-
let validateUndefined = util.isDefined(opts.validateUndefined) ? opts.validateUndefined :
|
|
266
|
+
let validateUndefined = util.isDefined(opts.validateUndefined) ? opts.validateUndefined : opts.insert || isDeepProp
|
|
266
267
|
if (!ruleMessage) ruleMessage = rule.message
|
|
267
268
|
|
|
268
|
-
//
|
|
269
|
-
if (typeof value === 'undefined' && (!validateUndefined
|
|
269
|
+
// Undefined value
|
|
270
|
+
if (typeof value === 'undefined' && (!validateUndefined || !rule.validateUndefined)) return
|
|
270
271
|
|
|
271
272
|
// Ignore null (if nullObject is set on objects or arrays)
|
|
272
273
|
if (value === null && (field.isObject || field.isArray) && field.nullObject) return
|
|
@@ -289,9 +290,9 @@ module.exports = {
|
|
|
289
290
|
},
|
|
290
291
|
|
|
291
292
|
_ignoredRules: [ // todo: change name? i.e. 'specialFields'
|
|
292
|
-
// Need to remove
|
|
293
|
-
'default', 'defaultOverride', '
|
|
294
|
-
'nullObject', 'timestampField', 'type', 'virtual'
|
|
293
|
+
// Need to remove filesize and formats..
|
|
294
|
+
'default', 'defaultOverride', 'filename', 'filesize', 'formats', 'image', 'index', 'insertOnly',
|
|
295
|
+
'model', 'nullObject', 'params', 'getSignedUrl', 'timestampField', 'type', 'virtual'
|
|
295
296
|
]
|
|
296
297
|
|
|
297
298
|
}
|
package/lib/model.js
CHANGED
|
@@ -188,9 +188,12 @@ Model.prototype._setupFields = function(fields) {
|
|
|
188
188
|
|
|
189
189
|
// Rule doesn't exist
|
|
190
190
|
util.forEach(field, (rule, ruleName) => {
|
|
191
|
+
if ((this.rules[ruleName] || rules[ruleName]) && this._ignoredRules.indexOf(ruleName) != -1) {
|
|
192
|
+
this.error(`The rule name "${ruleName}" for the model "${this.name}" is a reserved keyword, ignoring rule.`)
|
|
193
|
+
}
|
|
191
194
|
if (!this.rules[ruleName] && !rules[ruleName] && this._ignoredRules.indexOf(ruleName) == -1) {
|
|
192
195
|
// console.log(field)
|
|
193
|
-
this.error(`No rule "${ruleName}" exists for model "${this.name}"
|
|
196
|
+
this.error(`No rule "${ruleName}" exists for model "${this.name}", ignoring rule.`)
|
|
194
197
|
delete field[ruleName]
|
|
195
198
|
}
|
|
196
199
|
}, this)
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "monastery",
|
|
3
3
|
"description": "⛪ A straight forward MongoDB ODM built around Monk",
|
|
4
4
|
"author": "Ricky Boyce",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.32.0",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": "github:boycce/monastery",
|
|
8
8
|
"homepage": "https://boycce.github.io/monastery/",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"mong": "nodemon resources/mong.js",
|
|
24
24
|
"minor": "standard-version --release-as minor && npm publish",
|
|
25
25
|
"patch": "standard-version --release-as patch && npm publish",
|
|
26
|
-
"release": "standard-version && npm publish",
|
|
26
|
+
"release": "standard-version && npm publish && git push --tags",
|
|
27
27
|
"test": "npm run lint && jest",
|
|
28
28
|
"test-one-example": "jest -t images"
|
|
29
29
|
},
|
package/plugins/images/index.js
CHANGED
|
@@ -32,7 +32,9 @@ let plugin = module.exports = {
|
|
|
32
32
|
return
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
// Create s3 service instance
|
|
35
|
+
// Create s3 'service' instance
|
|
36
|
+
// https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#constructor-property
|
|
37
|
+
manager.getSignedUrl = this._getSignedUrl
|
|
36
38
|
this.s3 = new S3({
|
|
37
39
|
credentials: {
|
|
38
40
|
accessKeyId: this.awsAccessKeyId,
|
|
@@ -71,6 +73,9 @@ let plugin = module.exports = {
|
|
|
71
73
|
model.afterInsert.push(function(data, n) {
|
|
72
74
|
plugin.addImages(this, data).then(() => n(null, data)).catch(e => n(e))
|
|
73
75
|
})
|
|
76
|
+
model.afterFind.push(function(data, n) {
|
|
77
|
+
plugin.getSignedUrls(this, data).then(() => n(null, data)).catch(e => n(e))
|
|
78
|
+
})
|
|
74
79
|
}
|
|
75
80
|
},
|
|
76
81
|
|
|
@@ -121,14 +126,15 @@ let plugin = module.exports = {
|
|
|
121
126
|
return Promise.all(filesArr.map(file => {
|
|
122
127
|
return new Promise((resolve, reject) => {
|
|
123
128
|
let uid = nanoid.nanoid()
|
|
129
|
+
let pathFilename = filesArr.imageField.filename ? '/' + filesArr.imageField.filename : ''
|
|
124
130
|
let image = {
|
|
125
131
|
bucket: this.awsBucket,
|
|
126
132
|
date: this.manager.useMilliseconds? Date.now() : Math.floor(Date.now() / 1000),
|
|
127
133
|
filename: file.name,
|
|
128
134
|
filesize: file.size,
|
|
129
|
-
path: `${plugin.bucketDir}/${uid}.${file.ext}`,
|
|
135
|
+
path: `${plugin.bucketDir}/${uid}${pathFilename}.${file.ext}`,
|
|
130
136
|
// sizes: ['large', 'medium', 'small'],
|
|
131
|
-
uid: uid
|
|
137
|
+
uid: uid,
|
|
132
138
|
}
|
|
133
139
|
this.manager.info(
|
|
134
140
|
`Uploading '${image.filename}' to '${image.bucket}/${image.path}'`
|
|
@@ -141,7 +147,11 @@ let plugin = module.exports = {
|
|
|
141
147
|
Bucket: this.awsBucket,
|
|
142
148
|
Key: image.path,
|
|
143
149
|
Body: file.data,
|
|
144
|
-
|
|
150
|
+
// The IAM permission "s3:PutObjectACL" must be included in the appropriate policy
|
|
151
|
+
ACL: 'public-read',
|
|
152
|
+
// upload params,https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#upload-property
|
|
153
|
+
...filesArr.imageField.params,
|
|
154
|
+
|
|
145
155
|
}, (err, response) => {
|
|
146
156
|
if (err) return reject(err)
|
|
147
157
|
plugin._addImageObjectsToData(filesArr.inputPath, data, image)
|
|
@@ -161,7 +171,7 @@ let plugin = module.exports = {
|
|
|
161
171
|
return model._update(
|
|
162
172
|
idquery,
|
|
163
173
|
{ '$set': prunedData },
|
|
164
|
-
{ 'multi': options.multi || options.create }
|
|
174
|
+
{ 'multi': options.multi || options.create },
|
|
165
175
|
)
|
|
166
176
|
|
|
167
177
|
// If errors, remove inserted documents to prevent double ups when the user resaves.
|
|
@@ -172,6 +182,23 @@ let plugin = module.exports = {
|
|
|
172
182
|
})
|
|
173
183
|
},
|
|
174
184
|
|
|
185
|
+
getSignedUrls: async function(options, data) {
|
|
186
|
+
// Not wanting signed urls for this operation?
|
|
187
|
+
if (util.isDefined(options.getSignedUrls) && !options.getSignedUrls) return
|
|
188
|
+
|
|
189
|
+
// Find all image objects in data
|
|
190
|
+
for (let doc of util.toArray(data)) {
|
|
191
|
+
for (let imageField of options.model.imageFields) {
|
|
192
|
+
if (options.getSignedUrls || imageField.getSignedUrl) {
|
|
193
|
+
let images = plugin._findImagesInData(doc, imageField, 0, '').filter(o => o.image)
|
|
194
|
+
for (let image of images) {
|
|
195
|
+
image.image.signedUrl = this._getSignedUrl(image.image.path)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
|
|
175
202
|
keepImagePlacement: async function(options, data) {
|
|
176
203
|
/**
|
|
177
204
|
* Hook before update/remove
|
|
@@ -376,7 +403,7 @@ let plugin = module.exports = {
|
|
|
376
403
|
return Promise.all(filesArr.map((file, i) => {
|
|
377
404
|
return new Promise((resolve, reject) => {
|
|
378
405
|
fileType.fromBuffer(file.data).then(res => {
|
|
379
|
-
let maxSize = filesArr.imageField.
|
|
406
|
+
let maxSize = filesArr.imageField.filesize
|
|
380
407
|
let formats = filesArr.imageField.formats || plugin.formats
|
|
381
408
|
let allowAny = util.inArray(formats, 'any')
|
|
382
409
|
file.format = res? res.ext : ''
|
|
@@ -431,7 +458,10 @@ let plugin = module.exports = {
|
|
|
431
458
|
// Image field. Test for field.image as field.type may be 'any'
|
|
432
459
|
} else if (field.type == 'image' || field.image) {
|
|
433
460
|
let formats = field.formats
|
|
434
|
-
let
|
|
461
|
+
let filesize = field.filesize || field.fileSize // old <= v1.31.7
|
|
462
|
+
let filename = field.filename
|
|
463
|
+
let getSignedUrl = field.getSignedUrl
|
|
464
|
+
let params = { ...field.params||{} }
|
|
435
465
|
// Convert image field to subdocument
|
|
436
466
|
fields[fieldName] = {
|
|
437
467
|
bucket: { type: 'string' },
|
|
@@ -440,13 +470,16 @@ let plugin = module.exports = {
|
|
|
440
470
|
filesize: { type: 'number' },
|
|
441
471
|
path: { type: 'string' },
|
|
442
472
|
schema: { image: true, nullObject: true, isImageObject: true },
|
|
443
|
-
uid: { type: 'string' }
|
|
473
|
+
uid: { type: 'string' },
|
|
444
474
|
}
|
|
445
475
|
list.push({
|
|
446
476
|
fullPath: path2,
|
|
447
477
|
fullPathRegex: new RegExp('^' + path2.replace(/\.[0-9]+/g, '.[0-9]+').replace(/\./g, '\\.') + '$'),
|
|
448
478
|
formats: formats,
|
|
449
|
-
|
|
479
|
+
filesize: filesize,
|
|
480
|
+
filename: filename,
|
|
481
|
+
getSignedUrl: getSignedUrl,
|
|
482
|
+
params: params,
|
|
450
483
|
})
|
|
451
484
|
}
|
|
452
485
|
})
|
|
@@ -495,6 +528,16 @@ let plugin = module.exports = {
|
|
|
495
528
|
}
|
|
496
529
|
|
|
497
530
|
return list
|
|
498
|
-
}
|
|
531
|
+
},
|
|
532
|
+
|
|
533
|
+
_getSignedUrl: (path, expires=3600) => {
|
|
534
|
+
// https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getSignedUrl-property
|
|
535
|
+
let signedUrl = plugin.s3.getSignedUrl('getObject', {
|
|
536
|
+
Bucket: plugin.awsBucket,
|
|
537
|
+
Key: path,
|
|
538
|
+
Expires: expires
|
|
539
|
+
})
|
|
540
|
+
return signedUrl
|
|
541
|
+
},
|
|
499
542
|
|
|
500
543
|
}
|
package/test/crud.js
CHANGED
|
@@ -415,6 +415,40 @@ module.exports = function(monastery, opendb) {
|
|
|
415
415
|
db.close()
|
|
416
416
|
})
|
|
417
417
|
|
|
418
|
+
test('remove basics', async () => {
|
|
419
|
+
let db = (await opendb(null)).db
|
|
420
|
+
let user = db.model('user', {
|
|
421
|
+
fields: {
|
|
422
|
+
name: { type: 'string' },
|
|
423
|
+
},
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
// Insert multiple
|
|
427
|
+
let inserted2 = await user.insert({ data: [{ name: 'Martin' }, { name: 'Martin' }, { name: 'Martin' }]})
|
|
428
|
+
expect(inserted2).toEqual([
|
|
429
|
+
{
|
|
430
|
+
_id: expect.any(Object),
|
|
431
|
+
name: 'Martin'
|
|
432
|
+
}, {
|
|
433
|
+
_id: expect.any(Object),
|
|
434
|
+
name: 'Martin'
|
|
435
|
+
}, {
|
|
436
|
+
_id: expect.any(Object),
|
|
437
|
+
name: 'Martin'
|
|
438
|
+
}
|
|
439
|
+
])
|
|
440
|
+
|
|
441
|
+
// Remove one
|
|
442
|
+
await expect(user.remove({ query: { name: 'Martin' }, multi: false }))
|
|
443
|
+
.resolves.toMatchObject({ deletedCount: 1, result: { n: 1, ok: 1 }})
|
|
444
|
+
|
|
445
|
+
// Remove many (default)
|
|
446
|
+
await expect(user.remove({ query: { name: 'Martin' } }))
|
|
447
|
+
.resolves.toMatchObject({ deletedCount: 2, result: { n: 2, ok: 1 }})
|
|
448
|
+
|
|
449
|
+
db.close()
|
|
450
|
+
})
|
|
451
|
+
|
|
418
452
|
test('hooks', async () => {
|
|
419
453
|
let db = (await opendb(null)).db
|
|
420
454
|
let user = db.model('user', {
|
package/test/model.js
CHANGED
|
@@ -115,6 +115,29 @@ module.exports = function(monastery, opendb) {
|
|
|
115
115
|
})
|
|
116
116
|
})
|
|
117
117
|
|
|
118
|
+
test('model reserved rules', async () => {
|
|
119
|
+
// Setup
|
|
120
|
+
let db = (await opendb(false, {})).db
|
|
121
|
+
db.error = () => {} // hiding debug error
|
|
122
|
+
let user = db.model('user', {
|
|
123
|
+
fields: {
|
|
124
|
+
name: {
|
|
125
|
+
type: 'string',
|
|
126
|
+
params: {}, // reserved keyword (image plugin)
|
|
127
|
+
paramsUnreserved: {}
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
rules: {
|
|
131
|
+
params: (value) => {
|
|
132
|
+
return false // shouldn'r run
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
await expect(user.validate({ name: 'Martin' })).resolves.toMatchObject({
|
|
137
|
+
name: 'Martin',
|
|
138
|
+
})
|
|
139
|
+
})
|
|
140
|
+
|
|
118
141
|
test('model indexes', async () => {
|
|
119
142
|
// Setup: Need to test different types of indexes
|
|
120
143
|
let db = (await opendb(null)).db
|
|
@@ -189,6 +212,55 @@ module.exports = function(monastery, opendb) {
|
|
|
189
212
|
db.close()
|
|
190
213
|
})
|
|
191
214
|
|
|
215
|
+
test('model unique indexes', async () => {
|
|
216
|
+
let db = (await opendb(null)).db
|
|
217
|
+
// Setup: Drop previously tested collections
|
|
218
|
+
if ((await db._db.listCollections().toArray()).find(o => o.name == 'userUniqueIndex')) {
|
|
219
|
+
await db._db.collection('userUniqueIndex').drop()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Partial unique indexes (allows mulitple null values)
|
|
223
|
+
await db.model('userUniqueIndex', {
|
|
224
|
+
waitForIndexes: true,
|
|
225
|
+
fields: {
|
|
226
|
+
email: {
|
|
227
|
+
type: 'string',
|
|
228
|
+
index: {
|
|
229
|
+
type: 'unique',
|
|
230
|
+
partialFilterExpression: {
|
|
231
|
+
email: { $type: 'string' }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
},
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
let indexes2 = await db._db.collection('userUniqueIndex').indexes()
|
|
239
|
+
expect(indexes2[0]).toMatchObject({ v: 2, key: { _id: 1 }, name: '_id_' })
|
|
240
|
+
expect(indexes2[1]).toMatchObject({ v: 2, unique: true, key: { email: 1 }, name: 'email_1' })
|
|
241
|
+
|
|
242
|
+
await expect(db.userUniqueIndex.insert({ data: { 'email': 'ricky@orchid.co.nz' }})).resolves.toEqual({
|
|
243
|
+
_id: expect.any(Object),
|
|
244
|
+
email: 'ricky@orchid.co.nz'
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
await expect(db.userUniqueIndex.insert({ data: { 'email': 'ricky@orchid.co.nz' }})).rejects.toThrow(
|
|
248
|
+
/E11000 duplicate key error collection: monastery.userUniqueIndex index: email_1 dup key: {/
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
await expect(db.userUniqueIndex.insert({ data: { 'email': null }})).resolves.toEqual({
|
|
252
|
+
_id: expect.any(Object),
|
|
253
|
+
email: null
|
|
254
|
+
})
|
|
255
|
+
|
|
256
|
+
await expect(db.userUniqueIndex.insert({ data: { 'email': null }})).resolves.toEqual({
|
|
257
|
+
_id: expect.any(Object),
|
|
258
|
+
email: null
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
db.close()
|
|
262
|
+
})
|
|
263
|
+
|
|
192
264
|
test('model subdocument indexes', async () => {
|
|
193
265
|
// Setup: Need to test different types of indexes
|
|
194
266
|
let db = (await opendb(null)).db
|
package/test/plugin-images.js
CHANGED
|
@@ -707,4 +707,59 @@ module.exports = function(monastery, opendb) {
|
|
|
707
707
|
db.close()
|
|
708
708
|
})
|
|
709
709
|
|
|
710
|
+
test('images getSignedUrls', async () => {
|
|
711
|
+
// latest (2022.02)
|
|
712
|
+
let db = (await opendb(null, {
|
|
713
|
+
timestamps: false,
|
|
714
|
+
serverSelectionTimeoutMS: 2000,
|
|
715
|
+
imagePlugin: { awsBucket: 'fake', awsAccessKeyId: 'fake', awsSecretAccessKey: 'fake' }
|
|
716
|
+
})).db
|
|
717
|
+
|
|
718
|
+
db.model('user', { fields: {
|
|
719
|
+
photos: [{ type: 'image' }],
|
|
720
|
+
photos2: [{ type: 'image', getSignedUrl: true }],
|
|
721
|
+
}})
|
|
722
|
+
|
|
723
|
+
let image = {
|
|
724
|
+
bucket: 'test',
|
|
725
|
+
date: 1234,
|
|
726
|
+
filename: 'lion1.png',
|
|
727
|
+
filesize: 1234,
|
|
728
|
+
path: 'test/lion1.png',
|
|
729
|
+
uid: 'lion1'
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
let userInserted = await db.user._insert({
|
|
733
|
+
photos: [image, image],
|
|
734
|
+
photos2: [image, image],
|
|
735
|
+
})
|
|
736
|
+
|
|
737
|
+
// Find signed URL
|
|
738
|
+
await expect(db.user.findOne({ query: userInserted._id, getSignedUrls: true })).resolves.toEqual({
|
|
739
|
+
_id: expect.any(Object),
|
|
740
|
+
photos: [
|
|
741
|
+
{ ...image, signedUrl: expect.stringMatching(/^https/) },
|
|
742
|
+
{ ...image, signedUrl: expect.stringMatching(/^https/) },
|
|
743
|
+
],
|
|
744
|
+
photos2: [
|
|
745
|
+
{ ...image, signedUrl: expect.stringMatching(/^https/) },
|
|
746
|
+
{ ...image, signedUrl: expect.stringMatching(/^https/) },
|
|
747
|
+
]
|
|
748
|
+
})
|
|
749
|
+
// Find signed URL
|
|
750
|
+
await expect(db.user.findOne({ query: userInserted._id })).resolves.toEqual({
|
|
751
|
+
_id: expect.any(Object),
|
|
752
|
+
photos: [
|
|
753
|
+
{ ...image },
|
|
754
|
+
{ ...image },
|
|
755
|
+
],
|
|
756
|
+
photos2: [
|
|
757
|
+
{ ...image, signedUrl: expect.stringMatching(/^https/) },
|
|
758
|
+
{ ...image, signedUrl: expect.stringMatching(/^https/) },
|
|
759
|
+
]
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
db.close()
|
|
763
|
+
})
|
|
764
|
+
|
|
710
765
|
}
|
package/test/validate.js
CHANGED
|
@@ -26,17 +26,18 @@ module.exports = function(monastery, opendb) {
|
|
|
26
26
|
meta: { rule: 'required', model: 'user', field: 'name' }
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
-
// Required error (insert, and with ignoreRequired)
|
|
30
|
-
await expect(user.validate({}, { validateUndefined: false })).resolves.toEqual({})
|
|
31
|
-
await expect(user.validate({}, { validateUndefined: false, update: true })).resolves.toEqual({})
|
|
32
|
-
|
|
33
29
|
// No required error (update)
|
|
34
30
|
await expect(user.validate({}, { update: true })).resolves.toEqual({})
|
|
35
31
|
|
|
36
32
|
// Type error (string)
|
|
37
33
|
await expect(user.validate({ name: 1 })).resolves.toEqual({ name: '1' })
|
|
38
34
|
await expect(user.validate({ name: 1.123 })).resolves.toEqual({ name: '1.123' })
|
|
39
|
-
await expect(user.validate({ name: undefined }
|
|
35
|
+
await expect(user.validate({ name: undefined })).rejects.toContainEqual({
|
|
36
|
+
status: '400',
|
|
37
|
+
title: 'name',
|
|
38
|
+
detail: 'This field is required.',
|
|
39
|
+
meta: { rule: 'required', model: 'user', field: 'name' }
|
|
40
|
+
})
|
|
40
41
|
await expect(user.validate({ name: null })).rejects.toContainEqual({
|
|
41
42
|
status: '400',
|
|
42
43
|
title: 'name',
|
|
@@ -65,20 +66,19 @@ module.exports = function(monastery, opendb) {
|
|
|
65
66
|
await expect(usernum.validate({ amount: 0 })).resolves.toEqual({ amount: 0 })
|
|
66
67
|
await expect(usernum.validate({ amount: '0' })).resolves.toEqual({ amount: 0 })
|
|
67
68
|
await expect(usernum2.validate({ amount: '' })).resolves.toEqual({ amount: null })
|
|
68
|
-
await expect(usernum.validate({ amount: undefined }, { validateUndefined: false })).resolves.toEqual({})
|
|
69
69
|
await expect(usernum.validate({ amount: false })).rejects.toEqual([{
|
|
70
70
|
status: '400',
|
|
71
71
|
title: 'amount',
|
|
72
72
|
detail: 'Value was not a number.',
|
|
73
73
|
meta: { rule: 'isNumber', model: 'usernum', field: 'amount' }
|
|
74
74
|
}])
|
|
75
|
-
await expect(usernum.validate({ amount:
|
|
75
|
+
await expect(usernum.validate({ amount: undefined })).rejects.toEqual([{
|
|
76
76
|
status: '400',
|
|
77
77
|
title: 'amount',
|
|
78
78
|
detail: 'This field is required.',
|
|
79
79
|
meta: { rule: 'required', model: 'usernum', field: 'amount' },
|
|
80
80
|
}])
|
|
81
|
-
await expect(usernum.validate({ amount: null }
|
|
81
|
+
await expect(usernum.validate({ amount: null })).rejects.toEqual([{
|
|
82
82
|
status: '400',
|
|
83
83
|
title: 'amount',
|
|
84
84
|
detail: 'This field is required.',
|
|
@@ -309,6 +309,41 @@ module.exports = function(monastery, opendb) {
|
|
|
309
309
|
.rejects.toContainEqual(error)
|
|
310
310
|
})
|
|
311
311
|
|
|
312
|
+
test('validation array schema errors', async () => {
|
|
313
|
+
// Setup
|
|
314
|
+
let db = (await opendb(false)).db
|
|
315
|
+
function arrayWithSchema(array, schema) {
|
|
316
|
+
array.schema = schema
|
|
317
|
+
return array
|
|
318
|
+
}
|
|
319
|
+
let user = db.model('user', { fields: {
|
|
320
|
+
animals: arrayWithSchema(
|
|
321
|
+
[{ type: 'string' }],
|
|
322
|
+
{ required: true, minLength: 2 },
|
|
323
|
+
)
|
|
324
|
+
}})
|
|
325
|
+
|
|
326
|
+
// MinLength error
|
|
327
|
+
await expect(user.validate({
|
|
328
|
+
animals: [],
|
|
329
|
+
})).rejects.toContainEqual({
|
|
330
|
+
status: '400',
|
|
331
|
+
title: 'animals',
|
|
332
|
+
detail: 'This field is required.',
|
|
333
|
+
meta: { rule: 'required', model: 'user', field: 'animals' }
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
// MinLength error
|
|
337
|
+
await expect(user.validate({
|
|
338
|
+
animals: ['dog'],
|
|
339
|
+
})).rejects.toContainEqual({
|
|
340
|
+
status: '400',
|
|
341
|
+
title: 'animals',
|
|
342
|
+
detail: 'Value needs to contain a minimum of 2 items.',
|
|
343
|
+
meta: { rule: 'minLength', model: 'user', field: 'animals' }
|
|
344
|
+
})
|
|
345
|
+
})
|
|
346
|
+
|
|
312
347
|
test('validation getMostSpecificKeyMatchingPath', async () => {
|
|
313
348
|
let fn = validate._getMostSpecificKeyMatchingPath
|
|
314
349
|
let mock = {
|
|
@@ -870,7 +905,7 @@ module.exports = function(monastery, opendb) {
|
|
|
870
905
|
db.close()
|
|
871
906
|
})
|
|
872
907
|
|
|
873
|
-
test('validation
|
|
908
|
+
test('validation option skipValidation', async () => {
|
|
874
909
|
let db = (await opendb(false)).db
|
|
875
910
|
let user = db.model('user', { fields: {
|
|
876
911
|
name: { type: 'string', required: true }
|
|
@@ -955,6 +990,66 @@ module.exports = function(monastery, opendb) {
|
|
|
955
990
|
})
|
|
956
991
|
})
|
|
957
992
|
|
|
993
|
+
test('validation option validateUndefined', async () => {
|
|
994
|
+
// ValidateUndefined runs required rules on all fields, `true` for insert, `false` for update.
|
|
995
|
+
|
|
996
|
+
// Setup
|
|
997
|
+
let db = (await opendb(false)).db
|
|
998
|
+
let user = db.model('user', { fields: {
|
|
999
|
+
date: { type: 'number' },
|
|
1000
|
+
name: { type: 'string', required: true },
|
|
1001
|
+
}})
|
|
1002
|
+
let usernum = db.model('usernum', { fields: {
|
|
1003
|
+
amount: { type: 'number', required: true }
|
|
1004
|
+
}})
|
|
1005
|
+
let userdeep = db.model('userdeep', { fields: {
|
|
1006
|
+
date: { type: 'number' },
|
|
1007
|
+
name: {
|
|
1008
|
+
first: { type: 'string', required: true },
|
|
1009
|
+
},
|
|
1010
|
+
names: [{
|
|
1011
|
+
first: { type: 'string', required: true },
|
|
1012
|
+
}]
|
|
1013
|
+
}})
|
|
1014
|
+
let errorRequired = {
|
|
1015
|
+
status: '400',
|
|
1016
|
+
title: 'name',
|
|
1017
|
+
detail: 'This field is required.',
|
|
1018
|
+
meta: expect.any(Object),
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// Required error for undefined
|
|
1022
|
+
await expect(user.validate({}))
|
|
1023
|
+
.rejects.toEqual([errorRequired])
|
|
1024
|
+
await expect(user.validate({}, { update: true, validateUndefined: true }))
|
|
1025
|
+
.rejects.toEqual([errorRequired])
|
|
1026
|
+
await expect(userdeep.validate({}))
|
|
1027
|
+
.rejects.toEqual([{ ...errorRequired, title: 'name.first' }])
|
|
1028
|
+
await expect(userdeep.validate({ name: {} }, { update: true }))
|
|
1029
|
+
.rejects.toEqual([{ ...errorRequired, title: 'name.first' }])
|
|
1030
|
+
await expect(userdeep.validate({ names: [{}] }, { update: true }))
|
|
1031
|
+
.rejects.toEqual([{ ...errorRequired, title: 'names.0.first' }])
|
|
1032
|
+
|
|
1033
|
+
// Required error for null
|
|
1034
|
+
await expect(user.validate({ name: null }, { update: true }))
|
|
1035
|
+
.rejects.toEqual([errorRequired])
|
|
1036
|
+
await expect(usernum.validate({ amount: null }, { update: true }))
|
|
1037
|
+
.rejects.toEqual([{ ...errorRequired, title: 'amount' }])
|
|
1038
|
+
await expect(user.validate({ name: null }, { update: true, validateUndefined: true }))
|
|
1039
|
+
.rejects.toEqual([errorRequired])
|
|
1040
|
+
|
|
1041
|
+
// Skip required error
|
|
1042
|
+
await expect(user.validate({ name: undefined }, { validateUndefined: false })).resolves.toEqual({})
|
|
1043
|
+
await expect(user.validate({}, { validateUndefined: false })).resolves.toEqual({})
|
|
1044
|
+
await expect(user.validate({}, { update: true })).resolves.toEqual({})
|
|
1045
|
+
await expect(user.validate({}, { update: true, validateUndefined: false })).resolves.toEqual({})
|
|
1046
|
+
await expect(userdeep.validate({}, { update: true })).resolves.toEqual({})
|
|
1047
|
+
await expect(userdeep.validate({ name: {} }, { update: true, validateUndefined: false }))
|
|
1048
|
+
.resolves.toEqual({ name: {} })
|
|
1049
|
+
await expect(userdeep.validate({ names: [{}] }, { update: true, validateUndefined: false }))
|
|
1050
|
+
.resolves.toEqual({ names: [{}] })
|
|
1051
|
+
})
|
|
1052
|
+
|
|
958
1053
|
test('validation hooks', async () => {
|
|
959
1054
|
let db = (await opendb(null)).db
|
|
960
1055
|
let user = db.model('user', {
|