monastery 3.1.0 → 3.2.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.2.0](https://github.com/boycce/monastery/compare/3.1.0...3.2.0) (2024-07-17)
6
+
7
+ ### [3.1.1](https://github.com/boycce/monastery/compare/3.1.0...3.1.1) (2024-05-27)
8
+
5
9
  ## [3.1.0](https://github.com/boycce/monastery/compare/3.0.23...3.1.0) (2024-05-27)
6
10
 
7
11
  ### [3.0.23](https://github.com/boycce/monastery/compare/3.0.22...3.0.23) (2024-05-25)
@@ -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,7 +15,7 @@ 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)*
@@ -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,8 @@ 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 for this field name(s)
36
+ * @param {array|string|true} <opts.skipValidation> - skip validation for these fields, or pass `true` to skip
37
+ * all fields and hooks
37
38
  * @param {boolean} <opts.timestamps> - whether `createdAt` and `updatedAt` are automatically inserted
38
39
  * @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
39
40
  * default, but false on update
@@ -65,8 +66,9 @@ Model.prototype.insert = async function (opts) {
65
66
  Model.prototype.find = async function (opts, _one) {
66
67
  /**
67
68
  * Finds document(s), with auto population
68
- * @param {object} opts
69
+ * @param {object} opts (todo doc getSignedUrls like in the doc)
69
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']
70
72
  * @param {array} <opts.populate> - population, see docs
71
73
  * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
72
74
  * @param {object} <opts.query> - mongodb query object
@@ -148,7 +150,7 @@ Model.prototype.find = async function (opts, _one) {
148
150
  }
149
151
  if (arrayTarget) {
150
152
  // Create lookup
151
- lookups.push({
153
+ lookups.push({
152
154
  $lookup: {
153
155
  as: path,
154
156
  from: modelName,
@@ -164,7 +166,7 @@ Model.prototype.find = async function (opts, _one) {
164
166
  // Convert array into a document for non-array targets
165
167
  (opts.addFields = opts.addFields || {})[path] = { '$arrayElemAt': [ '$' + path, 0 ] }
166
168
  // Create basic lookup
167
- lookups.push({
169
+ lookups.push({
168
170
  $lookup: {
169
171
  as: path,
170
172
  from: modelName,
@@ -215,6 +217,7 @@ Model.prototype.findOneAndUpdate = async function (opts) {
215
217
  * Find and update document(s) with auto population
216
218
  * @param {object} opts
217
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']
218
221
  * @param {array} <opts.populate> - find population, see docs
219
222
  * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
220
223
  * @param {object} <opts.query> - mongodb query object
@@ -223,7 +226,8 @@ Model.prototype.findOneAndUpdate = async function (opts) {
223
226
  *
224
227
  * Update options:
225
228
  * @param {object|array} opts.data - mongodb document update object(s)
226
- * @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
229
+ * @param {array|string|true} <opts.skipValidation>- skip validation for these fields, or pass `true` to skip
230
+ * all fields and hooks
227
231
  * @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
228
232
  * @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
229
233
  * default, but false on update
@@ -261,7 +265,8 @@ Model.prototype.update = async function (opts, type='update') {
261
265
  * @param {array|string} <opts.project> - return only these fields, ignores blacklisting
262
266
  * @param {object} <opts.query> - mongodb query object
263
267
  * @param {boolean} <opts.respond> - automatically call res.json/error (requires opts.req)
264
- * @param {array|string|true} <opts.skipValidation> - skip validation for this field name(s)
268
+ * @param {array|string|true} <opts.skipValidation> - skip validation for these fields, or pass `true` to skip
269
+ * all fields and hooks
265
270
  * @param {boolean} <opts.timestamps> - whether `updatedAt` is automatically updated
266
271
  * @param {array|string|false} <opts.validateUndefined> - validates all 'required' undefined fields, true by
267
272
  * default, but false on update
@@ -526,6 +531,9 @@ Model.prototype._queryObject = async function (opts, type, _one) {
526
531
  let order = (opts.sort.match(/:(-?[0-9])/) || [])[1]
527
532
  opts.sort = { [name]: parseInt(order || 1) }
528
533
  }
534
+ if (util.isString(opts.noDefaults)) {
535
+ opts.noDefaults = [opts.noDefaults]
536
+ }
529
537
  }
530
538
 
531
539
  // Data
@@ -591,6 +599,7 @@ Model.prototype._processAfterFind = async function (data, projection={}, afterFi
591
599
  let seriesGroups = []
592
600
  let models = this.manager.models
593
601
  let isArray = util.isArray(data)
602
+ const noDefaults = afterFindContext.noDefaults
594
603
  const projectionKeys = Object.keys(projection)
595
604
  const projectionInclusion = projection[projectionKeys[0]] ? true : false // default false
596
605
  if (!isArray) data = [data]
@@ -623,6 +632,9 @@ Model.prototype._processAfterFind = async function (data, projection={}, afterFi
623
632
  // Ignore default fields that are blacklisted
624
633
  if (this._pathBlacklisted(fullPathWithoutArrays, projectionInclusion, projectionKeys)) continue
625
634
 
635
+ // Ignore default fields if they are included in noDefaults
636
+ if (noDefaults && (noDefaults === true || this._pathBlacklisted(fullPathWithoutArrays, false, noDefaults))) continue
637
+
626
638
  // Set default value
627
639
  const value = util.isFunction(schema.default) ? schema.default(this.manager) : schema.default
628
640
  util.setDeepValue(dataRef, localSchemaFieldPath.replace(/\.0(\.|$)/g, '.$$$1'), value, true, false, true)
@@ -9,7 +9,8 @@ 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
12
+ * @param {array|string|true} <opts.skipValidation> - skip validation for these fields, or pass `true` to skip
13
+ * all fields and hooks
13
14
  * @param {boolean} <opts.timestamps> - whether `createdAt` and `updatedAt` are inserted, or `updatedAt` is
14
15
  * updated, depending on the `options.update` value
15
16
  * @param {boolean(false)} <opts.update> - are we validating for insert or update? todo: change to `type`
@@ -25,6 +26,7 @@ Model.prototype.validate = async function (data, opts) {
25
26
  opts.update = opts.update || opts.findOneAndUpdate
26
27
  opts.insert = !opts.update
27
28
  opts.skipValidation = opts.skipValidation === true ? true : util.toArray(opts.skipValidation||[])
29
+ if (opts.skipValidation === true) return data
28
30
 
29
31
  // Get projection
30
32
  if (opts.project) var projectionValidate = this._getProjectionFromProject(opts.project)
@@ -103,7 +105,6 @@ Model.prototype._validateFields = function (dataRoot, fields, data, opts, parent
103
105
  if (fieldName == 'schema') continue
104
106
  // if (!parentPath && fieldName == 'categories') console.time(fieldName)
105
107
  // if (!parentPath && fieldName == 'categories') console.time(fieldName + 1)
106
- let verrors = []
107
108
  let schema = field.schema
108
109
  let value = fieldsIsArray ? item : (item||{})[fieldName]
109
110
  let indexOrFieldName = fieldsIsArray ? i : fieldName
@@ -139,7 +140,8 @@ Model.prototype._validateFields = function (dataRoot, fields, data, opts, parent
139
140
  // Field is a subdocument
140
141
  if (schema.isObject) {
141
142
  // Object schema errors
142
- errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path)))
143
+ const verrors = this._validateRules(dataRoot, schema, value, opts, path)
144
+ if (verrors.length) errors.push(...verrors)
143
145
  // Recurse if inserting, value is a subdocument, or a deep property (todo: not dot-notation)
144
146
  if (
145
147
  opts.insert ||
@@ -147,7 +149,7 @@ Model.prototype._validateFields = function (dataRoot, fields, data, opts, parent
147
149
  (util.isDefined(opts.validateUndefined) ? opts.validateUndefined : (path||'').indexOf('.') !== -1)
148
150
  ) {
149
151
  var res = this._validateFields(dataRoot, field, value, opts, path, path2)
150
- errors.push(...res[0])
152
+ if (res[0].length) errors.push(...res[0])
151
153
  }
152
154
  if (util.isDefined(value) && !verrors.length) {
153
155
  data2[indexOrFieldName] = res? res[1] : value
@@ -156,11 +158,12 @@ Model.prototype._validateFields = function (dataRoot, fields, data, opts, parent
156
158
  // Field is an array
157
159
  } else if (schema.isArray) {
158
160
  // Array schema errors
159
- errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path)))
161
+ const verrors = this._validateRules(dataRoot, schema, value, opts, path)
162
+ if (verrors.length) errors.push(...verrors)
160
163
  // Data value is array too
161
164
  if (util.isArray(value)) {
162
165
  var res2 = this._validateFields(dataRoot, field, value, opts, path, path2)
163
- errors.push(...res2[0])
166
+ if (res2[0].length) errors.push(...res2[0])
164
167
  }
165
168
  if (util.isDefined(value) && !verrors.length) {
166
169
  data2[indexOrFieldName] = res2? res2[1] : value
@@ -168,7 +171,8 @@ Model.prototype._validateFields = function (dataRoot, fields, data, opts, parent
168
171
 
169
172
  // Field is a field-type/field-schema
170
173
  } else {
171
- errors.push(...(verrors = this._validateRules(dataRoot, schema, value, opts, path)))
174
+ const verrors = this._validateRules(dataRoot, schema, value, opts, path)
175
+ if (verrors.length) errors.push(...verrors)
172
176
  if (util.isDefined(value) && !verrors.length) data2[indexOrFieldName] = value
173
177
  }
174
178
  // if (!parentPath && fieldName == 'categories') console.timeEnd(fieldName)
@@ -212,9 +216,11 @@ Model.prototype._validateRules = function (dataRoot, fieldSchema, value, opts, p
212
216
  // Make sure there is numerical character representing arrays
213
217
  let skippedFieldChunks = skippedField.split('.')
214
218
  for (let i=0, l=pathChunks.length; i<l; i++) {
215
- if (pathChunks[i].match(/^[0-9]+$/)
216
- && skippedFieldChunks[i]
217
- && !skippedFieldChunks[i].match(/^\$$|^[0-9]+$/)) {
219
+ if (
220
+ pathChunks[i].match(/^[0-9]+$/)
221
+ && skippedFieldChunks[i]
222
+ && !skippedFieldChunks[i].match(/^\$$|^[0-9]+$/)
223
+ ) {
218
224
  skippedFieldChunks.splice(i, 0, '$')
219
225
  }
220
226
  }
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.0",
5
+ "version": "3.2.0",
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