monastery 3.1.1 → 3.2.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,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.2.1](https://github.com/boycce/monastery/compare/3.2.0...3.2.1) (2024-08-05)
6
+
7
+ ## [3.2.0](https://github.com/boycce/monastery/compare/3.1.0...3.2.0) (2024-07-17)
8
+
5
9
  ### [3.1.1](https://github.com/boycce/monastery/compare/3.1.0...3.1.1) (2024-05-27)
6
10
 
7
11
  ## [3.1.0](https://github.com/boycce/monastery/compare/3.0.23...3.1.0) (2024-05-27)
@@ -48,7 +48,7 @@ Model definition object.
48
48
  address: {
49
49
  name: { type: 'string' },
50
50
  type: { type: 'string' },
51
- schema: { minLength: 1 },
51
+ schema: { strict: false }, // allows non-defined fields to save
52
52
  },
53
53
  pets: db.arrayWithSchema(
54
54
  [{
@@ -135,6 +135,9 @@ fieldType: {
135
135
  // below for more information
136
136
  index: true|1|-1|'text'|'unique'|Object,
137
137
 
138
+ // Allows non-defined fields to save
139
+ strict: false,
140
+
138
141
  // The field won't stored, handy for fields that get populated with documents, see ./find for more details
139
142
  virtual: true
140
143
 
@@ -50,7 +50,7 @@ db.onError((err) => {
50
50
  ### Properties
51
51
 
52
52
  - `manager.db`: Raw Mongo db instance
53
- - `manager.client`: Raw Mongo client instance
53
+ - `manager.client`: Raw Mongo client instance, which you can use to create [transactions](#transactions).
54
54
 
55
55
  ### Methods
56
56
 
@@ -14,6 +14,7 @@ Find document(s) in a collection, and call the model hook: `afterFind`
14
14
  - `query` *(object\|id)*: [`MongoDB query document`](https://www.mongodb.com/docs/v5.0/tutorial/query-documents/), or id
15
15
  - [[`blacklist`](#blacklisting)] *(array\|string\|false)*: augment `definition.findBL`. `false` will remove all blacklisting
16
16
  - [`getSignedUrls`] *(boolean)*: get signed urls for all image objects
17
+ - [`noDefaults`] *(boolean\|string\|array)*: dont add defaults for any matching paths, e.g. ['pet.name']]
17
18
  - [[`populate`](#populate)] *(array)*
18
19
  - [`project`] *(string\|array\|object)*: return only these fields, ignores blacklisting
19
20
  - [`sort`] *(string\|array\|object)*: same as the mongodb option, but allows string parsing e.g. 'name', 'name:1'
@@ -14,7 +14,7 @@ Validate and insert document(s) in a collection and calls model hooks: `beforeIn
14
14
  - `data` *(object\|array)* - Data that is validated against the model fields. Key names can be in dot or bracket notation which is handy for HTML FormData.
15
15
  - [[`blacklist`](#blacklisting)] *(array\|string\|false)*: augment `definition.insertBL`. `false` will remove all blacklisting
16
16
  - [`project`] *(string\|array\|object)*: project these fields, ignores blacklisting
17
- - [`skipValidation`] (string\|array\|boolean): skip validation for these field name(s), or `true` for all fields
17
+ - [`skipValidation`] *(string\|array\|boolean)*: skip validation for these fields, or pass `true` to skip all fields and validation hooks
18
18
  - [`timestamps`] *(boolean)*: whether `createdAt` and `updatedAt` are automatically inserted, defaults to `manager.timestamps`
19
19
  - [[`any mongodb option`](https://mongodb.github.io/node-mongodb-native/5.9/classes/Collection.html#insertMany)] *(any)*
20
20
 
@@ -15,14 +15,24 @@ Update document(s) in a collection and calls model hooks: `beforeUpdate`, `afte
15
15
  - [`data`](#data) *(object)* - data that's validated against the model fields (always wrapped in `{ $set: .. }`)
16
16
  - [[`blacklist`](#blacklisting)]*(array\|string\|false)*: augment `definition.updateBL`. `false` will remove all blacklisting
17
17
  - [`project`] *(string\|array\|object)*: project these fields, ignores blacklisting
18
- - [`skipValidation`] (string\|array\|boolean): skip validation for these field name(s), or `true` for all fields
18
+ - [`skipValidation`] *(string\|array\|boolean)*: skip validation for these fields, or pass `true` to skip all fields and validation hooks
19
19
  - [`sort`] *(string\|object\|array)*: same as the mongodb option, but allows for string parsing e.g. 'name', 'name:1'
20
20
  - [`timestamps`] *(boolean)*: whether `updatedAt` is automatically updated, defaults to the `manager.timestamps` value
21
21
  - [[`any mongodb option`](https://mongodb.github.io/node-mongodb-native/5.9/classes/Collection.html#updateMany)] *(any)*
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
 
@@ -14,9 +14,9 @@ Validate a model and calls the model hook: `beforeValidate`
14
14
 
15
15
  [`options`] *(object)*
16
16
 
17
- - [`skipValidation`] (string\|array\/boolean): skip validation for thse field name(s), or `true` for all fields
18
17
  - [[`blacklist`](#blacklisting)] *(array\|string\|false)*: augment the model's blacklist. `false` will remove all blacklisting
19
18
  - [`project`] *(string\|array\|object)*: project these fields, ignores blacklisting
19
+ - [`skipValidation`] *(string\|array\/boolean)*: skip validation for these fields, or pass `true` to skip all fields and validation hooks
20
20
  - [`timestamps`] *(boolean)*: whether `createdAt` and `updatedAt` are inserted, or `updatedAt` is updated, depending on the `update` value. Defaults to the `manager.timestamps` value
21
21
  - [`update`] *(boolean)*: If true, required rules will be skipped, defaults to false
22
22
 
package/docs/readme.md CHANGED
@@ -14,6 +14,7 @@
14
14
  * Blacklist sensitive fields once in your model definition, or per operation
15
15
  * Model methods can accept data in bracket (multipart/form) and dot notation, you can also mix these together
16
16
  * Automatic Mongo index creation
17
+ * Documents validate, insert and update 7-10x faster than Mongoose
17
18
 
18
19
  #### Why Monastery over Mongoose?
19
20
 
@@ -94,6 +95,7 @@ You can view MongoDB's [compatibility table here](https://www.mongodb.com/docs/d
94
95
  - db._db moved to db.db
95
96
  - db.catch/then() moved to db.onError/db.onOpen()
96
97
  - next() is now redundant when returning promises from hooks, e.g. `afterFind: [async (data) => {...}]`
98
+ - option `skipValidation: true` now skips validation hooks
97
99
 
98
100
  ## v2 Breaking Changes
99
101
 
@@ -130,6 +132,8 @@ You can view MongoDB's [compatibility table here](https://www.mongodb.com/docs/d
130
132
  - ~~Docs: model.methods~~
131
133
  - ~~Convert hooks to promises~~
132
134
  - ~~added `model.count()`~~
135
+ - Typescript support
136
+ - Add soft remove plugin
133
137
 
134
138
  ## Debugging
135
139
 
@@ -0,0 +1,26 @@
1
+ ---
2
+ title: Transactions
3
+ nav_order: 8
4
+ ---
5
+
6
+ # Transactions
7
+
8
+ You can create a Mongo transaction using the `manager.client` instance:
9
+
10
+ ```js
11
+ try {
12
+ // Create a new session
13
+ var session = db.client.startSession()
14
+ // Start the transaction, any thrown errors rollback all operations within the callback. The callback must return a promise.
15
+ await session.withTransaction(async () => {
16
+ // Important:: You must pass the session to the operations
17
+ await db.person.insert({ data, session })
18
+ })
19
+ } catch (err) {
20
+ console.error(err)
21
+ } finally {
22
+ if (session) {
23
+ await session.endSession()
24
+ }
25
+ }
26
+ ```
package/lib/model-crud.js CHANGED
@@ -33,7 +33,7 @@ Model.prototype.insert = async function (opts) {
33
33
  * @param {array|string|false} <opts.blacklist> - augment schema.insertBL, `false` will remove blacklisting
34
34
  * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
35
35
  * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
36
- * @param {array|string|true} <opts.skipValidation> - skip validation on these fields, or pass `true` to skip
36
+ * @param {array|string|true} <opts.skipValidation> - skip validation for these fields, or pass `true` to skip
37
37
  * all fields and hooks
38
38
  * @param {boolean} <opts.timestamps> - whether `createdAt` and `updatedAt` are automatically inserted
39
39
  * @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
@@ -66,8 +66,9 @@ Model.prototype.insert = async function (opts) {
66
66
  Model.prototype.find = async function (opts, _one) {
67
67
  /**
68
68
  * Finds document(s), with auto population
69
- * @param {object} opts
69
+ * @param {object} opts (todo doc getSignedUrls like in the doc)
70
70
  * @param {array|string|false} <opts.blacklist> - augment schema.findBL, `false` will remove all blacklisting
71
+ * @param {boolean|string|array} <opts.noDefaults> - dont add defaults for any matching paths, e.g. ['pet.name']
71
72
  * @param {array} <opts.populate> - population, see docs
72
73
  * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
73
74
  * @param {object} <opts.query> - mongodb query object
@@ -149,7 +150,7 @@ Model.prototype.find = async function (opts, _one) {
149
150
  }
150
151
  if (arrayTarget) {
151
152
  // Create lookup
152
- lookups.push({
153
+ lookups.push({
153
154
  $lookup: {
154
155
  as: path,
155
156
  from: modelName,
@@ -165,7 +166,7 @@ Model.prototype.find = async function (opts, _one) {
165
166
  // Convert array into a document for non-array targets
166
167
  (opts.addFields = opts.addFields || {})[path] = { '$arrayElemAt': [ '$' + path, 0 ] }
167
168
  // Create basic lookup
168
- lookups.push({
169
+ lookups.push({
169
170
  $lookup: {
170
171
  as: path,
171
172
  from: modelName,
@@ -216,6 +217,7 @@ Model.prototype.findOneAndUpdate = async function (opts) {
216
217
  * Find and update document(s) with auto population
217
218
  * @param {object} opts
218
219
  * @param {array|string|false} <opts.blacklist> - augment findBL/updateBL, `false` will remove all blacklisting
220
+ * @param {boolean|string|array} <opts.noDefaults> - dont add defaults for any matching paths, e.g. ['pet.name']
219
221
  * @param {array} <opts.populate> - find population, see docs
220
222
  * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
221
223
  * @param {object} <opts.query> - mongodb query object
@@ -224,7 +226,7 @@ Model.prototype.findOneAndUpdate = async function (opts) {
224
226
  *
225
227
  * Update options:
226
228
  * @param {object|array} opts.data - mongodb document update object(s)
227
- * @param {array|string|true} <opts.skipValidation>- skip validation on these fields, or pass `true` to skip
229
+ * @param {array|string|true} <opts.skipValidation>- skip validation for these fields, or pass `true` to skip
228
230
  * all fields and hooks
229
231
  * @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
230
232
  * @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
@@ -263,7 +265,7 @@ Model.prototype.update = async function (opts, type='update') {
263
265
  * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
264
266
  * @param {object} <opts.query> - mongodb query object
265
267
  * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
266
- * @param {array|string|true} <opts.skipValidation> - skip validation on these fields, or pass `true` to skip
268
+ * @param {array|string|true} <opts.skipValidation> - skip validation for these fields, or pass `true` to skip
267
269
  * all fields and hooks
268
270
  * @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
269
271
  * @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
@@ -529,6 +531,9 @@ Model.prototype._queryObject = async function (opts, type, _one) {
529
531
  let order = (opts.sort.match(/:(-?[0-9])/) || [])[1]
530
532
  opts.sort = { [name]: parseInt(order || 1) }
531
533
  }
534
+ if (util.isString(opts.noDefaults)) {
535
+ opts.noDefaults = [opts.noDefaults]
536
+ }
532
537
  }
533
538
 
534
539
  // Data
@@ -594,6 +599,7 @@ Model.prototype._processAfterFind = async function (data, projection={}, afterFi
594
599
  let seriesGroups = []
595
600
  let models = this.manager.models
596
601
  let isArray = util.isArray(data)
602
+ const noDefaults = afterFindContext.noDefaults
597
603
  const projectionKeys = Object.keys(projection)
598
604
  const projectionInclusion = projection[projectionKeys[0]] ? true : false // default false
599
605
  if (!isArray) data = [data]
@@ -626,6 +632,9 @@ Model.prototype._processAfterFind = async function (data, projection={}, afterFi
626
632
  // Ignore default fields that are blacklisted
627
633
  if (this._pathBlacklisted(fullPathWithoutArrays, projectionInclusion, projectionKeys)) continue
628
634
 
635
+ // Ignore default fields if they are included in noDefaults
636
+ if (noDefaults && (noDefaults === true || this._pathBlacklisted(fullPathWithoutArrays, false, noDefaults))) continue
637
+
629
638
  // Set default value
630
639
  const value = util.isFunction(schema.default) ? schema.default(this.manager) : schema.default
631
640
  util.setDeepValue(dataRef, localSchemaFieldPath.replace(/\.0(\.|$)/g, '.$$$1'), value, true, false, true)
@@ -9,7 +9,7 @@ Model.prototype.validate = async function (data, opts) {
9
9
  * @param {object} <opts>
10
10
  * @param {array|string|false} <opts.blacklist> - augment insertBL/updateBL, `false` will remove blacklisting
11
11
  * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
12
- * @param {array|string|true} <opts.skipValidation> - skip validation on these fields, or pass `true` to skip
12
+ * @param {array|string|true} <opts.skipValidation> - skip validation for these fields, or pass `true` to skip
13
13
  * all fields and hooks
14
14
  * @param {boolean} <opts.timestamps> - whether `createdAt` and `updatedAt` are inserted, or `updatedAt` is
15
15
  * updated, depending on the `options.update` value
@@ -105,7 +105,6 @@ Model.prototype._validateFields = function (dataRoot, fields, data, opts, parent
105
105
  if (fieldName == 'schema') continue
106
106
  // if (!parentPath && fieldName == 'categories') console.time(fieldName)
107
107
  // if (!parentPath && fieldName == 'categories') console.time(fieldName + 1)
108
- let verrors = []
109
108
  let schema = field.schema
110
109
  let value = fieldsIsArray ? item : (item||{})[fieldName]
111
110
  let indexOrFieldName = fieldsIsArray ? i : fieldName
@@ -141,28 +140,32 @@ Model.prototype._validateFields = function (dataRoot, fields, data, opts, parent
141
140
  // Field is a subdocument
142
141
  if (schema.isObject) {
143
142
  // Object schema errors
144
- errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path)))
143
+ let res
144
+ const verrors = this._validateRules(dataRoot, schema, value, opts, path)
145
+ if (verrors.length) errors.push(...verrors)
145
146
  // Recurse if inserting, value is a subdocument, or a deep property (todo: not dot-notation)
146
147
  if (
147
148
  opts.insert ||
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
- errors.push(...res[0])
152
+ res = this._validateFields(dataRoot, field, value, opts, path, path2)
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
161
- errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path)))
162
+ let res2
163
+ const verrors = this._validateRules(dataRoot, schema, value, opts, path)
164
+ if (verrors.length) errors.push(...verrors)
162
165
  // Data value is array too
163
166
  if (util.isArray(value)) {
164
- var res2 = this._validateFields(dataRoot, field, value, opts, path, path2)
165
- errors.push(...res2[0])
167
+ res2 = this._validateFields(dataRoot, field, value, opts, path, path2)
168
+ if (res2[0].length) errors.push(...res2[0])
166
169
  }
167
170
  if (util.isDefined(value) && !verrors.length) {
168
171
  data2[indexOrFieldName] = res2? res2[1] : value
@@ -170,7 +173,8 @@ Model.prototype._validateFields = function (dataRoot, fields, data, opts, parent
170
173
 
171
174
  // Field is a field-type/field-schema
172
175
  } else {
173
- errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path)))
176
+ const verrors = this._validateRules(dataRoot, schema, value, opts, path)
177
+ if (verrors.length) errors.push(...verrors)
174
178
  if (util.isDefined(value) && !verrors.length) data2[indexOrFieldName] = value
175
179
  }
176
180
  // if (!parentPath && fieldName == 'categories') console.timeEnd(fieldName)
@@ -207,6 +211,8 @@ Model.prototype._validateRules = function (dataRoot, fieldSchema, value, opts, p
207
211
  if (opts.skipValidation === true) return []
208
212
 
209
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')/////
210
216
  if (opts.skipValidation.length) {
211
217
  //console.log(path, field, opts)
212
218
  let pathChunks = path.split('.')
@@ -214,9 +220,11 @@ Model.prototype._validateRules = function (dataRoot, fieldSchema, value, opts, p
214
220
  // Make sure there is numerical character representing arrays
215
221
  let skippedFieldChunks = skippedField.split('.')
216
222
  for (let i=0, l=pathChunks.length; i<l; i++) {
217
- if (pathChunks[i].match(/^[0-9]+$/)
218
- && skippedFieldChunks[i]
219
- && !skippedFieldChunks[i].match(/^\$$|^[0-9]+$/)) {
223
+ if (
224
+ pathChunks[i].match(/^[0-9]+$/)
225
+ && skippedFieldChunks[i]
226
+ && !skippedFieldChunks[i].match(/^\$$|^[0-9]+$/)
227
+ ) {
220
228
  skippedFieldChunks.splice(i, 0, '$')
221
229
  }
222
230
  }
package/lib/util.js CHANGED
@@ -35,7 +35,7 @@ module.exports = {
35
35
  let obj2 = Array.isArray(obj)? [] : {}
36
36
  for (let key in obj) {
37
37
  let v = obj[key]
38
- obj2[key] = (typeof v === 'object' && !this.isIdFast(v))? this.deepCopy(v) : v
38
+ obj2[key] = (typeof v === 'object' && !this.isHex24(v))? this.deepCopy(v) : v //isHex24
39
39
  }
40
40
  return obj2
41
41
  },
@@ -113,15 +113,23 @@ module.exports = {
113
113
  else return false
114
114
  },
115
115
 
116
- isIdFast: function(value) {
117
- // Check quickly if the value is an ObjectId. We can use db.isId() but this may be slower
118
- // console.log(isId('66333b1b3343d7e3b200005b')) = true
119
- // console.log(isId(db.id())) = true
120
- // console.log(isId(null)) = undefined
121
- // console.log(isId('qwefqwefqwef')) = undefined
122
- // console.log(isId({})) = undefined
123
- // console.log(isId(['66333b1b3343d7e3b200005b'])) = undefined
124
- if ((value||'').toString()?.match(/^[0-9a-fA-F]{24}$/) && !this.isArray(value)) return true
116
+ isHex24: (value) => {
117
+ // Fast function to check if the length is exactly 24 and all characters are valid hexadecimal digits
118
+ const str = (value||'').toString()
119
+ if (str.length !== 24) return false
120
+ else if (Array.isArray(value)) return false
121
+
122
+ // Check if all characters are valid hexadecimal digits
123
+ for (let i=24; i--;) {
124
+ const charCode = str.charCodeAt(i)
125
+ const isDigit = charCode >= 48 && charCode <= 57 // '0' to '9'
126
+ const isLowerHex = charCode >= 97 && charCode <= 102 // 'a' to 'f'
127
+ const isUpperHex = charCode >= 65 && charCode <= 70 // 'A' to 'F'
128
+ if (!isDigit && !isLowerHex && !isUpperHex) {
129
+ return false
130
+ }
131
+ }
132
+ return true
125
133
  },
126
134
 
127
135
  isNumber: function(value) {
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.1.1",
5
+ "version": "3.2.1",
6
6
  "license": "MIT",
7
7
  "repository": "github:boycce/monastery",
8
8
  "homepage": "https://boycce.github.io/monastery/",
@@ -20,6 +20,7 @@
20
20
  ],
21
21
  "scripts": {
22
22
  "dev": "npm run lint & jest --watchAll --runInBand --verbose=false",
23
+ "dev-profile": "node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand",
23
24
  "docs": "cd docs && bundle exec jekyll serve --livereload --livereload-port 4001",
24
25
  "lint": "eslint ./lib ./plugins ./test",
25
26
  "mona": "nodemon resources/mona.js",
@@ -0,0 +1,122 @@
1
+ const monastery = require('../lib/index.js')
2
+ const mongoose = require('mongoose') // very slow initialisation
3
+
4
+ test('comparison insert', async () => {
5
+ const db = monastery('127.0.0.1/monastery', { timestamps: false })
6
+ const Test1 = db.model('Test1', { fields: {
7
+ raw: {
8
+ words: [{
9
+ id: { type: 'number' },
10
+ isVertical: { type: 'boolean' },
11
+ text: { type: 'string' },
12
+ confidence: { type: 'number' },
13
+ }],
14
+ },
15
+ }})
16
+ function getData() {
17
+ return {
18
+ words: Array(500).fill(0).map((_, i) => ({
19
+ id: i,
20
+ isVertical: i % 2 === 0,
21
+ text: 'Test ' + i,
22
+ confidence: Math.random(),
23
+ sub: {
24
+ greeting: 'Hello ' + i,
25
+ },
26
+ })),
27
+ }
28
+ }
29
+ console.time('Monastery')
30
+ for (let i = 0; i < 100; ++i) {
31
+ const res = await Test1.insert({ data: { raw: getData() } })
32
+ if (i == 50) console.log(res.raw.words[0])
33
+ }
34
+ console.timeEnd('Monastery') // 320ms =50, 935ms =500
35
+ db.close()
36
+
37
+
38
+ await mongoose.connect('mongodb://127.0.0.1:27017/mongoose_test')
39
+ const schema = new mongoose.Schema({
40
+ raw: {
41
+ words: [{
42
+ id: Number,
43
+ isVertical: Boolean,
44
+ text: String,
45
+ confidence: Number,
46
+ }],
47
+ },
48
+ })
49
+ const Test2 = mongoose.model('Test2', schema)
50
+ console.time('Mongoose')
51
+ for (let i = 0; i < 100; ++i) {
52
+ const res = await Test2.create({ raw: getData() })
53
+ if (i == 50) console.log(res.raw.words[0])
54
+ }
55
+ console.timeEnd('Mongoose') // 1202ms =50, 9203ms =500
56
+ //result: monastery is 4.5-10x faster than mongoose
57
+ }, 20000)
58
+
59
+ test('comparison validate', async () => {
60
+ function getData() {
61
+ return {
62
+ words: Array(5000).fill(0).map((_, i) => ({
63
+ id: i,
64
+ isVertical: i % 2 === 0,
65
+ // text: 'Test ' + i,
66
+ confidence: Math.random(),
67
+ sub: {
68
+ greeting: 'Hello ' + i,
69
+ },
70
+ })),
71
+ }
72
+ }
73
+ const db = monastery('127.0.0.1/monastery', { timestamps: false })
74
+ const Test1 = db.model('Test1', { fields: {
75
+ raw: {
76
+ words: [{
77
+ id: { type: 'number' },
78
+ isVertical: { type: 'boolean' },
79
+ text: { type: 'string', default: 'hi'},
80
+ confidence: { type: 'number' },
81
+ sub: {
82
+ greeting: { type: 'string' },
83
+ },
84
+ }],
85
+ },
86
+ }})
87
+ const data1 = getData()
88
+ console.time('Monastery')
89
+ const test1 = await Test1.validate({
90
+ raw: data1,
91
+ })
92
+ console.log(test1.raw.words[0])
93
+ console.timeEnd('Monastery') // 59ms =5000, 495ms =50000
94
+
95
+ await mongoose.connect('mongodb://127.0.0.1:27017/mongoose_test')
96
+ const schema = new mongoose.Schema({
97
+ raw: {
98
+ words: [{
99
+ id: Number,
100
+ isVertical: Boolean,
101
+ text: { type: String, default: 'hi' },
102
+ confidence: Number,
103
+ sub: {
104
+ greeting: String,
105
+ },
106
+ }],
107
+ },
108
+ })
109
+ const Test2 = mongoose.model('Test2', schema)
110
+
111
+ const data2 = getData()
112
+ console.time('Mongoose 1')
113
+ const test2 = new Test2({ raw: data2 }) // calls validate too
114
+ console.timeEnd('Mongoose 1') // 429ms =5000, 3722ms =50000
115
+ console.time('Mongoose 2')
116
+ test2.validateSync()
117
+ console.timeEnd('Mongoose 2') // 284ms =5000, 2486ms =50000
118
+ console.log(test2.raw.words[0])
119
+
120
+ //result: monastery is 7.3x faster than mongoose
121
+ }, 20000)
122
+
package/test/crud.js CHANGED
@@ -337,9 +337,100 @@ 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
+ },
351
+ })
352
+ db.model('dog', {
353
+ fields: {
354
+ name: { type: 'string', default: 'Scruff' },
355
+ user: { model: 'user' },
356
+ },
357
+ })
358
+
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({
364
+ addresses: [
365
+ { city: 'Frankfurt' },
366
+ { city: 'Christchurch', country: 'New Zealand' },
367
+ ],
368
+ pet: { dog: dog1._id },
369
+ pets: { dog: [dog1._id, dog2._id]},
370
+ })
371
+ await db.dog._update(dog1._id, { $set: { user: user1._id }})
372
+
373
+ let find1 = await db.user.findOne({
374
+ query: user1._id,
375
+ populate: ['pet.dog', 'pets.dog', {
376
+ from: 'dog',
377
+ localField: '_id',
378
+ foreignField: 'user',
379
+ as: 'dogs',
380
+ }],
381
+ noDefaults: true,
382
+ })
383
+
384
+ expect(find1).toEqual({
385
+ _id: user1._id,
386
+ addresses: [
387
+ { city: 'Frankfurt' },
388
+ { city: 'Christchurch', country: 'New Zealand' },
389
+ ],
390
+ pet: { dog: { _id: dog1._id, user: user1._id }},
391
+ pets: {
392
+ dog: [
393
+ { _id: dog1._id, user: user1._id },
394
+ { _id: dog2._id },
395
+ ],
396
+ },
397
+ dogs: [{ _id: dog1._id, user: user1._id }],
398
+ })
399
+
400
+ let find2 = await db.user.findOne({
401
+ query: user1._id,
402
+ populate: ['pet.dog', 'pets.dog', {
403
+ from: 'dog',
404
+ localField: '_id',
405
+ foreignField: 'user',
406
+ as: 'dogs',
407
+ }],
408
+ noDefaults: ['dogs', 'pet.dog'],
409
+ })
410
+
411
+ expect(find2).toEqual({
412
+ _id: user1._id,
413
+ name: 'Martin Luther',
414
+ addresses: [
415
+ { city: 'Frankfurt', country: 'Germany' },
416
+ { city: 'Christchurch', country: 'New Zealand' },
417
+ ],
418
+ address: { country: 'Germany' },
419
+ pet: { dog: { _id: dog1._id, user: user1._id }},
420
+ pets: {
421
+ dog: [
422
+ { _id: dog1._id, name: 'Scruff', user: user1._id },
423
+ { _id: dog2._id, name: 'Scruff' },
424
+ ],
425
+ },
426
+ dogs: [{ _id: dog1._id, user: user1._id }],
427
+ })
428
+ })
429
+
340
430
  test('update general', async () => {
341
431
  let user = db.model('user', {
342
432
  fields: {
433
+ age: { type: 'number' },
343
434
  name: { type: 'string' },
344
435
  },
345
436
  })
@@ -364,8 +455,8 @@ test('update general', async () => {
364
455
  ])
365
456
 
366
457
  // Update
367
- await expect(user.update({ query: inserted._id, data: { name: 'Martin Luther2' }}))
368
- .resolves.toEqual({ name: 'Martin Luther2' })
458
+ await expect(user.update({ query: inserted._id, data: { name: 'Martin Luther2', age: Infinity }}))
459
+ .resolves.toEqual({ name: 'Martin Luther2', age: Infinity })
369
460
 
370
461
  // Update (no/empty data object)
371
462
  await expect(user.update({ query: inserted._id, data: {}}))
@@ -590,20 +681,20 @@ test('update large document', async () => {
590
681
  // Insert
591
682
  let inserted = await large._insert({})
592
683
  // Update
593
- // console.time('update large document')
684
+ console.time('update large document')
594
685
  let update = await large.update({
595
686
  query: inserted._id,
596
687
  data: largePayload,
597
688
  })
598
- // console.timeEnd('update large document')
689
+ console.timeEnd('update large document')
599
690
  // Check
600
691
  await expect(update).toEqual(removePrunedProperties(largePayload))
601
692
  // Find
602
- // console.time('find large document')
603
- // await large.findOne({
604
- // query: inserted._id,
605
- // })
606
- // console.timeEnd('find large document')
693
+ console.time('find large document')
694
+ await large.findOne({
695
+ query: inserted._id,
696
+ })
697
+ console.timeEnd('find large document')
607
698
 
608
699
  function removePrunedProperties(entity) {
609
700
  for (let entitiesKey of [
@@ -633,6 +724,71 @@ test('update large document', async () => {
633
724
  }
634
725
  })
635
726
 
727
+ // test('update larger document', async () => {
728
+ // // todo: sereach util.deepCopy
729
+ // // todo: check castIds and any other recursive functions
730
+ // // todo: move default fields to before validate
731
+ // db.model('a', { fields: {} })
732
+ // db.model('b', { fields: {} })
733
+ // db.model('c', { fields: {} })
734
+ // db.model('d', { fields: {} })
735
+ // db.model('e', { fields: {} })
736
+ // try {
737
+ // var larger = db.model('larger', require('../resources/fixtures/larger-definition.js'))
738
+ // var largerPayload = require('../resources/fixtures/larger-payload.json').version
739
+ // } catch (e) {
740
+ // // ignore publicly for now
741
+ // return
742
+ // }
743
+ // // Validate
744
+ // console.time('validate larger document')
745
+ // let validate = await larger.validate(largerPayload, { update: true })
746
+ // console.timeEnd('validate larger document')
747
+ // // // Insert
748
+ // // let inserted = await larger._insert({})
749
+ // // // Update
750
+ // // console.time('update larger document')
751
+ // // let update = await larger.update({
752
+ // // query: inserted._id,
753
+ // // data: largerPayload,
754
+ // // })
755
+ // // console.timeEnd('update larger document')
756
+ // // // Check
757
+ // // await expect(update).toEqual(convertIdsToObjectIdsAndRemoveActuallyValues(largerPayload))
758
+ // // // Find
759
+ // // console.time('find larger document')
760
+ // // await larger.findOne({
761
+ // // query: inserted._id,
762
+ // // })
763
+ // // console.timeEnd('find larger document')
764
+
765
+ // // function convertIdsToObjectIdsAndRemoveActuallyValues(entity) {
766
+ // // for (let entitiesKey of [
767
+ // // 'components', 'connections', 'bridges', 'openings', 'spaces', 'elements', 'elementTypes', 'categories',
768
+ // // 'typologies',
769
+ // // ]) {
770
+ // // if (entity[entitiesKey]) {
771
+ // // for (let i=0, l=entity[entitiesKey].length; i<l; i++) {
772
+ // // entity[entitiesKey][i] = convertIdsToObjectIdsAndRemoveActuallyValues(entity[entitiesKey][i])
773
+ // // }
774
+ // // }
775
+ // // }
776
+ // // // remove index and actually keys
777
+ // // if (entity.metrics) {
778
+ // // delete entity.index
779
+ // // for (let key in entity.metrics) {
780
+ // // if (entity.metrics[key].S_ == 'A_') delete entity.metrics[key].S_
781
+ // // delete entity.metrics[key].A_
782
+ // // }
783
+ // // }
784
+ // // // convert _id to ObjectId
785
+ // // if (entity._id) {
786
+ // // entity._id = db.id(entity._id)
787
+ // // }
788
+ // // return entity
789
+ // // }
790
+ // })
791
+
636
792
  test('findOneAndUpdate general', async () => {
637
793
  // todo: test all findOneAndUpdate options (e.g. array population)
638
794
  // todo: test find & update hooks
@@ -25,3 +25,4 @@ require('./populate.js')
25
25
  require('./validate.js')
26
26
  require('./plugin-images.js')
27
27
  require('./virtuals.js')
28
+ // require('./comparison.js')
package/test/util.js CHANGED
@@ -35,9 +35,25 @@ test('util > isId', async () => {
35
35
  expect(util.isId('')).toEqual(false)
36
36
  expect(util.isId(1234)).toEqual(false)
37
37
  expect(util.isId('1234')).toEqual(false)
38
+ expect(util.isId(null)).toEqual(false)
39
+ expect(util.isId({})).toEqual(false)
40
+ expect(util.isId(['5ff50fe955da2c00170de734'])).toEqual(false)
38
41
  expect(util.isId('5ff50fe955da2c00170de734')).toEqual(true)
42
+ expect(util.isId(monastery.prototype.id())).toEqual(true)
39
43
  })
40
44
 
45
+ test('util > isHex24', async () => {
46
+ expect(util.isHex24('')).toEqual(false)
47
+ expect(util.isHex24(1234)).toEqual(false)
48
+ expect(util.isHex24('1234')).toEqual(false)
49
+ expect(util.isHex24(null)).toEqual(false)
50
+ expect(util.isHex24({})).toEqual(false)
51
+ expect(util.isHex24(['5ff50fe955da2c00170de734'])).toEqual(false)
52
+ expect(util.isHex24('5ff50fe955da2c00170de734')).toEqual(true)
53
+ expect(util.isHex24(monastery.prototype.id())).toEqual(true)
54
+ })
55
+
56
+
41
57
  test('util > arrayWithSchema', async () => {
42
58
  let res = monastery.prototype.arrayWithSchema([{ name: { type: 'string' }}], { minLength: 1 })
43
59
  expect(res).toContainEqual({ name: { type: 'string' }})
package/test/validate.js CHANGED
@@ -787,6 +787,7 @@ test('validation custom rules', async () => {
787
787
 
788
788
  test('validated data', async () => {
789
789
  let fields = {
790
+ age: { type: 'number' },
790
791
  name: { type: 'string' },
791
792
  names: [{ type: 'string' }],
792
793
  animals: {
@@ -808,6 +809,12 @@ test('validated data', async () => {
808
809
  await expect(user.validate({ name: 'Martin Luther' })).resolves.toEqual({ name: 'Martin Luther' })
809
810
  await expect(user.validate({ name: null })).resolves.toEqual({ name: null })
810
811
 
812
+ // Number data
813
+ await expect(user.validate({ age: 1 })).resolves.toEqual({ age: 1 })
814
+ await expect(user.validate({ age: Infinity })).resolves.toEqual({ age: Infinity })
815
+ await expect(user.validate({ age: NaN })).resolves.toEqual({ age: NaN })
816
+ await expect(user.validate({ age: null })).resolves.toEqual({ age: null })
817
+
811
818
  // Array data
812
819
  await expect(user.validate({ names: ['blue'] })).resolves.toEqual({ names: ['blue'] })
813
820