monastery 2.2.3 → 3.0.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/docs/readme.md CHANGED
@@ -2,16 +2,27 @@
2
2
 
3
3
  [![NPM](https://img.shields.io/npm/v/monastery.svg)](https://www.npmjs.com/package/monastery) [![Build Status](https://travis-ci.com/boycce/monastery.svg?branch=master)](https://app.travis-ci.com/github/boycce/monastery)
4
4
 
5
+ > [!IMPORTANT]
6
+ > v3.0 has been released 🎉 refer to [breaking changes](#v3.0BreakingChanges) below when upgrading from v2.x.
7
+
5
8
  ## Features
6
9
 
7
- * User friendly API design, built around the awesome [Monk](https://automattic.github.io/monk/)
8
- * Simple CRUD operations with model population
9
- * Model validation deriving from your model definitions
10
- * Custom error messages can be defined in your model definition
11
- * Normalised error responses ready for client consumption
12
- * Automatic mongodb index setup
13
- * CRUD operations can accept bracket (multipart/form-data) and dot notation data formats, you can also mix these together
10
+ * User friendly API design, *inspired by SailsJS*
11
+ * Simple CRUD operations, with simple but fast model population
12
+ * Model validation controlled by your model definitions
13
+ * Normalized error responses objects ready for client consumption
14
+ * Custom error messages can be defined in your model definitions
15
+ * Blacklist sensitive fields once in your model definition, or per operation
16
+ * Model methods can accept bracket (multipart/form-data) and dot notation data formats, you can also mix these together
17
+ * Automatic Mongo index creation
18
+
19
+ #### Why Monastery over Mongoose?
14
20
 
21
+ * User friendly API designed for busy agencies, allowing you to quickly build projects without distractions
22
+ * Model schema and configurations are all defined within a single object (model definition)
23
+ * You can blacklist/exclude sensitive model fields in the model definition for each CRUD operation
24
+ * Model population uses a single aggregation call instead of multiple queries for faster responses
25
+ * Errors throw normalized error objects that contain the model and field name, error message etc, handy in the client
15
26
 
16
27
  ## Install
17
28
 
@@ -26,9 +37,8 @@ $ npm install --save monastery
26
37
  ```javascript
27
38
  import monastery from 'monastery'
28
39
 
29
- // Initialise a monastery manager
40
+ // Initialize a monastery manager
30
41
  const db = monastery('localhost/mydb')
31
- // const db = monastery('user:pass@localhost:port/mydb')
32
42
 
33
43
  // Define a model
34
44
  db.model('user', {
@@ -41,18 +51,16 @@ db.model('user', {
41
51
  })
42
52
 
43
53
  // Insert some data
44
- db.user.insert({
45
- data: {
46
- name: 'Martin Luther',
47
- pets: ['sparky', 'tiny'],
48
- address: { city: 'Eisleben' },
49
- points: [[1, 5], [3, 1]]
50
- }
51
-
52
- }).then(data => {
53
- // valid data..
54
-
55
- }).catch(errs => {
54
+ try {
55
+ const newUser = await db.user.insert({
56
+ data: {
57
+ name: 'Martin Luther',
58
+ pets: ['sparky', 'tiny'],
59
+ address: { city: 'Eisleben' },
60
+ points: [[1, 5], [3, 1]]
61
+ }
62
+ })
63
+ } catch (errs) {
56
64
  // [{
57
65
  // detail: "Value needs to be at least 10 characters long.",
58
66
  // status: "400",
@@ -63,31 +71,28 @@ db.user.insert({
63
71
  // rule: "minLength"
64
72
  // }
65
73
  // }]
66
- })
74
+ }
67
75
  ```
68
- ## Versions
76
+ ## Version Compatibility
69
77
 
70
- - Monk: `v7.3.4`
71
- - MongoDB NodeJS driver: `v3.7.4` ([MongoDB compatibility](https://www.mongodb.com/docs/drivers/node/current/compatibility/#compatibility))
72
- - MongoDB: [`v5.0.0`](https://www.mongodb.com/docs/v5.0/reference/) [(`v6.0.0` partial support)](https://www.mongodb.com/docs/v6.0/reference/)
73
-
74
- ## Debugging
78
+ You can view MongoDB's [compatibility table here](https://www.mongodb.com/docs/drivers/node/current/compatibility/), and see all of MongoDB NodeJS Driver [releases here](https://mongodb.github.io/node-mongodb-native/)
75
79
 
76
- This package uses [debug](https://github.com/visionmedia/debug) which allows you to set different levels of output via the `DEBUG` environment variable. Due to known limations `monastery:warning` and `monastery:error` are forced on, you can however disable these via [manager settings](./manager).
80
+ | Monastery | Mongo NodeJS Driver | MongoDB Server | Node |
81
+ | :------------------- | :-----------------: | :---------------: | ------------------: |
82
+ | `3.x` | [`5.9.x`](https://mongodb.github.io/node-mongodb-native/5.9/) | `>=3.6 <=7.x` | `>=14.x <=latest` |
83
+ | `2.x` | [`3.7.x`](https://mongodb.github.io/node-mongodb-native/3.7/api/) | `>=2.6 <=6.x` | `>=4.x <=14.x` |
77
84
 
78
- ```bash
79
- $ DEBUG=monastery:info # shows operation information
80
- ```
81
85
 
82
- To run isolated tests with Jest:
86
+ ## v3.0 Breaking Changes
83
87
 
84
- ```bash
85
- npm run dev -- -t 'Model indexes'
86
- ```
87
-
88
- ## Contributing
89
-
90
- Coming soon...
88
+ - Removed callback functions on all model methods, you can use the returned promise instead
89
+ - `model.update()` now returns the following _update property:
90
+ - `{ acknowledged: true, matchedCount: 1, modifiedCount: 1, upsertedCount: 0, upsertedId: null }`, instead of
91
+ - `{ n: 1, nModified: 1, ok: 1 }`
92
+ - `model.remove()` now returns `{ acknowledged: true, deletedCount: 1 }`, instead of `{ results: { n: 1, ok: 1} }`
93
+ - Models are now added to `db.models` instead of `db.model`, e.g. `db.models.user`
94
+ - MongoDB connection can be found here `db.db` changed from `db._db`
95
+ - `model._indexes()` now returns `collection._indexes()` not `collection._indexInformation()`
91
96
 
92
97
  ## Roadmap
93
98
 
@@ -103,19 +108,38 @@ Coming soon...
103
108
  - ~~Ability to change ACL default on the manager~~
104
109
  - ~~Public db.arrayWithSchema method~~
105
110
  - ~~Added support for array population~~
111
+ - ~~MongoClient instances can now be reused when initializing the manager, e.g. `monastery(mongoClient)`, handy for migrate-mongo~~
106
112
  - Change population warnings into errors
107
113
  - Global after/before hooks
108
- - before hooks can receive a data array, remove this
109
- - docs: Make the implicit ID query conversion more apparent
110
- - Split away from Monk so we can update the MongoDB NodeJS Driver version
114
+ - Before hooks can receive a data array, remove this
115
+ - Docs: Make the implicit ID query conversion more apparent
116
+ - ~~Split away from Monk so we can update the MongoDB NodeJS Driver version~~
111
117
  - Add a warning if an invalid model is referenced in jthe schema
112
118
  - Remove leading forward slashes from custom image paths (AWS adds this as a seperate folder)
113
- - double check await db.model.remove({ query: idfromparam }) doesnt cause issues for null, undefined or '', but continue to allow {}
114
- - ~~can't insert/update model id (maybe we can allow this and add _id to default insert/update blacklists)~~
119
+ - Double check await db.model.remove({ query: idfromparam }) doesnt cause issues for null, undefined or '', but continue to allow {}
120
+ - ~~Can't insert/update model id (maybe we can allow this and add _id to default insert/update blacklists)~~
115
121
  - timstamps are blacklisted by default (instead of the `timestamps` opt), and can be switched off via blacklisting
116
122
  - Allow rules on image types, e.g. `required`
117
- - test importing of models
123
+ - Test importing of models
118
124
  - Docs: model.methods
125
+ - ~~Convert hooks to promises~~
126
+ - ~~added `model.count()` ~~
127
+
128
+ ## Debugging
129
+
130
+ This package uses [debug](https://github.com/visionmedia/debug) which allows you to set different levels of output via the `DEBUG` environment variable. Due to known limations `monastery:warning` and `monastery:error` are forced on, you can however disable these via [manager settings](./manager).
131
+
132
+ ```bash
133
+ $ DEBUG=monastery:info # shows operation information
134
+ ```
135
+
136
+ ## Contributing
137
+
138
+ All pull requests are welcome. To run isolated tests with Jest:
139
+
140
+ ```bash
141
+ npm run dev -- -t 'Model indexes'
142
+ ```
119
143
 
120
144
  ## Special Thanks
121
145
 
@@ -123,4 +147,13 @@ Coming soon...
123
147
 
124
148
  ## License
125
149
 
126
- Copyright 2020 Ricky Boyce. Code released under the MIT license.
150
+ Copyright 2024 Ricky Boyce. Code released under the MIT license.
151
+
152
+
153
+
154
+
155
+
156
+
157
+ ///////////////////////////////
158
+ /////3. add 'Collection' to monastery docs sidebar ( also add model.count)
159
+ //// docs sapcing.... +4px
@@ -0,0 +1,324 @@
1
+ const util = require('./util.js')
2
+
3
+ function Collection (manager, name, options) {
4
+ this.col = null
5
+ this.manager = manager
6
+ this.name = name
7
+ this.options = options
8
+ }
9
+
10
+ Collection.prototype._middleware = async function (args) {
11
+ /**
12
+ * Modfy the arguments before passing them to the MongoDB driver collection operations
13
+ * @return {Object} args
14
+ */
15
+ const objectsToCast = ['operations', 'query', 'data', 'update']
16
+ let { fields, opts, query } = args
17
+ if (!opts) args.opts = opts = {}
18
+
19
+ // Get the collection
20
+ this.col = this.col || this.manager.db.collection(this.name)
21
+
22
+ // Query: convert strings to ObjectIds
23
+ if (query) {
24
+ if (typeof query === 'string' || typeof query.toHexString === 'function') {
25
+ args.query = {_id: args.query}
26
+ }
27
+ }
28
+
29
+ // Options: setup
30
+ if (typeof opts === 'string' || Array.isArray(opts)) {
31
+ throw new Error('You can no longer pass an array or string `projection` to find()')
32
+ } else {
33
+ // MongoDB 5.0 docs seem to be a little off, projection is still included in `opts`...
34
+ if (opts.fields) opts.projection = _fields(opts.fields, 0)
35
+ if (opts.sort) opts.sort = _fields(opts.sort, -1)
36
+ delete opts.fields
37
+ }
38
+
39
+ // Options: use collection defaults
40
+ for (let key in this.options) {
41
+ if (typeof opts[key] === 'undefined') {
42
+ opts[key] = this.options[key]
43
+ }
44
+ }
45
+
46
+ // Options: castIds (defaults on)
47
+ if (opts.castIds !== false) {
48
+ for (const key of objectsToCast) {
49
+ if (args[key]) args[key] = util.cast(args[key])
50
+ }
51
+ }
52
+
53
+ // Fields: convert strings/arrays to objects, e.g. 'name _id' -> {name: 1, _id: 1}
54
+ if (fields && !util.isObject(fields)) {
55
+ let fieldsArray = typeof fields === 'string' ? fields.split(' ') : (fields || [])
56
+ args.fields = fieldsArray.reduce((acc, fieldName) => {
57
+ acc[fieldName] = 1
58
+ return acc
59
+ }, {})
60
+ }
61
+
62
+ function _fields (obj, numberWhenMinus) {
63
+ if (!Array.isArray(obj) && typeof obj === 'object') return obj
64
+ obj = typeof obj === 'string' ? obj.split(' ') : (obj || [])
65
+ let fields = {}
66
+ for (let i = 0, l = obj.length; i < l; i++) {
67
+ if (obj[i][0] === '-') fields[obj[i].substr(1)] = numberWhenMinus
68
+ else fields[obj[i]] = 1
69
+ }
70
+ return fields
71
+ }
72
+
73
+ return args
74
+ }
75
+
76
+ Collection.prototype.aggregate = async function (stages, opts) {
77
+ const args = await this._middleware({ stages, opts })
78
+ return this.col.aggregate(args.stages, args.opts).toArray()
79
+ }
80
+
81
+ Collection.prototype.bulkWrite = async function (operations, opts) {
82
+ const args = await this._middleware({ operations, opts })
83
+ return this.col.bulkWrite(args.operations, args.opts)
84
+ }
85
+
86
+ Collection.prototype.count = async function (query, opts) {
87
+ const args = await this._middleware({ query, opts })
88
+ const { estimate, ..._opts } = args.opts
89
+ if (estimate) {
90
+ return this.col.estimatedDocumentCount(_opts)
91
+ } else {
92
+ return this.col.countDocuments(args.query, _opts)
93
+ }
94
+ }
95
+
96
+ Collection.prototype.createIndex = async function (indexSpec, opts) {
97
+ const args = await this._middleware({ indexSpec, opts })
98
+ return await this.col.createIndex(args.indexSpec, args.opts)
99
+ }
100
+
101
+ Collection.prototype.createIndexes = async function (indexSpecs, opts) {
102
+ const args = await this._middleware({ indexSpecs, opts }) // doesn't currently accept string or array parsing.
103
+ return await this.col.createIndexes(args.indexSpecs, args.opts)
104
+ }
105
+
106
+ Collection.prototype.distinct = async function (field, query, opts) {
107
+ const args = await this._middleware({ field, query, opts })
108
+ return this.col.distinct(args.field, args.query, args.opts)
109
+ }
110
+
111
+ Collection.prototype.drop = async function () {
112
+ try {
113
+ await this._middleware({})
114
+ await this.col.drop()
115
+ // this.col = null
116
+ } catch (err) {
117
+ if (err?.message == 'ns not found') return 'ns not found'
118
+ throw err
119
+ }
120
+ }
121
+
122
+ Collection.prototype.dropIndex = async function (name, opts) {
123
+ const args = await this._middleware({ name, opts })
124
+ return await this.col.dropIndex(args.name, args.opts)
125
+ }
126
+
127
+ Collection.prototype.dropIndexes = async function () {
128
+ await this._middleware({})
129
+ return this.col.dropIndexes()
130
+ }
131
+
132
+ Collection.prototype.find = async function (query, opts) {
133
+ // v3.0 - removed the abillity to pass an array or string to opts as `opts.projection`
134
+ // v3.0 - find().each() is removed, use `opts.stream` instead
135
+ const args = await this._middleware({ query, opts })
136
+ query = args.query
137
+ opts = args.opts
138
+ const rawCursor = opts.rawCursor
139
+
140
+ // Get the raw cursor
141
+ const cursor = this.col.find(query, opts)
142
+
143
+ // If a raw cursor is requested, return it now
144
+ if (rawCursor) return cursor
145
+
146
+ // If no stream is requested, return now the array of results
147
+ if (!opts.stream) return cursor.toArray()
148
+
149
+ if (typeof opts.stream !== 'function') {
150
+ throw new Error('opts.stream must be a function')
151
+ }
152
+
153
+ const stream = cursor.stream()
154
+ return new Promise((resolve, reject) => {
155
+ let closed = false
156
+ let finished = false
157
+ let processing = 0
158
+ function close () {
159
+ closed = true
160
+ processing -= 1
161
+ cursor.close()
162
+ }
163
+ function pause () {
164
+ processing += 1
165
+ stream.pause()
166
+ }
167
+ function resume () {
168
+ processing -= 1
169
+ stream.resume()
170
+ if (processing === 0 && finished) done()
171
+ }
172
+ function done () {
173
+ finished = true
174
+ if (processing <= 0) resolve()
175
+ }
176
+
177
+ stream.on('close', done)
178
+ stream.on('end', done)
179
+ stream.on('error', (err) => reject(err))
180
+ stream.on('data', (doc) => {
181
+ if (!closed) opts.stream(doc, { close, pause, resume })
182
+ })
183
+ })
184
+ }
185
+
186
+ Collection.prototype.findOne = async function (query, opts) {
187
+ const args = await this._middleware({ query, opts })
188
+ const docs = await this.col.find(args.query, args.opts).limit(1).toArray()
189
+ return docs?.[0] || null
190
+ }
191
+
192
+ Collection.prototype.findOneAndDelete = async function (query, opts) {
193
+ const args = await this._middleware({ query, opts })
194
+ const doc = await this.col.findOneAndDelete(args.query, args.opts)
195
+
196
+ if (doc && typeof doc.value !== 'undefined') return doc.value
197
+ if (doc.ok && doc.lastErrorObject && doc.lastErrorObject.n === 0) return null
198
+ return doc
199
+ }
200
+
201
+ Collection.prototype.findOneAndUpdate = async function (query, update, opts) {
202
+ const args = await this._middleware({ query, update, opts })
203
+ let method = 'findOneAndUpdate'
204
+
205
+ if (typeof args.opts?.returnDocument === 'undefined') {
206
+ args.opts.returnDocument = 'after'
207
+ }
208
+ if (typeof args.opts?.returnOriginal !== 'undefined') {
209
+ this.manager.warn('The `returnOriginal` option is deprecated, use `returnDocument` instead.')
210
+ args.opts.returnDocument = args.opts.returnOriginal ? 'before' : 'after'
211
+ }
212
+ if (args.opts.replaceOne || args.opts.replace) {
213
+ method = 'findOneAndReplace'
214
+ }
215
+
216
+ const doc = await this.col[method](args.query, args.update, args.opts)
217
+ if (doc && typeof doc.value !== 'undefined') return doc.value
218
+ if (doc.ok && doc.lastErrorObject && doc.lastErrorObject.n === 0) return null
219
+ return doc
220
+ }
221
+
222
+ Collection.prototype.geoHaystackSearch = async function (x, y, opts) {
223
+ // https://www.mongodb.com/docs/manual/geospatial-queries/
224
+ throw new Error('geoHaystackSearch is depreciated in MongoDB 4.0, use geospatial queries instead, e.g. $geoWithin')
225
+ }
226
+
227
+ Collection.prototype.indexInformation = async function (opts) {
228
+ try {
229
+ const args = await this._middleware({ opts })
230
+ return await this.col.indexInformation(args.opts)
231
+ } catch (e) {
232
+ // col.indexInformation() throws an error if the collection is created yet...
233
+ if (e?.message.match(/ns does not exist/)) return {}
234
+ else throw new Error(e)
235
+ }
236
+ }
237
+
238
+ Collection.prototype.indexes = async function (opts) {
239
+ try {
240
+ const args = await this._middleware({ opts })
241
+ return await this.col.indexes(args.opts)
242
+ } catch (e) {
243
+ // col.indexes() throws an error if the collection is created yet...
244
+ if (e?.message.match(/ns does not exist/)) return []
245
+ else throw new Error(e)
246
+ }
247
+ }
248
+
249
+ Collection.prototype.insert = async function (data, opts) {
250
+ const args = await this._middleware({ data, opts })
251
+ const arrayInsert = Array.isArray(args.data)
252
+
253
+
254
+ if (arrayInsert && args.data.length === 0) {
255
+ return Promise.resolve([])
256
+ }
257
+
258
+ const doc = await this.col[`insert${arrayInsert ? 'Many' : 'One'}`](args.data, args.opts)
259
+ if (!doc) return
260
+
261
+ // Starting MongoDB 4 the `insert` method only returns the _id, rather than the whole doc.
262
+ // We need to return the whole doc for consistency with previous versions.
263
+ const output = util.deepCopy(args.data)
264
+ if (arrayInsert) {
265
+ for (let i=output.length; i--;) {
266
+ output[i]._id = doc.insertedIds[i]
267
+ }
268
+ } else {
269
+ output._id = doc.insertedId
270
+ }
271
+
272
+ return output
273
+ }
274
+
275
+ Collection.prototype.mapReduce = async function (map, reduce, opts) {
276
+ // https://www.mongodb.com/docs/manual/reference/method/db.collection.mapReduce/
277
+ throw new Error('mapReduce is depreciated in MongoDB 5.0, use aggregation pipeline instead')
278
+ }
279
+
280
+ Collection.prototype.remove = async function (query, opts) {
281
+ const args = await this._middleware({ query, opts })
282
+ const method = args.opts.single || args.opts.multi === false ? 'deleteOne' : 'deleteMany'
283
+ return this.col[method](args.query, args.opts)
284
+ }
285
+
286
+ Collection.prototype.stats = async function (opts) {
287
+ const args = await this._middleware({ opts })
288
+ return this.col.stats(args.opts)
289
+ }
290
+
291
+ Collection.prototype.update = async function (query, update, opts) {
292
+ // v3.0 - returns now an object with the following properties:
293
+ // {
294
+ // acknowledged: true,
295
+ // matchedCount: 0,
296
+ // modifiedCount: 0,
297
+ // upsertedCount: 1,
298
+ // upsertedId: expect.any(ObjectId),
299
+ // }
300
+ // was:
301
+ // {
302
+ // result: {
303
+ // ok: 1,
304
+ // n: 1,
305
+ // nModified: 1,
306
+ // upserted: [{ _id: expect.any(ObjectId) }],
307
+ // }
308
+ // }
309
+ const args = await this._middleware({ query, update, opts })
310
+ let method = args.opts.multi || args.opts.single === false ? 'updateMany' : 'updateOne'
311
+
312
+ if (args.opts.replace || args.opts.replaceOne) {
313
+ if (args.opts.multi || args.opts.single === false) {
314
+ throw new Error('The `replace` option is only available for single updates.')
315
+ }
316
+ method = 'replaceOne'
317
+ }
318
+
319
+ const doc = await this.col[method](args.query, args.update, args.opts)
320
+ // return doc?.result || doc
321
+ return doc
322
+ }
323
+
324
+ module.exports = Collection