monastery 3.0.23 → 3.1.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,8 @@
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.1.0](https://github.com/boycce/monastery/compare/3.0.23...3.1.0) (2024-05-27)
6
+
5
7
  ### [3.0.23](https://github.com/boycce/monastery/compare/3.0.22...3.0.23) (2024-05-25)
6
8
 
7
9
  ### [3.0.22](https://github.com/boycce/monastery/compare/3.0.21...3.0.22) (2024-05-08)
@@ -91,6 +91,7 @@ Model.prototype._validateFields = function (dataRoot, fields, data, opts, parent
91
91
  let timestamps = util.isDefined(opts.timestamps) ? opts.timestamps : this.manager.opts.timestamps
92
92
  let dataArray = util.forceArray(data)
93
93
  let data2 = fieldsIsArray ? [] : {}
94
+ let notStrict = fields.schema.strict === false
94
95
 
95
96
  for (let i=0, l=dataArray.length; i<l; i++) {
96
97
  const item = dataArray[i]
@@ -172,6 +173,15 @@ Model.prototype._validateFields = function (dataRoot, fields, data, opts, parent
172
173
  }
173
174
  // if (!parentPath && fieldName == 'categories') console.timeEnd(fieldName)
174
175
  }
176
+
177
+ // Add any extra fields that are not in the schema. Item maybe false when inserting (from recursing above)
178
+ if (notStrict && !fieldsIsArray && item) {
179
+ const allDataKeys = Object.keys(item)
180
+ for (let m=0, n=allDataKeys.length; m<n; m++) {
181
+ const key = allDataKeys[m]
182
+ if (!fieldsArray.includes(key)) data2[key] = item[key]
183
+ }
184
+ }
175
185
  }
176
186
 
177
187
  // Normalise array indexes and return
@@ -266,5 +276,5 @@ Model.prototype._ignoredRules = [
266
276
  // todo: need to remove filesize and formats..
267
277
  'awsAcl', 'awsBucket', 'default', 'defaultOverride', 'filename', 'filesize', 'fileSize', 'formats',
268
278
  'image', 'index', 'insertOnly', 'model', 'nullObject', 'params', 'path', 'getSignedUrl', 'timestampField',
269
- 'type', 'isType', 'isSchema', 'virtual',
279
+ 'type', 'isType', 'isSchema', 'virtual', 'strict',
270
280
  ]
package/lib/model.js CHANGED
@@ -21,6 +21,11 @@ function Model(name, opts, manager) {
21
21
  } else if (!opts.fields) {
22
22
  throw `We couldn't find ${name}.fields in the model definition, the model maybe setup `
23
23
  + `or exported incorrectly:\n${JSON.stringify(opts, null, 2)}`
24
+ } else if (!util.isSubdocument(opts.fields) && opts.fields.type == 'any') {
25
+ throw `Instead of using { type: 'any' } for ${name}.fields, please use the new 'strict' definition rule` +
26
+ ', e.g. { schema: { strict: false }}'
27
+ } else if (!util.isSubdocument(opts.fields) && !util.isEmpty(opts.fields)) {
28
+ throw `The ${name}.fields object should be a valid document, e.g. { name: { type: 'string' }}`
24
29
  }
25
30
 
26
31
  // Add schema options
@@ -107,7 +112,9 @@ function Model(name, opts, manager) {
107
112
  }, this)
108
113
 
109
114
  // Extend default fields with passed in fields and check for invalid fields
110
- this._setupFields(this.fields = Object.assign({}, this._defaultFields, this.fields))
115
+ this._setupFields(
116
+ this.fields = util.isSchema(this.fields) ? this.fields : Object.assign({}, this._defaultFields, this.fields)
117
+ )
111
118
  this.fieldsFlattened = this._getFieldsFlattened(this.fields, '') // test output?
112
119
  this.modelFieldsArray = this._getModelFieldsArray()
113
120
 
@@ -173,11 +180,14 @@ Model.prototype._getModelFieldsArray = function() {
173
180
  }, [])
174
181
  },
175
182
 
