@onehat/data 1.9.3 → 1.10.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.
@@ -1,3 +1,4 @@
1
+ import Joi from 'Joi';
1
2
  import Entity from '../../src/Entity.js';
2
3
  import Schema from '../../src/Schema/index.js';
3
4
  import PropertyTypes from '../../src/Property/index.js';
@@ -16,13 +17,17 @@ describe('Entity', function() {
16
17
  { name: 'bar' },
17
18
  { name: 'baz', mapping: 'baz.test.val', type: 'bool', defaultValue: null, },
18
19
  ],
20
+ validator: null,
19
21
  },
20
22
  entity: {
21
23
  methods: {
22
24
  testMethod: function() {
23
- this.bar = 'test me';
25
+ this.azx = 'test me';
24
26
  },
25
27
  },
28
+ statics: {
29
+ azx: null,
30
+ },
26
31
  },
27
32
  });
28
33
  this.data = {
@@ -116,7 +121,7 @@ describe('Entity', function() {
116
121
 
117
122
  it('_createMethods', function() {
118
123
  this.entity.testMethod();
119
- expect(this.entity.bar).to.be.eq('test me');
124
+ expect(this.entity.azx).to.be.eq('test me');
120
125
  });
121
126
 
122
127
  it('loadOriginalData', function() {
@@ -737,7 +742,42 @@ describe('Entity', function() {
737
742
  property.setValue('Test');
738
743
  expect(didFire).to.be.true;
739
744
  });
745
+ });
746
+
747
+ describe('validators', function() {
748
+
749
+ it('whole validation process', function() {
750
+ let didFire = false;
751
+ this.entity.on('changeValidity', (entity) => {
752
+ didFire = true;
753
+ });
740
754
 
755
+ // Initial condition
756
+ expect(this.entity.isValid).to.be.null;
757
+
758
+ // Add validators
759
+ this.schema.model.validator = Joi.object({
760
+ foo: Joi.number()
761
+ .integer(),
762
+ bar: Joi.string()
763
+ .alphanum()
764
+ .required(),
765
+ baz: Joi.any(),
766
+ });
767
+ const result = this.entity.validate();
768
+ expect(result).to.be.true;
769
+
770
+ // Set a property to be invalid
771
+ this.entity.bar = null;
772
+ expect(didFire).to.be.true;
773
+ expect(this.entity.isValid).to.be.false;
774
+ expect(this.entity.validationError).to.match(/"bar" must be a string/);
775
+
776
+ // Restore validity
777
+ this.entity.bar = 'test';
778
+ expect(this.entity.isValid).to.be.true;
779
+ expect(this.entity.validationError).to.be.null;
780
+ });
741
781
  });
742
782
 
743
783
  });
@@ -1,3 +1,4 @@
1
+ import Joi from 'Joi';
1
2
  import PropertyTypes from '../../../src/Property/index.js';
2
3
  import Entity from '../../../src/Entity.js';
3
4
  import Schema from '../../../src/Schema/index.js';
@@ -233,4 +234,51 @@ describe('Property', function() {
233
234
 
234
235
  });
235
236
 
237
+ describe('validators', function() {
238
+
239
+ it('whole validation process', function() {
240
+ let didFire = false;
241
+ this.property.on('changeValidity', (entity) => {
242
+ didFire = true;
243
+ });
244
+
245
+ // Initial condition
246
+ expect(this.property.isValid).to.be.null;
247
+
248
+ // Add validators
249
+ const schema = new Schema({
250
+ name: 'baz',
251
+ model: {
252
+ idProperty: 'foo',
253
+ displayProperty: 'bar',
254
+ properties: [
255
+ { name: 'foo', type: 'int' },
256
+ { name: 'bar' },
257
+ ],
258
+ validator: Joi.object({
259
+ foo: Joi.number()
260
+ .integer(),
261
+ }),
262
+ },
263
+ }),
264
+ entity = new Entity(schema);
265
+ entity.initialize();
266
+ const property = entity.getProperty('foo');
267
+ property.setValue(2);
268
+
269
+ const result = property.validate();
270
+ expect(result).to.be.true;
271
+
272
+ // Set a property to be invalid
273
+ property.setValue(null);
274
+ expect(property.isValid).to.be.false;
275
+ expect(property.validationError).to.match(/"foo" must be a number/);
276
+
277
+ // Restore validity
278
+ property.setValue(2);
279
+ expect(property.isValid).to.be.true;
280
+ expect(property.validationError).to.be.null;
281
+ });
282
+ });
283
+
236
284
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onehat/data",
3
- "version": "1.9.3",
3
+ "version": "1.10.0",
4
4
  "description": "JS data modeling package with adapters for many storage mediums.",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -41,6 +41,7 @@
41
41
  "chrono-node": "^2.4.2",
42
42
  "fast-xml-parser": "^4.0.12",
43
43
  "he": "^1.2.0",
44
+ "joi": "^17.7.0",
44
45
  "js-base64": "^3.7.3",
45
46
  "lodash": "^4.17.21",
46
47
  "moment": "^2.29.4",
package/src/Entity.js CHANGED
@@ -49,6 +49,7 @@ class Entity extends EventEmitter {
49
49
 
50
50
  this.registerEvents([
51
51
  'change',
52
+ 'changeValidity',
52
53
  'reset',
53
54
  'reload',
54
55
  'save',
@@ -139,6 +140,17 @@ class Entity extends EventEmitter {
139
140
  * @member {string} isFrozen - Prevent the entity from being destroyed, but don't let it be changed either.
140
141
  */
141
142
  this.isFrozen = false;
143
+
144
+ /**
145
+ * @member {boolean} isValid - Whether this Entity passes validation
146
+ * @private
147
+ */
148
+ this.isValid = null;
149
+
150
+ /**
151
+ * @member {object} validationError - Any error in last validation.
152
+ */
153
+ this.validationError = null;
142
154
 
143
155
 
144
156
  // This ES6 Proxy allows us to create magic getters and setters for all property values.
@@ -268,7 +280,6 @@ class Entity extends EventEmitter {
268
280
  }
269
281
  const Property = PropertyTypes[type],
270
282
  property = new Property(definition, this._proxy);
271
-
272
283
  property.on('change', this._onPropertyChange);
273
284
 
274
285
  properties[definition.name] = property;
@@ -286,6 +297,7 @@ class Entity extends EventEmitter {
286
297
  */
287
298
  _onPropertyChange = () => {
288
299
  this._recalculateDependentProperties();
300
+ this.validate();
289
301
  this.emit('change', this._proxy);
290
302
  }
291
303
 
@@ -1136,6 +1148,7 @@ class Entity extends EventEmitter {
1136
1148
 
1137
1149
  if (isChanged) {
1138
1150
  this._recalculateDependentProperties();
1151
+ this.validate();
1139
1152
  this.emit('change', this._proxy);
1140
1153
  }
1141
1154
  return isChanged;
@@ -1286,6 +1299,43 @@ class Entity extends EventEmitter {
1286
1299
  this.isFrozen = true;
1287
1300
  }
1288
1301
 
1302
+
1303
+ // _ __ ___ __ __ _
1304
+ // | | / /___ _/ (_)___/ /___ _/ /_(_)___ ____
1305
+ // | | / / __ `/ / / __ / __ `/ __/ / __ \/ __ \
1306
+ // | |/ / /_/ / / / /_/ / /_/ / /_/ / /_/ / / / /
1307
+ // |___/\__,_/_/_/\__,_/\__,_/\__/_/\____/_/ /_/
1308
+
1309
+ /**
1310
+ * Gets whether or not the Entity validates according to schema's validation rules
1311
+ * @return {boolean} isValid
1312
+ */
1313
+ validate = () => {
1314
+ if (this.isDestroyed) {
1315
+ throw Error('this.validate is no longer valid. Entity has been destroyed.');
1316
+ }
1317
+
1318
+ let isValid = null,
1319
+ error;
1320
+
1321
+ if (this.schema.model.validator) {
1322
+ const validationResult = this.schema.model.validator.validate(this.submitValues);
1323
+ error = validationResult && validationResult.error || null;
1324
+ isValid = !error;
1325
+ if (this.validationError !== error) {
1326
+ this.validationError = error;
1327
+ }
1328
+ }
1329
+
1330
+ if (this.isValid !== isValid) {
1331
+ this.emit('changeValidity', this._proxy, isValid);
1332
+ this.isValid = isValid;
1333
+ }
1334
+
1335
+ return isValid;
1336
+ }
1337
+
1338
+
1289
1339
  /**
1290
1340
  * Destroy this object.
1291
1341
  * - Removes all circular references to parent objects
@@ -135,6 +135,7 @@ export default class Property extends EventEmitter {
135
135
 
136
136
  this.registerEvents([
137
137
  'change',
138
+ 'changeValidity',
138
139
  'destroy',
139
140
  ]);
140
141
 
@@ -148,17 +149,28 @@ export default class Property extends EventEmitter {
148
149
  * @member {any} rawValue - The raw value supplied to this property, *before* any parsing was applied
149
150
  */
150
151
  this.rawValue = null;
151
-
152
+
152
153
  /**
153
154
  * @member {any} parsedValue - The value for this property, *after* any parsing was applied
154
155
  */
155
156
  this.parsedValue = null;
156
-
157
+
157
158
  /**
158
159
  * @member {boolean} isDestroyed - Whether this object has been destroyed
159
160
  * @private
160
161
  */
161
162
  this.isDestroyed = false;
163
+
164
+ /**
165
+ * @member {boolean} isValid - Whether this Property passes validation
166
+ * @private
167
+ */
168
+ this.isValid = null;
169
+
170
+ /**
171
+ * @member {object} validationError - Any error in last validation.
172
+ */
173
+ this.validationError = null;
162
174
 
163
175
  }
164
176
 
@@ -357,6 +369,7 @@ export default class Property extends EventEmitter {
357
369
  if (isChanged) {
358
370
  this.rawValue = rawValue;
359
371
  this.parsedValue = newValue;
372
+ this.validate();
360
373
  this.emit('change', this, oldValue, newValue);
361
374
  }
362
375
 
@@ -433,6 +446,43 @@ export default class Property extends EventEmitter {
433
446
  return this.mapping;
434
447
  }
435
448
 
449
+ // _ __ ___ __ __ _
450
+ // | | / /___ _/ (_)___/ /___ _/ /_(_)___ ____
451
+ // | | / / __ `/ / / __ / __ `/ __/ / __ \/ __ \
452
+ // | |/ / /_/ / / / /_/ / /_/ / /_/ / /_/ / / / /
453
+ // |___/\__,_/_/_/\__,_/\__,_/\__/_/\____/_/ /_/
454
+
455
+ /**
456
+ * Gets whether or not the Property validates according to schema's validation rules
457
+ * @return {boolean} isValid
458
+ */
459
+ validate = () => {
460
+ if (this.isDestroyed) {
461
+ throw Error('this.validate is no longer valid. Entity has been destroyed.');
462
+ }
463
+
464
+ let isValid = null,
465
+ error;
466
+
467
+ if (this._entity?.schema?.model?.validator) {
468
+ const obj = {};
469
+ obj[this.name] = this.submitValue;
470
+ const validationResult = this._entity.schema.model.validator.validate(obj);
471
+ error = validationResult && validationResult.error || null;
472
+ isValid = !error;
473
+ if (this.validationError !== error) {
474
+ this.validationError = error;
475
+ }
476
+ }
477
+
478
+ if (this.isValid !== isValid) {
479
+ this.emit('changeValidity', this._proxy, isValid);
480
+ this.isValid = isValid;
481
+ }
482
+
483
+ return isValid;
484
+ }
485
+
436
486
  /**
437
487
  * Destroy this object.
438
488
  * - Removes all circular references to parent objects
@@ -72,6 +72,11 @@ export default class Schema extends EventEmitter {
72
72
  */
73
73
  sorters: [],
74
74
 
75
+ /**
76
+ * @member {object} validators - A JOI schema. See JOI docs for more info (https://joi.dev)
77
+ */
78
+ validator: null,
79
+
75
80
  /**
76
81
  * @member {object} associations - List of associated Models
77
82
  */