monastery 1.31.6 → 1.32.1

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,11 +2,34 @@
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.1](https://github.com/boycce/monastery/compare/1.32.0...1.32.1) (2022-03-01)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * processAfterFind bug ([3183b79](https://github.com/boycce/monastery/commit/3183b79fc288665000b63e0221fbe8acf6f482aa))
11
+
12
+ ## [1.32.0](https://github.com/boycce/monastery/compare/1.31.7...1.32.0) (2022-02-28)
13
+
14
+
15
+ ### Features
16
+
17
+ * added getSignedUrl(s) ([3552a4d](https://github.com/boycce/monastery/commit/3552a4d0b21c192a256a590e3ac1cb48b31c6564))
18
+ * added image optiosn filename, and params ([353b2f0](https://github.com/boycce/monastery/commit/353b2f09ed429a5cd8d74a3b2e94493650fb52e4))
19
+
20
+ ### [1.31.7](https://github.com/boycce/monastery/compare/1.31.6...1.31.7) (2022-02-28)
21
+
22
+
23
+ ### Bug Fixes
24
+
25
+ * refactored crud ops ([e7f3f78](https://github.com/boycce/monastery/commit/e7f3f784e123e4a66586a4d9e733d5cac477b98b))
26
+
5
27
  ### [1.31.6](https://github.com/boycce/monastery/compare/1.31.5...1.31.6) (2022-02-25)
6
28
 
7
29
 
8
30
  ### Bug Fixes
9
31
 
32
+ * Fixed validateUndefined ([58daed1](https://github.com/boycce/monastery/commit/58daed1ca5317c061a4ddde280bf45b0a134ab30))
10
33
  * added partial unqiue index tests ([ff6f193](https://github.com/boycce/monastery/commit/ff6f1938e333407ee17895873d2b42fa5263d7e3))
11
34
  * scripts ([bc32680](https://github.com/boycce/monastery/commit/bc326809098ae24686158a0386fbbd6671d86c98))
12
35
 
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
- gem "github-docs", git: "https://github.com/boycce/github-docs"
8
- group :jekyll_plugins do
9
- gem "jekyll-remote-theme", "~> 0.4.2"
10
- end
2
+
3
+ # Development (strict gem versions used)
4
+ gem 'github-pages', group: :jekyll_plugins
package/docs/_config.yml CHANGED
@@ -2,6 +2,7 @@ remote_theme: boycce/github-docs
2
2
  title: Monastery
3
3
  description: A straight forward MongoDB ODM built upon MonkJS
4
4
  github_url: "https://github.com/boycce/monastery"
5
+ port: 4001
5
6
  basedir: "docs"
6
7
 
7
8
  # Aux links for the naviation.
@@ -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'], // optional plugin rule
31
- fileSize: 1000 * 1000 * 5 // optional plugin rule, size in bytes
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 after every `find()` operation (can be overridden per request)
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
-
@@ -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
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  title: Rules
3
3
  nav_order: 5
4
+ parent: Schema
4
5
  ---
5
6
 
6
7
  # Validation Rules
package/lib/model-crud.js CHANGED
@@ -2,7 +2,7 @@ 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
@@ -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
- return util.parseData(data).then(data => {
33
- opts.data = data
34
- return this.validate(data, { ...opts })
35
-
36
- }).then(data => {
37
- return util.runSeries(this.beforeInsert.map(f => f.bind(opts, data))).then(() => data)
38
-
39
- }).then(data => {
40
- return this._insert(data, options)
41
-
42
- }).then(data => {
43
- return util.runSeries(this.afterInsert.map(f => f.bind(opts, data))).then(() => data)
44
-
45
- // Success/error
46
- }).then(data => {
47
- if (cb) cb(null, data)
48
- else if (opts.req && opts.respond) opts.req.res.json(data)
49
- else return Promise.resolve(data)
50
-
51
- }).catch(err => {
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
- return new Promise((resolve, reject) => {
77
- opts = this._queryObject(opts)
78
- opts.one = one || opts.one
79
- // Operation options
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 aggreagte = [
135
+ let aggregate = [
152
136
  { $match: opts.query },
153
137
  { $sort: options.sort },
154
138
  { $skip: options.skip },
@@ -157,41 +141,37 @@ module.exports = {
157
141
  ...util.isEmpty(options.addFields)? [] : [{ $addFields: options.addFields }],
158
142
  { $project: options.projection }
159
143
  ]
160
- var operation = this._aggregate.bind(this._collection, aggreagte)
161
- this.info('aggregate', JSON.stringify(aggreagte))
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
- }).then(data => {
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
- return this._processAfterFind(data, options.projection, opts)
152
+ response = await this._processAfterFind(response, options.projection, opts)
177
153
 
178
- }).then(data => {
179
- if (cb) cb(null, data)
180
- else if (opts.req && opts.respond) opts.req.res.json(data)
181
- else return Promise.resolve(data)
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
- }).catch(err => {
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
- update: function(opts, cb) {
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
@@ -208,159 +188,144 @@ 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
- return new Promise((resolve, reject) => {
216
- opts = this._queryObject(opts)
217
- opts.update = true
218
- opts.model = this
219
- let req = opts.req
220
- data = opts.data = util.isDefined(opts.data)? opts.data : util.isDefined((req||{}).body) ? req.body : undefined
221
- operators = util.pluck(opts, [/^\$/])
222
- // Operation options
223
- options = util.omit(opts, ['data', 'query', 'respond', 'validateUndefined', 'skipValidation', 'blacklist'])
224
- options.sort = options.sort || { 'createdAt': -1 }
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.parseData(data).then(d => resolve(d))
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
- if (!util.isDefined(data) && util.isEmpty(operators)) {
244
- throw new Error(`Please pass an update operator to ${this.name}.update(), e.g. data, $unset, etc`)
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
- return this._update(opts.query, operators, options).then(output => {
259
- if (!output.n) return null
260
- let response = Object.assign(Object.create({ _output: output }), operators['$set']||{})
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
- }).then(data => {
266
- if (!data) return data
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
- // Success/error
270
- }).then(data => {
271
- if (cb) cb(null, data)
272
- else if (opts.req && opts.respond) opts.req.res.json(data)
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
- }).catch(err => {
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
- return new Promise((resolve, reject) => {
298
- opts = this._queryObject(opts)
299
- opts.remove = true
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
- }).then(data => {
320
- return util.runSeries(this.afterRemove.map(f => f.bind(opts))).then(() => data)
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
- // Success/error
323
- }).then(data => {
324
- if (cb) cb(null, data)
325
- else if (opts.req && opts.respond) opts.req.res.json(data)
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
- }).catch(err => {
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
- * Extract the query id from opts, opts.query
338
- * @param {MongoId|ID string|Query object} opts
339
- * @return opts
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|MongodID - treated as an id
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
- let isIdType = (o) => util.isId(o) || util.isString(o)
347
- if (isIdType(opts)) opts = { query: { _id: opts || '' }}
348
- if (isIdType((opts||{}).query)) opts.query = { _id: opts.query || '' }
349
- if (!util.isObject(opts) || !util.isObject(opts.query)) {
350
- throw new Error('Please pass an object or MongoId to options.query')
351
- }
352
- // For security, if _id is set and undefined, throw an error
353
- if (typeof opts.query._id == 'undefined' && opts.query.hasOwnProperty('_id')) {
354
- throw new Error('Please pass an object or MongoId to options.query')
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
- // Remove undefined query parameters
357
- opts.query = util.removeUndefined(opts.query)
358
- if (util.isId(opts.query._id)) opts.query._id = this.manager.id(opts.query._id)
359
- if (isIdType(opts.query._id)) opts.one = true
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
 
363
- _processAfterFind: function(data, projection, afterFindContext) {
328
+ _processAfterFind: function(data, projection={}, afterFindContext={}) {
364
329
  /**
365
330
  * Todo: Maybe make this method public?
366
331
  * Recurses through fields that are models and populates missing default-fields and calls model.afterFind([fn,..])
@@ -290,9 +290,9 @@ module.exports = {
290
290
  },
291
291
 
292
292
  _ignoredRules: [ // todo: change name? i.e. 'specialFields'
293
- // Need to remove fileSize and formats..
294
- 'default', 'defaultOverride', 'fileSize', 'formats', 'image', 'index', 'insertOnly', 'model',
295
- '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'
296
296
  ]
297
297
 
298
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}". Ignoring rule.`)
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.31.6",
5
+ "version": "1.32.1",
6
6
  "license": "MIT",
7
7
  "repository": "github:boycce/monastery",
8
8
  "homepage": "https://boycce.github.io/monastery/",
@@ -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.call(model, this, data).then(() => n(null, data)).catch(e => n(e))
78
+ })
74
79
  }
75
80
  },
76
81
 
@@ -121,16 +126,17 @@ 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
- bucket: this.awsBucket,
126
- date: this.manager.useMilliseconds? Date.now() : Math.floor(Date.now() / 1000),
131
+ bucket: plugin.awsBucket,
132
+ date: plugin.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
- this.manager.info(
139
+ plugin.manager.info(
134
140
  `Uploading '${image.filename}' to '${image.bucket}/${image.path}'`
135
141
  )
136
142
  if (test) {
@@ -138,10 +144,14 @@ let plugin = module.exports = {
138
144
  resolve()
139
145
  } else {
140
146
  plugin.s3.upload({
141
- Bucket: this.awsBucket,
147
+ Bucket: plugin.awsBucket,
142
148
  Key: image.path,
143
149
  Body: file.data,
144
- ACL: 'public-read'
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,30 @@ let plugin = module.exports = {
172
182
  })
173
183
  },
174
184
 
185
+ getSignedUrls: async function(options, data) {
186
+ /**
187
+ * Get signed urls for all image objects in data
188
+ * @param {object} options - monastery operation options {model, query, files, ..}
189
+ * @param {object} data
190
+ * @return promise(data)
191
+ * @this model
192
+ */
193
+ // Not wanting signed urls for this operation?
194
+ if (util.isDefined(options.getSignedUrls) && !options.getSignedUrls) return
195
+
196
+ // Find all image objects in data
197
+ for (let doc of util.toArray(data)) {
198
+ for (let imageField of this.imageFields) {
199
+ if (options.getSignedUrls || imageField.getSignedUrl) {
200
+ let images = plugin._findImagesInData(doc, imageField, 0, '').filter(o => o.image)
201
+ for (let image of images) {
202
+ image.image.signedUrl = plugin._getSignedUrl(image.image.path)
203
+ }
204
+ }
205
+ }
206
+ }
207
+ },
208
+
175
209
  keepImagePlacement: async function(options, data) {
176
210
  /**
177
211
  * Hook before update/remove
@@ -303,7 +337,7 @@ let plugin = module.exports = {
303
337
  { Key: `medium/${key}.jpg` },
304
338
  { Key: `large/${key}.jpg` }
305
339
  )
306
- this.manager.info(
340
+ plugin.manager.info(
307
341
  `Removing '${pre.image.filename}' from '${pre.image.bucket}/${pre.image.path}'`
308
342
  )
309
343
  }
@@ -376,7 +410,7 @@ let plugin = module.exports = {
376
410
  return Promise.all(filesArr.map((file, i) => {
377
411
  return new Promise((resolve, reject) => {
378
412
  fileType.fromBuffer(file.data).then(res => {
379
- let maxSize = filesArr.imageField.fileSize
413
+ let maxSize = filesArr.imageField.filesize
380
414
  let formats = filesArr.imageField.formats || plugin.formats
381
415
  let allowAny = util.inArray(formats, 'any')
382
416
  file.format = res? res.ext : ''
@@ -421,17 +455,20 @@ let plugin = module.exports = {
421
455
  // Subdocument field
422
456
  if (util.isSubdocument(field)) {//schema.isObject
423
457
  // log(`Recurse 1: ${path2}`)
424
- list = list.concat(this._findAndTransformImageFields(field, path2))
458
+ list = list.concat(plugin._findAndTransformImageFields(field, path2))
425
459
 
426
460
  // Array field
427
461
  } else if (util.isArray(field)) {//schema.isArray
428
462
  // log(`Recurse 2: ${path2}`)
429
- list = list.concat(this._findAndTransformImageFields(field, path2))
463
+ list = list.concat(plugin._findAndTransformImageFields(field, path2))
430
464
 
431
465
  // Image field. Test for field.image as field.type may be 'any'
432
466
  } else if (field.type == 'image' || field.image) {
433
467
  let formats = field.formats
434
- let fileSize = field.fileSize
468
+ let filesize = field.filesize || field.fileSize // old <= v1.31.7
469
+ let filename = field.filename
470
+ let getSignedUrl = field.getSignedUrl
471
+ let params = { ...field.params||{} }
435
472
  // Convert image field to subdocument
436
473
  fields[fieldName] = {
437
474
  bucket: { type: 'string' },
@@ -440,13 +477,16 @@ let plugin = module.exports = {
440
477
  filesize: { type: 'number' },
441
478
  path: { type: 'string' },
442
479
  schema: { image: true, nullObject: true, isImageObject: true },
443
- uid: { type: 'string' }
480
+ uid: { type: 'string' },
444
481
  }
445
482
  list.push({
446
483
  fullPath: path2,
447
484
  fullPathRegex: new RegExp('^' + path2.replace(/\.[0-9]+/g, '.[0-9]+').replace(/\./g, '\\.') + '$'),
448
485
  formats: formats,
449
- fileSize: fileSize
486
+ filesize: filesize,
487
+ filename: filename,
488
+ getSignedUrl: getSignedUrl,
489
+ params: params,
450
490
  })
451
491
  }
452
492
  })
@@ -474,7 +514,7 @@ let plugin = module.exports = {
474
514
  if (`${dataPath}.${m}`.match(imageField.fullPathRegex)) {
475
515
  list.push({ imageField: imageField, dataPath: `${dataPath}.${m}`, image: target[m] })
476
516
  } else {
477
- list.push(...this._findImagesInData(
517
+ list.push(...plugin._findImagesInData(
478
518
  target[m],
479
519
  imageField,
480
520
  imageFieldChunkIndex+i+1,
@@ -495,6 +535,16 @@ let plugin = module.exports = {
495
535
  }
496
536
 
497
537
  return list
498
- }
538
+ },
539
+
540
+ _getSignedUrl: (path, expires=3600) => {
541
+ // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getSignedUrl-property
542
+ let signedUrl = plugin.s3.getSignedUrl('getObject', {
543
+ Bucket: plugin.awsBucket,
544
+ Key: path,
545
+ Expires: expires
546
+ })
547
+ return signedUrl
548
+ },
499
549
 
500
550
  }
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
@@ -707,4 +707,57 @@ 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 via query option
738
+ let imageWithSignedUrl = { ...image, signedUrl: expect.stringMatching(/^https/) }
739
+ await expect(db.user.findOne({ query: userInserted._id, getSignedUrls: true })).resolves.toEqual({
740
+ _id: expect.any(Object),
741
+ photos: [imageWithSignedUrl, imageWithSignedUrl],
742
+ photos2: [imageWithSignedUrl, imageWithSignedUrl],
743
+ })
744
+
745
+ // Find signed URL via schema option
746
+ await expect(db.user.findOne({ query: userInserted._id })).resolves.toEqual({
747
+ _id: expect.any(Object),
748
+ photos: [image, image],
749
+ photos2: [imageWithSignedUrl, imageWithSignedUrl],
750
+ })
751
+
752
+ // Works with _processAfterFind
753
+ let rawUser = await db.user._findOne({ _id: userInserted._id })
754
+ await expect(db.user._processAfterFind(rawUser)).resolves.toEqual({
755
+ _id: expect.any(Object),
756
+ photos: [image, image],
757
+ photos2: [imageWithSignedUrl, imageWithSignedUrl],
758
+ })
759
+
760
+ db.close()
761
+ })
762
+
710
763
  }