monastery 3.2.0 → 3.3.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 CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [3.3.0](https://github.com/boycce/monastery/compare/3.2.1...3.3.0) (2024-08-07)
6
+
7
+ ### [3.2.1](https://github.com/boycce/monastery/compare/3.2.0...3.2.1) (2024-08-05)
8
+
5
9
  ## [3.2.0](https://github.com/boycce/monastery/compare/3.1.0...3.2.0) (2024-07-17)
6
10
 
7
11
  ### [3.1.1](https://github.com/boycce/monastery/compare/3.1.0...3.1.1) (2024-05-27)
@@ -13,11 +13,12 @@ Monastery manager constructor.
13
13
  `uri` *(string\|array)*: A [mongo connection string URI](https://www.mongodb.com/docs/v5.0/reference/connection-string/). Replica sets can be an array or comma separated.
14
14
 
15
15
  [`options`] *(object)*:
16
- - [`defaultObjects=false`] *(boolean)*: when [inserting](../model/insert.html#defaults-example), undefined embedded documents and arrays are defined
17
- - [`logLevel=2`] *(number)*: 1=errors, 2=warnings, 3=info. You can also use the debug environment variable `DEBUG=monastery:info`
16
+ - [`defaultObjects=false`] *(boolean)*: when [inserting](../model/insert.html#defaults-example), undefined embedded documents and arrays are defined.
17
+ - [`logLevel=2`] *(number)*: 1=errors, 2=warnings, 3=info. You can also use the debug environment variable `DEBUG=monastery:info`.
18
+ - [`noDefaults`] *(boolean\|string\|array)*: after find operations, don't add defaults for any matching paths, e.g. ['pet.name']. You can override this per operation.
18
19
  - [`nullObjects=false`] *(boolean)*: embedded documents and arrays can be set to null or an empty string (which gets converted to null). You can override this per field via `nullObject: true`.
19
- - [`promise=false`] *(boolean)*: return a promise instead of the manager instance
20
- - [`timestamps=true`] *(boolean)*: whether to use [`createdAt` and `updatedAt`](../definition), this can be overridden per operation
20
+ - [`promise=false`] *(boolean)*: return a promise instead of the manager instance.
21
+ - [`timestamps=true`] *(boolean)*: whether to use [`createdAt` and `updatedAt`](../definition), this can be overridden per operation.
21
22
  - [`useMilliseconds=false`] *(boolean)*: by default the `createdAt` and `updatedAt` fields that get created automatically use unix timestamps in seconds, set this to true to use milliseconds instead.
22
23
  - [`mongo options`](https://mongodb.github.io/node-mongodb-native/5.9/interfaces/MongoClientOptions.html)...
23
24
 
@@ -22,7 +22,17 @@ Update document(s) in a collection and calls model hooks: `beforeUpdate`, `afte
22
22
 
23
23
  ### Returns
24
24
 
25
- A promise
25
+ `{Promise<Object>}` A promise that resolves to an object with the updated fields.
26
+
27
+ You can also access the native MongoDB output via `result._output`, a prototype property:
28
+ ```js
29
+ {
30
+ acknowledged: true,
31
+ modifiedCount: 1,
32
+ matchedCount: 1,
33
+ ...
34
+ }
35
+ ```
26
36
 
27
37
  ### Example
28
38
 
package/lib/index.js CHANGED
@@ -38,7 +38,7 @@ function Manager(uri, opts) {
38
38
  const mongoOpts = Object.keys(opts||{}).reduce((acc, key) => {
39
39
  if (
40
40
  ![
41
- 'databaseName', 'defaultObjects', 'logLevel', 'imagePlugin', 'limit', 'nullObjects',
41
+ 'databaseName', 'defaultObjects', 'logLevel', 'imagePlugin', 'limit', 'noDefaults', 'nullObjects',
42
42
  'promise', 'timestamps', 'useMilliseconds',
43
43
  ].includes(key)) {
44
44
  acc[key] = opts[key]
package/lib/model-crud.js CHANGED
@@ -531,6 +531,9 @@ Model.prototype._queryObject = async function (opts, type, _one) {
531
531
  let order = (opts.sort.match(/:(-?[0-9])/) || [])[1]
532
532
  opts.sort = { [name]: parseInt(order || 1) }
533
533
  }
534
+ if (typeof opts.noDefaults == 'undefined' && typeof this.manager.opts.noDefaults != 'undefined') {
535
+ opts.noDefaults = this.manager.opts.noDefaults
536
+ }
534
537
  if (util.isString(opts.noDefaults)) {
535
538
  opts.noDefaults = [opts.noDefaults]
536
539
  }
@@ -140,6 +140,7 @@ Model.prototype._validateFields = function (dataRoot, fields, data, opts, parent
140
140
  // Field is a subdocument
141
141
  if (schema.isObject) {
142
142
  // Object schema errors
143
+ let res
143
144
  const verrors = this._validateRules(dataRoot, schema, value, opts, path)
144
145
  if (verrors.length) errors.push(...verrors)
145
146
  // Recurse if inserting, value is a subdocument, or a deep property (todo: not dot-notation)
@@ -148,21 +149,22 @@ Model.prototype._validateFields = function (dataRoot, fields, data, opts, parent
148
149
  util.isObject(value) ||
149
150
  (util.isDefined(opts.validateUndefined) ? opts.validateUndefined : (path||'').indexOf('.') !== -1)
150
151
  ) {
151
- var res = this._validateFields(dataRoot, field, value, opts, path, path2)
152
+ res = this._validateFields(dataRoot, field, value, opts, path, path2)
152
153
  if (res[0].length) errors.push(...res[0])
153
154
  }
154
155
  if (util.isDefined(value) && !verrors.length) {
155
- data2[indexOrFieldName] = res? res[1] : value
156
+ data2[indexOrFieldName] = res ? res[1] : value
156
157
  }
157
158
 
158
159
  // Field is an array
159
160
  } else if (schema.isArray) {
160
161
  // Array schema errors
162
+ let res2
161
163
  const verrors = this._validateRules(dataRoot, schema, value, opts, path)
162
164
  if (verrors.length) errors.push(...verrors)
163
165
  // Data value is array too
164
166
  if (util.isArray(value)) {
165
- var res2 = this._validateFields(dataRoot, field, value, opts, path, path2)
167
+ res2 = this._validateFields(dataRoot, field, value, opts, path, path2)
166
168
  if (res2[0].length) errors.push(...res2[0])
167
169
  }
168
170
  if (util.isDefined(value) && !verrors.length) {
@@ -209,6 +211,8 @@ Model.prototype._validateRules = function (dataRoot, fieldSchema, value, opts, p
209
211
  if (opts.skipValidation === true) return []
210
212
 
211
213
  // Skip validation for a field, takes in to account if a parent has been skipped.
214
+ // Todo: Maybe we can use model-crud:blacklisted logic? But just allow it to skip all [0-9] paths via '$'
215
+ // if (!parentPath && fieldName == 'categories') console.timeEnd(i + ' - ' + m + ' - ' + fieldName + ' - 1')/////
212
216
  if (opts.skipValidation.length) {
213
217
  //console.log(path, field, opts)
214
218
  let pathChunks = path.split('.')
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "monastery",
3
3
  "description": "⛪ A simple, straightforward MongoDB ODM",
4
4
  "author": "Ricky Boyce",
5
- "version": "3.2.0",
5
+ "version": "3.3.0",
6
6
  "license": "MIT",
7
7
  "repository": "github:boycce/monastery",
8
8
  "homepage": "https://boycce.github.io/monastery/",
package/test/crud.js CHANGED
@@ -337,41 +337,112 @@ test('find default field blacklisted', async () => {
337
337
  })
338
338
  })
339
339
 
340
- test('find default field population with noDefaults', async () => {
341
- // similar to "find default field population"
342
- db.model('user', {
343
- fields: {
344
- name: { type: 'string', default: 'Martin Luther' },
345
- addresses: [{ city: { type: 'string' }, country: { type: 'string', default: 'Germany' } }],
346
- address: { country: { type: 'string', default: 'Germany' }},
347
- pet: { dog: { model: 'dog' }},
348
- pets: { dog: [{ model: 'dog' }]},
349
- dogs: [{ model: 'dog' }], // virtual association
350
- },
340
+ test('find default field population with option noDefaults', async () => {
341
+ async function setup(noDefaults) {
342
+ /**
343
+ * Setup
344
+ * @returns {Object} {dog, user, dog1Doc, dog2Doc, user1Doc}
345
+ */
346
+ // similar to "find default field population"
347
+ const db = monastery('127.0.0.1/monastery', { noDefaults: noDefaults, timestamps: false })
348
+ const userDefinition = {
349
+ fields: {
350
+ name: { type: 'string', default: 'Martin Luther' },
351
+ addresses: [{ city: { type: 'string' }, country: { type: 'string', default: 'Germany' } }],
352
+ address: { country: { type: 'string', default: 'Germany' }},
353
+ pet: { dog: { model: 'dog' }},
354
+ pets: { dog: [{ model: 'dog' }]},
355
+ dogs: [{ model: 'dog' }], // virtual association
356
+ },
357
+ }
358
+ const dogDefinition = {
359
+ fields: {
360
+ name: { type: 'string', default: 'Scruff' },
361
+ user: { model: 'user' },
362
+ },
363
+ }
364
+ const user = db.model('user', userDefinition)
365
+ const dog = db.model('dog', dogDefinition)
366
+
367
+ // Default field population test
368
+ // Insert documents (without defaults)
369
+ let dog1Doc = await dog._insert({})
370
+ let dog2Doc = await dog._insert({})
371
+ let user1Doc = await user._insert({
372
+ addresses: [
373
+ { city: 'Frankfurt' },
374
+ { city: 'Christchurch', country: 'New Zealand' },
375
+ ],
376
+ pet: { dog: dog1Doc._id },
377
+ pets: { dog: [dog1Doc._id, dog2Doc._id]},
378
+ })
379
+ await dog._update(dog1Doc._id, { $set: { user: user1Doc._id }})
380
+ return { db, dog, user, dog1Doc, dog2Doc, user1Doc }
381
+ }
382
+
383
+ const s1 = await setup()
384
+
385
+ // Test noDefaults = true
386
+ const find1 = await s1.db.user.findOne({
387
+ query: s1.user1Doc._id,
388
+ populate: ['pet.dog', 'pets.dog', {
389
+ from: 'dog',
390
+ localField: '_id',
391
+ foreignField: 'user',
392
+ as: 'dogs',
393
+ }],
394
+ noDefaults: true,
351
395
  })
352
- db.model('dog', {
353
- fields: {
354
- name: { type: 'string', default: 'Scruff' },
355
- user: { model: 'user' },
396
+ expect(find1).toEqual({
397
+ _id: s1.user1Doc._id,
398
+ addresses: [
399
+ { city: 'Frankfurt' },
400
+ { city: 'Christchurch', country: 'New Zealand' },
401
+ ],
402
+ pet: { dog: { _id: s1.dog1Doc._id, user: s1.user1Doc._id }},
403
+ pets: {
404
+ dog: [
405
+ { _id: s1.dog1Doc._id, user: s1.user1Doc._id },
406
+ { _id: s1.dog2Doc._id },
407
+ ],
356
408
  },
409
+ dogs: [{ _id: s1.dog1Doc._id, user: s1.user1Doc._id }],
357
410
  })
358
411
 
359
- // Default field population test
360
- // Insert documents (without defaults)
361
- let dog1 = await db.dog._insert({})
362
- let dog2 = await db.dog._insert({})
363
- let user1 = await db.user._insert({
412
+ // Test noDefaults = ['dogs', 'pet.dog']
413
+ const find2 = await s1.db.user.findOne({
414
+ query: s1.user1Doc._id,
415
+ populate: ['pet.dog', 'pets.dog', {
416
+ from: 'dog',
417
+ localField: '_id',
418
+ foreignField: 'user',
419
+ as: 'dogs',
420
+ }],
421
+ noDefaults: ['dogs', 'pet.dog'],
422
+ })
423
+ expect(find2).toEqual({
424
+ _id: s1.user1Doc._id,
425
+ name: 'Martin Luther',
364
426
  addresses: [
365
- { city: 'Frankfurt' },
427
+ { city: 'Frankfurt', country: 'Germany' },
366
428
  { city: 'Christchurch', country: 'New Zealand' },
367
429
  ],
368
- pet: { dog: dog1._id },
369
- pets: { dog: [dog1._id, dog2._id]},
430
+ address: { country: 'Germany' },
431
+ pet: { dog: { _id: s1.dog1Doc._id, user: s1.user1Doc._id }}, // should not have a default name
432
+ pets: {
433
+ dog: [
434
+ { _id: s1.dog1Doc._id, name: 'Scruff', user: s1.user1Doc._id },
435
+ { _id: s1.dog2Doc._id, name: 'Scruff' },
436
+ ],
437
+ },
438
+ dogs: [{ _id: s1.dog1Doc._id, user: s1.user1Doc._id }], // should not have a default name
370
439
  })
371
- await db.dog._update(dog1._id, { $set: { user: user1._id }})
372
440
 
373
- let find1 = await db.user.findOne({
374
- query: user1._id,
441
+ const s2 = await setup(['dogs'])
442
+
443
+ // Test noDefaults = true overrides manager option noDefaults = ['dogs']
444
+ const find3 = await s2.db.user.findOne({
445
+ query: s2.user1Doc._id,
375
446
  populate: ['pet.dog', 'pets.dog', {
376
447
  from: 'dog',
377
448
  localField: '_id',
@@ -380,50 +451,48 @@ test('find default field population with noDefaults', async () => {
380
451
  }],
381
452
  noDefaults: true,
382
453
  })
383
-
384
- expect(find1).toEqual({
385
- _id: user1._id,
454
+ expect(find3).toEqual({
455
+ _id: s2.user1Doc._id,
386
456
  addresses: [
387
457
  { city: 'Frankfurt' },
388
458
  { city: 'Christchurch', country: 'New Zealand' },
389
459
  ],
390
- pet: { dog: { _id: dog1._id, user: user1._id }},
460
+ pet: { dog: { _id: s2.dog1Doc._id, user: s2.user1Doc._id }},
391
461
  pets: {
392
462
  dog: [
393
- { _id: dog1._id, user: user1._id },
394
- { _id: dog2._id },
463
+ { _id: s2.dog1Doc._id, user: s2.user1Doc._id },
464
+ { _id: s2.dog2Doc._id },
395
465
  ],
396
466
  },
397
- dogs: [{ _id: dog1._id, user: user1._id }],
467
+ dogs: [{ _id: s2.dog1Doc._id, user: s2.user1Doc._id }],
398
468
  })
399
469
 
400
- let find2 = await db.user.findOne({
401
- query: user1._id,
470
+ // Test manager option noDefaults = ['dogs']
471
+ const find4 = await s2.db.user.findOne({
472
+ query: s2.user1Doc._id,
402
473
  populate: ['pet.dog', 'pets.dog', {
403
474
  from: 'dog',
404
475
  localField: '_id',
405
476
  foreignField: 'user',
406
477
  as: 'dogs',
407
478
  }],
408
- noDefaults: ['dogs', 'pet.dog'],
409
479
  })
410
-
411
- expect(find2).toEqual({
412
- _id: user1._id,
480
+ expect(find4).toEqual({
481
+ _id: s2.user1Doc._id,
413
482
  name: 'Martin Luther',
414
483
  addresses: [
415
484
  { city: 'Frankfurt', country: 'Germany' },
416
485
  { city: 'Christchurch', country: 'New Zealand' },
417
486
  ],
418
487
  address: { country: 'Germany' },
419
- pet: { dog: { _id: dog1._id, user: user1._id }},
488
+ pet: { dog: { _id: s2.dog1Doc._id, user: s2.user1Doc._id, name: 'Scruff' }},
420
489
  pets: {
421
490
  dog: [
422
- { _id: dog1._id, name: 'Scruff', user: user1._id },
423
- { _id: dog2._id, name: 'Scruff' },
491
+ { _id: s2.dog1Doc._id, name: 'Scruff', user: s2.user1Doc._id },
492
+ { _id: s2.dog2Doc._id, name: 'Scruff' },
424
493
  ],
425
494
  },
426
- dogs: [{ _id: dog1._id, user: user1._id }],
495
+ dogs: [{ _id: s2.dog1Doc._id, user: s2.user1Doc._id }], // should not have a default name
427
496
  })
428
497
  })
429
498