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 +4 -0
- package/docs/definition/index.md +4 -1
- package/docs/manager/index.md +1 -1
- package/docs/model/find.md +1 -0
- package/docs/model/insert.md +1 -1
- package/docs/model/update.md +1 -1
- package/docs/model/validate.md +1 -1
- package/docs/readme.md +4 -0
- package/docs/transactions.md +26 -0
- package/lib/model-crud.js +18 -6
- package/lib/model-validate.js +16 -10
- package/lib/util.js +18 -10
- package/package.json +2 -1
- package/test/comparison.js +122 -0
- package/test/crud.js +165 -9
- package/test/index.test.js +1 -0
- package/test/util.js +16 -0
- package/test/validate.js +7 -0
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)
|
package/docs/definition/index.md
CHANGED
|
@@ -48,7 +48,7 @@ Model definition object.
|
|
|
48
48
|
address: {
|
|
49
49
|
name: { type: 'string' },
|
|
50
50
|
type: { type: 'string' },
|
|
51
|
-
schema: {
|
|
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
|
|
package/docs/manager/index.md
CHANGED
|
@@ -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
|
|
package/docs/model/find.md
CHANGED
|
@@ -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'
|
package/docs/model/insert.md
CHANGED
|
@@ -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)
|
|
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
|
|
package/docs/model/update.md
CHANGED
|
@@ -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)
|
|
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)*
|
package/docs/model/validate.md
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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)
|
package/lib/model-validate.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
216
|
-
|
|
217
|
-
|
|
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.
|
|
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
|
-
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
689
|
+
console.timeEnd('update large document')
|
|
599
690
|
// Check
|
|
600
691
|
await expect(update).toEqual(removePrunedProperties(largePayload))
|
|
601
692
|
// Find
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
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
|
package/test/index.test.js
CHANGED
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
|
|