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 +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 +12 -2
- package/docs/model/validate.md +1 -1
- package/docs/readme.md +4 -0
- package/docs/transactions.md +26 -0
- package/lib/model-crud.js +15 -6
- package/lib/model-validate.js +21 -13
- 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.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)
|
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,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)
|
|
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
|
|
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,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
|
|
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
|
|
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
|
|
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)
|
package/lib/model-validate.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
218
|
-
|
|
219
|
-
|
|
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.
|
|
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.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
|
-
|
|
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
|
|