176
- Model.prototype._setupFields = function(fields) {
183
+ Model.prototype._setupFields = function(fields, isSub) {
177
184
  /**
178
185
  * Check for invalid rules on a field object, and set field.isType
179
186
  * @param {object|array} fields - subsdocument or array
180
187
  */
188
+ // We need to allow the processing of the root schema object
189
+ if (!isSub) fields = { fields }
190
+
181
191
  util.forEach(fields, function(field, fieldName) {
182
192
  // Schema field
183
193
  if (fieldName == 'schema') return
@@ -214,17 +224,8 @@ Model.prototype._setupFields = function(fields) {
214
224
  isSchema: true,
215
225
  }
216
226
 
217
- // Rule doesn't exist
218
- for (let ruleName in field.schema) {
219
- if ((this.rules[ruleName] || rules[ruleName]) && this._ignoredRules.indexOf(ruleName) != -1) {
220
- this.error(`The rule name "${ruleName}" for the model "${this.name}" is a reserved keyword, ignoring rule.`)
221
- }
222
- if (!this.rules[ruleName] && !rules[ruleName] && this._ignoredRules.indexOf(ruleName) == -1) {
223
- // console.log(field.schema)
224
- this.error(`No rule "${ruleName}" exists for model "${this.name}", ignoring rule.`)
225
- delete field.schema[ruleName]
226
- }
227
- }
227
+ // Remove invalid rules
228
+ this._removeInvalidRules(field)
228
229
 
229
230
  // Misused schema property
230
231
  } else if (fieldName == 'schema' || fieldName == 'isSchema') {
@@ -233,6 +234,7 @@ Model.prototype._setupFields = function(fields) {
233
234
 
234
235
  // Fields be an array
235
236
  } else if (util.isArray(field)) {
237
+ this._removeInvalidRules(field)
236
238
  field.schema = util.removeUndefined({
237
239
  type: 'array',
238
240
  isArray: true,
@@ -243,7 +245,7 @@ Model.prototype._setupFields = function(fields) {
243
245
  virtual: field.length == 1 && (field[0] || {}).virtual ? true : undefined,
244
246
  ...(field.schema || {}),
245
247
  })
246
- this._setupFields(field)
248
+ this._setupFields(field, true)
247
249
 
248
250
  // Fields can be a subdocument, e.g. user.pet = { name: {}, ..}
249
251
  } else if (util.isSubdocument(field)) {
@@ -253,6 +255,7 @@ Model.prototype._setupFields = function(fields) {
253
255
  field.schema.index = index2dsphere
254
256
  delete field.index
255
257
  }
258
+ this._removeInvalidRules(field)
256
259
  field.schema = util.removeUndefined({
257
260
  type: 'object',
258
261
  isObject: true,
@@ -262,11 +265,33 @@ Model.prototype._setupFields = function(fields) {
262
265
  nullObject: this.manager.opts.nullObjects,
263
266
  ...field.schema,
264
267
  })
265
- this._setupFields(field)
268
+ this._setupFields(field, true)
266
269
  }
267
270
  }, this)
268
271
  },
269
272
 
273
+ Model.prototype._removeInvalidRules = function(field) {
274
+ /**
275
+ * Remove invalid rules on a field object
276
+ * @param {object} field
277
+ * @return {object} field
278
+ **/
279
+ for (let ruleName in (field||{}).schema) {
280
+ const ruleFn = this.rules[ruleName] || rules[ruleName]
281
+ // Rule doesn't exist
282
+ if (!ruleFn && this._ignoredRules.indexOf(ruleName) == -1) {
283
+ // console.log(field.schema)
284
+ this.error(`No rule "${ruleName}" exists for model "${this.name}", ignoring rule.`)
285
+ delete field.schema[ruleName]
286
+ }
287
+ // Reserved rule
288
+ if (this.rules[ruleName] && this._ignoredRules.indexOf(ruleName) != -1) {
289
+ this.error(`The rule "${ruleName}" for the model "${this.name}" is a reserved keyword, ignoring custom rule function.`)
290
+ }
291
+ }
292
+ return field
293
+ },
294
+
270
295
  Model.prototype._setupIndexes = async function(fields, opts={}) {
271
296
  /**
272
297
  * Creates indexes for the model (multikey, and sub-document supported)
@@ -294,7 +319,7 @@ Model.prototype._setupIndexes = async function(fields, opts={}) {
294
319
  }
295
320
 
296
321
  // Process custom 'unprocessed' fields
297
- if (fields && !fields[Object.keys(fields)[0]].schema) {
322
+ if (fields && !(fields[Object.keys(fields)[0]].schema||{}).isSchema) {
298
323
  fields = util.deepCopy(fields)
299
324
  this._setupFields(fields)
300
325
  // console.dir(fields, { depth: null })
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.0.23",
5
+ "version": "3.1.0",
6
6
  "license": "MIT",
7
7
  "repository": "github:boycce/monastery",
8
8
  "homepage": "https://boycce.github.io/monastery/",
package/test/model.js CHANGED
@@ -21,18 +21,8 @@ test('model > model on manager', async () => {
21
21
  db2.close()
22
22
  })
23
23
 
24
- test('model setup basics', async () => {
25
- // Setup
26
- let user = db.model('user', { fields: {
27
- name: { type: 'string' },
28
- pets: [{ type: 'string' }],
29
- colors: { red: { type: 'string' } },
30
- points: [[{ type: 'number' }]],
31
- points2: [[{ x: { type: 'number' } }]],
32
- logo: { type: 'image' },
33
- }})
34
-
35
- // no fields defined
24
+ test('model setup with default fields', async () => {
25
+ // Default fields
36
26
  expect(db.model('user2', { fields: {} }).fields).toEqual({
37
27
  _id: {
38
28
  schema: {
@@ -64,12 +54,39 @@ test('model setup basics', async () => {
64
54
  type: 'integer',
65
55
  },
66
56
  },
57
+ schema: {
58
+ isObject: true,
59
+ isSchema: true,
60
+ isType: 'isObject',
61
+ type: 'object',
62
+ },
63
+ })
64
+ })
65
+
66
+ test('model setup basics', async () => {
67
+ // Setup
68
+ let user = db.model('user', {
69
+ fields: {
70
+ name: { type: 'string' },
71
+ pets: [{ type: 'string' }],
72
+ colors: { red: { type: 'string' } },
73
+ points: [[{ type: 'number' }]],
74
+ points2: [[{ x: { type: 'number' } }]],
75
+ logo: { type: 'image' },
76
+ },
67
77
  })
68
78
 
69
79
  // Has model name
70
80
  expect(user.name)
71
81
  .toEqual('user')
72
82
 
83
+ // Expect to throw an error
84
+ expect(() => db.model('user', { fields: { type: 'any' } }))
85
+ .toThrow(
86
+ 'Instead of using { type: \'any\' } for user.fields, please use the new \'strict\' definition rule, '
87
+ + 'e.g. { schema: { strict: false }}'
88
+ )
89
+
73
90
  // Basic field
74
91
  expect(user.fields.name.schema)
75
92
  .toEqual({ type: 'string', isString: true, isSchema: true, isType: 'isString' })
@@ -114,42 +131,6 @@ test('model setup basics', async () => {
114
131
  ))
115
132
  })
116
133
 
117
- test('model setup with default fields', async () => {
118
- // Default fields
119
- expect(db.model('user2', { fields: {} }).fields).toEqual({
120
- _id: {
121
- schema: {
122
- insertOnly: true,
123
- isId: true,
124
- isSchema: true,
125
- isType: 'isId',
126
- type: 'id',
127
- },
128
- },
129
- createdAt: {
130
- schema: {
131
- default: expect.any(Function),
132
- insertOnly: true,
133
- isInteger: true,
134
- isSchema: true,
135
- isType: 'isInteger',
136
- timestampField: true,
137
- type: 'integer',
138
- },
139
- },
140
- updatedAt: {
141
- schema: {
142
- default: expect.any(Function),
143
- isInteger: true,
144
- isSchema: true,
145
- isType: 'isInteger',
146
- timestampField: true,
147
- type: 'integer',
148
- },
149
- },
150
- })
151
- })
152
-
153
134
  test('model setup with default objects', async () => {
154
135
  const db2 = monastery('127.0.0.1/monastery', { defaultObjects: true })
155
136
  let user = db2.model('user', { fields: {
@@ -248,6 +229,24 @@ test('model setup with schema', async () => {
248
229
  })
249
230
  })
250
231
 
232
+ test('model setup with schema on root', async () => {
233
+ // Expect to throw an error
234
+ expect(() => db.model('user', { fields: { name: 'string' } }))
235
+ .toThrow('The user.fields object should be a valid document, e.g. { name: { type: \'string\' }}')
236
+
237
+ // root has schema
238
+ expect(db.model('user', { fields: { name: { type: 'string' } } }).fields.schema)
239
+ .toEqual({ type: 'object', isObject: true, isSchema: true, isType: 'isObject' })
240
+
241
+ // root has custom schema
242
+ expect(db.model('user', { fields: { schema: { nullObject: true } } }).fields.schema)
243
+ .toEqual({ type: 'object', isObject: true, isSchema: true, isType: 'isObject', nullObject: true })
244
+
245
+ // strict mode off
246
+ expect(db.model('user', { fields: { schema: { strict: false } } }).fields.schema)
247
+ .toEqual({ type: 'object', isObject: true, isSchema: true, isType: 'isObject', strict: false })
248
+ })
249
+
251
250
  test('model setup with messages', async () => {
252
251
  let user = db.model('user', {
253
252
  fields: {
@@ -375,15 +374,15 @@ test('model setup with messages', async () => {
375
374
  })
376
375
  })
377
376
 
378
- test('model setup reserved rules', async () => {
377
+ test('model setup with reserved and invalid rules', async () => {
379
378
  // Setup
380
379
  const db2 = monastery('127.0.0.1/monastery', { logLevel: 0 })
381
380
  let user = db2.model('user-model', {
382
381
  fields: {
383
382
  name: {
384
383
  type: 'string',
385
- params: {}, // reserved keyword (image plugin)
386
- paramsUnreserved: {},
384
+ params: {}, // reserved keyword (image plugin)
385
+ invalidRule: {}, // no rule function found
387
386
  },
388
387
  },
389
388
  rules: {
@@ -392,10 +391,15 @@ test('model setup reserved rules', async () => {
392
391
  },
393
392
  },
394
393
  })
395
- await expect(user.validate({ name: 'Martin' })).resolves.toEqual({
396
- name: 'Martin',
397
- createdAt: expect.any(Number),
398
- updatedAt: expect.any(Number),
394
+ expect(user.fields.name).toEqual({
395
+ schema: {
396
+ type: 'string',
397
+ isString: true,
398
+ isSchema: true,
399
+ isType: 'isString',
400
+ params: {}, // still included
401
+ // invalidRule: {}, should be removed
402
+ },
399
403
  })
400
404
  db2.close()
401
405
  })
package/test/validate.js CHANGED
@@ -154,6 +154,87 @@ test('validation basic errors', async () => {
154
154
  })
155
155
  })
156
156
 
157
+ test('validation type any', async () => {
158
+ let user1 = db.model('user', {
159
+ fields: {
160
+ name: { type: 'any' },
161
+ },
162
+ })
163
+ // Any type on field
164
+ await expect(user1.validate({ name: 'benjamin' })).resolves.toEqual({ name: 'benjamin' })
165
+ await expect(user1.validate({ name: 1 })).resolves.toEqual({ name: 1 })
166
+ await expect(user1.validate({ name: null })).resolves.toEqual({ name: null })
167
+ await expect(user1.validate({ name: true })).resolves.toEqual({ name: true })
168
+ await expect(user1.validate({ name: false })).resolves.toEqual({ name: false })
169
+ await expect(user1.validate({ name: [1, 2] })).resolves.toEqual({ name: [1, 2] })
170
+ await expect(user1.validate({ name: { first: 1 } })).resolves.toEqual({ name: { first: 1 } })
171
+ })
172
+
173
+ test('validation schema with reserved and invalid rules', async () => {
174
+ const db2 = monastery('127.0.0.1/monastery', { logLevel: 0 })
175
+ let user = db2.model('user-model', {
176
+ fields: {
177
+ sub: {
178
+ name: {
179
+ type: 'string',
180
+ default: true, // reserved keyword
181
+ invalidRule: {}, // no rule function found
182
+ validRule: true,
183
+ },
184
+ },
185
+ },
186
+ rules: {
187
+ default: (value) => { // function shouldn't run (i.e. value still is 'Martin')
188
+ return false
189
+ },
190
+ validRule: (value) => {
191
+ if (value === 'Martin') return true
192
+ },
193
+ },
194
+ })
195
+ await expect(user.validate({ sub: { name: 'Martin' } })).resolves.toEqual({
196
+ createdAt: expect.any(Number),
197
+ updatedAt: expect.any(Number),
198
+ sub: {
199
+ name: 'Martin',
200
+ },
201
+ })
202
+ db2.close()
203
+ })
204
+
205
+ test('validation strict false', async () => {
206
+ let user1 = db.model('user', {
207
+ fields: {
208
+ name: { type: 'string' },
209
+ },
210
+ })
211
+ // strict on (default)
212
+ let inserted1 = await user1.insert({ data: { nonDefinedField: 1 } })
213
+ expect(inserted1).toEqual({
214
+ _id: inserted1._id,
215
+ })
216
+ let user2 = db.model('user', {
217
+ fields: {
218
+ name: { type: 'string' },
219
+ sub: {
220
+ name: { type: 'string' },
221
+ schema: { strict: false },
222
+ },
223
+ subArray: [{
224
+ name: { type: 'string' },
225
+ schema: { strict: false },
226
+ }],
227
+ schema: { strict: false },
228
+ },
229
+ })
230
+ // strict off
231
+ let inserted2 = await user2.insert({ data: { nonDefinedField: 1 } })
232
+ expect(inserted2).toEqual({
233
+ _id: inserted2._id,
234
+ nonDefinedField: 1,
235
+ })
236
+ })
237
+
157
238
  test('validation subdocument errors', async () => {
158
239
  let user = db.model('user', { fields: {
159
240
  animals: {