monastery 2.2.2 → 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,25 +71,28 @@ db.user.insert({
63
71
  // rule: "minLength"
64
72
  // }
65
73
  // }]
66
- })
74
+ }
67
75
  ```
68
- ## Debugging
69
-
70
- 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).
76
+ ## Version Compatibility
71
77
 
72
- ```bash
73
- $ DEBUG=monastery:info # shows operation information
74
- ```
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
- To run isolated tests with Jest:
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
- npm run dev -- -t 'Model indexes'
80
- ```
81
85
 
82
- ## Contributing
86
+ ## v3.0 Breaking Changes
83
87
 
84
- 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()`
85
96
 
86
97
  ## Roadmap
87
98
 
@@ -97,25 +108,38 @@ Coming soon...
97
108
  - ~~Ability to change ACL default on the manager~~
98
109
  - ~~Public db.arrayWithSchema method~~
99
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~~
100
112
  - Change population warnings into errors
101
113
  - Global after/before hooks
102
- - before hooks can receive a data array, remove this
103
- - docs: Make the implicit ID query conversion more apparent
104
- - Split away from Monk (unless updated)
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~~
105
117
  - Add a warning if an invalid model is referenced in jthe schema
106
118
  - Remove leading forward slashes from custom image paths (AWS adds this as a seperate folder)
107
- - double check await db.model.remove({ query: idfromparam }) doesnt cause issues for null, undefined or '', but continue to allow {}
108
- - ~~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)~~
109
121
  - timstamps are blacklisted by default (instead of the `timestamps` opt), and can be switched off via blacklisting
110
122
  - Allow rules on image types, e.g. `required`
111
- - test importing of models
123
+ - Test importing of models
112
124
  - Docs: model.methods
125
+ - ~~Convert hooks to promises~~
126
+ - ~~added `model.count()` ~~
113
127
 
114
- ## Versions
128
+ ## Debugging
115
129
 
116
- - Monk: `v7.3.4`
117
- - MongoDB NodeJS driver: `v3.2.3` ([compatibility](https://www.mongodb.com/docs/drivers/node/current/compatibility/#compatibility))
118
- - MongoDB: [`v4.0.0`](https://www.mongodb.com/docs/v4.2/reference/)
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