@e22m4u/js-repository 0.1.8 → 0.1.9

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.
@@ -0,0 +1,135 @@
1
+ import chai from 'chai';
2
+ import {expect} from 'chai';
3
+ import {Adapter} from '../adapter.js';
4
+ import {Schema} from '../../schema.js';
5
+ import {PropertyUniquenessValidator} from '../../definition/index.js';
6
+
7
+ const S = new Schema();
8
+ S.defineModel({name: 'model'});
9
+
10
+ class TestAdapter extends Adapter {
11
+ // eslint-disable-next-line no-unused-vars
12
+ create(modelName, modelData, filter = undefined) {
13
+ return Promise.resolve(modelData);
14
+ }
15
+
16
+ // eslint-disable-next-line no-unused-vars
17
+ replaceById(modelName, id, modelData, filter = undefined) {
18
+ return Promise.resolve(modelData);
19
+ }
20
+
21
+ // eslint-disable-next-line no-unused-vars
22
+ replaceOrCreate(modelName, modelData, filter = undefined) {
23
+ return Promise.resolve(modelData);
24
+ }
25
+
26
+ // eslint-disable-next-line no-unused-vars
27
+ patch(modelName, modelData, where = undefined) {
28
+ return Promise.resolve(modelData);
29
+ }
30
+
31
+ // eslint-disable-next-line no-unused-vars
32
+ patchById(modelName, id, modelData, filter = undefined) {
33
+ return Promise.resolve(modelData);
34
+ }
35
+ }
36
+
37
+ const A = S.getService(TestAdapter);
38
+ const V = S.getService(PropertyUniquenessValidator);
39
+ const sandbox = chai.spy.sandbox();
40
+
41
+ describe('PropertyUniquenessDecorator', function () {
42
+ afterEach(function () {
43
+ sandbox.restore();
44
+ });
45
+
46
+ it('overrides the "create" method and validates a given data', async function () {
47
+ const data = {kind: 'data'};
48
+ sandbox.on(
49
+ V,
50
+ 'validate',
51
+ (countMethod, methodName, modelName, modelData, id = undefined) => {
52
+ expect(typeof countMethod).to.be.eq('function');
53
+ expect(methodName).to.be.eq('create');
54
+ expect(modelName).to.be.eq('model');
55
+ expect(modelData).to.be.eql(data);
56
+ expect(id).to.be.undefined;
57
+ },
58
+ );
59
+ const res = await A.create('model', data);
60
+ expect(res).to.be.eql(data);
61
+ expect(V.validate).to.be.called.once;
62
+ });
63
+
64
+ it('overrides the "replaceById" method and validates a given data', async function () {
65
+ const data = {kind: 'data'};
66
+ sandbox.on(
67
+ V,
68
+ 'validate',
69
+ (countMethod, methodName, modelName, modelData, id = undefined) => {
70
+ expect(typeof countMethod).to.be.eq('function');
71
+ expect(methodName).to.be.eq('replaceById');
72
+ expect(modelName).to.be.eq('model');
73
+ expect(modelData).to.be.eql(data);
74
+ expect(id).to.be.eq(1);
75
+ },
76
+ );
77
+ const res = await A.replaceById('model', 1, data);
78
+ expect(res).to.be.eql(data);
79
+ expect(V.validate).to.be.called.once;
80
+ });
81
+
82
+ it('overrides the "replaceOrCreate" method and validates a given data', async function () {
83
+ const data = {kind: 'data'};
84
+ sandbox.on(
85
+ V,
86
+ 'validate',
87
+ (countMethod, methodName, modelName, modelData, id = undefined) => {
88
+ expect(typeof countMethod).to.be.eq('function');
89
+ expect(methodName).to.be.eq('replaceOrCreate');
90
+ expect(modelName).to.be.eq('model');
91
+ expect(modelData).to.be.eql(data);
92
+ expect(id).to.be.undefined;
93
+ },
94
+ );
95
+ const res = await A.replaceOrCreate('model', data);
96
+ expect(res).to.be.eql(data);
97
+ expect(V.validate).to.be.called.once;
98
+ });
99
+
100
+ it('overrides the "patch" method and validates a given data', async function () {
101
+ const data = {kind: 'data'};
102
+ sandbox.on(
103
+ V,
104
+ 'validate',
105
+ (countMethod, methodName, modelName, modelData, id = undefined) => {
106
+ expect(typeof countMethod).to.be.eq('function');
107
+ expect(methodName).to.be.eq('patch');
108
+ expect(modelName).to.be.eq('model');
109
+ expect(modelData).to.be.eql(data);
110
+ expect(id).to.be.undefined;
111
+ },
112
+ );
113
+ const res = await A.patch('model', data);
114
+ expect(res).to.be.eql(data);
115
+ expect(V.validate).to.be.called.once;
116
+ });
117
+
118
+ it('overrides the "patchById" method and validates a given data', async function () {
119
+ const data = {kind: 'data'};
120
+ sandbox.on(
121
+ V,
122
+ 'validate',
123
+ (countMethod, methodName, modelName, modelData, id = undefined) => {
124
+ expect(typeof countMethod).to.be.eq('function');
125
+ expect(methodName).to.be.eq('patchById');
126
+ expect(modelName).to.be.eq('model');
127
+ expect(modelData).to.be.eql(data);
128
+ expect(id).to.be.eq(1);
129
+ },
130
+ );
131
+ const res = await A.patchById('model', 1, data);
132
+ expect(res).to.be.eql(data);
133
+ expect(V.validate).to.be.called.once;
134
+ });
135
+ });
@@ -8,6 +8,7 @@ const sandbox = chai.spy.sandbox();
8
8
 
9
9
  describe('DefinitionRegistry', function () {
10
10
  let S;
11
+
11
12
  beforeEach(function () {
12
13
  S = new DefinitionRegistry();
13
14
  });
@@ -2,5 +2,6 @@ export * from './data-type.js';
2
2
  export * from './property-definition.js';
3
3
  export * from './property-validator/index.js';
4
4
  export * from './property-transformer/index.js';
5
+ export * from './property-uniqueness-validator.js';
5
6
  export * from './properties-definition-validator.js';
6
7
  export * from './primary-keys-definition-validator.js';
@@ -2,5 +2,6 @@ export * from './data-type.js';
2
2
  export * from './property-definition.js';
3
3
  export * from './property-validator/index.js';
4
4
  export * from './property-transformer/index.js';
5
+ export * from './property-uniqueness-validator.js';
5
6
  export * from './properties-definition-validator.js';
6
7
  export * from './primary-keys-definition-validator.js';
@@ -297,5 +297,20 @@ export class PropertiesDefinitionValidator extends Service {
297
297
  );
298
298
  }
299
299
  }
300
+ if (propDef.unique && typeof propDef.unique !== 'boolean')
301
+ throw new InvalidArgumentError(
302
+ 'The provided option "unique" of the property %v in the model %v ' +
303
+ 'should be a Boolean, but %v given.',
304
+ propName,
305
+ modelName,
306
+ propDef.unique,
307
+ );
308
+ if (propDef.unique && propDef.primaryKey)
309
+ throw new InvalidArgumentError(
310
+ 'The property %v of the model %v is a primary key, ' +
311
+ 'so it should not have the option "unique" to be provided.',
312
+ propName,
313
+ modelName,
314
+ );
300
315
  }
301
316
  }
@@ -250,6 +250,7 @@ describe('PropertiesDefinitionValidator', function () {
250
250
  'should be a Boolean, but %s given.',
251
251
  v,
252
252
  );
253
+ expect(validate('str')).to.throw(error('"str"'));
253
254
  expect(validate(10)).to.throw(error('10'));
254
255
  expect(validate([])).to.throw(error('Array'));
255
256
  expect(validate({})).to.throw(error('Object'));
@@ -277,7 +278,7 @@ describe('PropertiesDefinitionValidator', function () {
277
278
  expect(validate([])).to.throw(error);
278
279
  expect(validate({})).to.throw(error);
279
280
  expect(validate(null)).to.throw(error);
280
- validate(undefined);
281
+ validate(undefined)();
281
282
  });
282
283
 
283
284
  it('expects the primary key should not have the option "required" to be true', function () {
@@ -294,8 +295,8 @@ describe('PropertiesDefinitionValidator', function () {
294
295
  'so it should not have the option "required" to be provided.',
295
296
  );
296
297
  expect(validate(true)).to.throw(error);
297
- validate(false);
298
- validate(undefined);
298
+ validate(false)();
299
+ validate(undefined)();
299
300
  });
300
301
 
301
302
  it('expects the primary key should not have the option "default" to be provided', function () {
@@ -318,7 +319,7 @@ describe('PropertiesDefinitionValidator', function () {
318
319
  expect(validate([])).to.throw(error);
319
320
  expect(validate({})).to.throw(error);
320
321
  expect(validate(null)).to.throw(error);
321
- validate(undefined);
322
+ validate(undefined)();
322
323
  });
323
324
 
324
325
  it('expects a non-array property should not have the option "itemType" to be provided', function () {
@@ -337,7 +338,7 @@ describe('PropertiesDefinitionValidator', function () {
337
338
  expect(validate(DataType.NUMBER)).to.throw(error);
338
339
  expect(validate(DataType.BOOLEAN)).to.throw(error);
339
340
  expect(validate(DataType.OBJECT)).to.throw(error);
340
- validate(DataType.ARRAY);
341
+ validate(DataType.ARRAY)();
341
342
  });
342
343
 
343
344
  it('the option "model" requires the "object" property type', function () {
@@ -488,5 +489,45 @@ describe('PropertiesDefinitionValidator', function () {
488
489
  validate(['myTransformer'])();
489
490
  validate({myTransformer: true})();
490
491
  });
492
+
493
+ it('expects provided the option "unique" to be a boolean', function () {
494
+ const validate = v => {
495
+ const foo = {
496
+ type: DataType.STRING,
497
+ unique: v,
498
+ };
499
+ return () => S.validate('model', {foo});
500
+ };
501
+ const error = v =>
502
+ format(
503
+ 'The provided option "unique" of the property "foo" in the model "model" ' +
504
+ 'should be a Boolean, but %s given.',
505
+ v,
506
+ );
507
+ expect(validate('str')).to.throw(error('"str"'));
508
+ expect(validate(10)).to.throw(error('10'));
509
+ expect(validate([])).to.throw(error('Array'));
510
+ expect(validate({})).to.throw(error('Object'));
511
+ validate(true)();
512
+ validate(false)();
513
+ });
514
+
515
+ it('expects the primary key should not have the option "unique" to be true', function () {
516
+ const validate = v => () => {
517
+ const foo = {
518
+ type: DataType.ANY,
519
+ primaryKey: true,
520
+ unique: v,
521
+ };
522
+ S.validate('model', {foo});
523
+ };
524
+ const error = format(
525
+ 'The property "foo" of the model "model" is a primary key, ' +
526
+ 'so it should not have the option "unique" to be provided.',
527
+ );
528
+ expect(validate(true)).to.throw(error);
529
+ validate(false)();
530
+ validate(undefined)();
531
+ });
491
532
  });
492
533
  });
@@ -1,5 +1,6 @@
1
1
  import {DataType} from './data-type.js';
2
2
  import {PropertyValidateOptions} from './property-validator/index.js';
3
+ import {PropertyTransformOptions} from './property-transformer/index.js';
3
4
 
4
5
  /**
5
6
  * Full property definition.
@@ -14,6 +15,8 @@ export declare type FullPropertyDefinition = {
14
15
  required?: boolean;
15
16
  default?: unknown;
16
17
  validate?: PropertyValidateOptions;
18
+ transform?: PropertyTransformOptions;
19
+ unique?: boolean;
17
20
  };
18
21
 
19
22
  /**
@@ -0,0 +1,31 @@
1
+ import {ModelId} from '../../../types.js';
2
+ import {Service} from '@e22m4u/js-service';
3
+ import {ModelData} from '../../../types.js';
4
+ import {WhereClause} from '../../../filter/index.js';
5
+
6
+ /**
7
+ * Count method.
8
+ */
9
+ type CountMethod = (where: WhereClause) => Promise<number>;
10
+
11
+ /**
12
+ * Property uniqueness validator.
13
+ */
14
+ export declare class PropertyUniquenessValidator extends Service {
15
+ /**
16
+ * Validate.
17
+ *
18
+ * @param countMethod
19
+ * @param methodName
20
+ * @param modelName
21
+ * @param modelData
22
+ * @param modelId
23
+ */
24
+ validate(
25
+ countMethod: CountMethod,
26
+ methodName: string,
27
+ modelName: string,
28
+ modelData: ModelData,
29
+ modelId?: ModelId,
30
+ ): Promise<void>;
31
+ }
@@ -0,0 +1,131 @@
1
+ import {Service} from '@e22m4u/js-service';
2
+ import {isPureObject} from '../../../utils/index.js';
3
+ import {InvalidArgumentError} from '../../../errors/index.js';
4
+ import {ModelDefinitionUtils} from '../model-definition-utils.js';
5
+
6
+ /**
7
+ * Property uniqueness validator.
8
+ */
9
+ export class PropertyUniquenessValidator extends Service {
10
+ /**
11
+ * Validate.
12
+ *
13
+ * @param {Function} countMethod
14
+ * @param {string} methodName
15
+ * @param {string} modelName
16
+ * @param {object} modelData
17
+ * @param {*} modelId
18
+ * @returns {Promise<undefined>}
19
+ */
20
+ async validate(
21
+ countMethod,
22
+ methodName,
23
+ modelName,
24
+ modelData,
25
+ modelId = undefined,
26
+ ) {
27
+ if (typeof countMethod !== 'function')
28
+ throw new InvalidArgumentError(
29
+ 'The parameter "countMethod" of the PropertyUniquenessValidator ' +
30
+ 'must be a Function, but %v given.',
31
+ countMethod,
32
+ );
33
+ if (!methodName || typeof methodName !== 'string')
34
+ throw new InvalidArgumentError(
35
+ 'The parameter "methodName" of the PropertyUniquenessValidator ' +
36
+ 'must be a non-empty String, but %v given.',
37
+ methodName,
38
+ );
39
+ if (!modelName || typeof modelName !== 'string')
40
+ throw new InvalidArgumentError(
41
+ 'The parameter "modelName" of the PropertyUniquenessValidator ' +
42
+ 'must be a non-empty String, but %v given.',
43
+ modelName,
44
+ );
45
+ if (!isPureObject(modelData))
46
+ throw new InvalidArgumentError(
47
+ 'The data of the model %v should be an Object, but %v given.',
48
+ modelName,
49
+ modelData,
50
+ );
51
+ const propDefs =
52
+ this.getService(
53
+ ModelDefinitionUtils,
54
+ ).getPropertiesDefinitionInBaseModelHierarchy(modelName);
55
+ const isPartial = methodName === 'patch' || methodName === 'patchById';
56
+ const propNames = Object.keys(isPartial ? modelData : propDefs);
57
+ const idProp =
58
+ this.getService(ModelDefinitionUtils).getPrimaryKeyAsPropertyName(
59
+ modelName,
60
+ );
61
+ const createError = (propName, propValue) =>
62
+ new InvalidArgumentError(
63
+ 'An existing document of the model %v already has ' +
64
+ 'the property %v with the value %v and should be unique.',
65
+ modelName,
66
+ propName,
67
+ propValue,
68
+ );
69
+ let willBeReplaced = undefined;
70
+ for (const propName of propNames) {
71
+ const propDef = propDefs[propName];
72
+ if (!propDef || typeof propDef === 'string' || !propDef.unique) continue;
73
+ const propValue = modelData[propName];
74
+ // create
75
+ if (methodName === 'create') {
76
+ const count = await countMethod({[propName]: propValue});
77
+ if (count > 0) throw createError(propName, propValue);
78
+ }
79
+ // replaceById
80
+ else if (methodName === 'replaceById') {
81
+ const count = await countMethod({
82
+ [idProp]: {neq: modelId},
83
+ [propName]: propValue,
84
+ });
85
+ if (count > 0) throw createError(propName, propValue);
86
+ }
87
+ // replaceOrCreate
88
+ else if (methodName === 'replaceOrCreate') {
89
+ const idFromData = modelData[idProp];
90
+ if (willBeReplaced == null && idFromData != null) {
91
+ const count = await countMethod({[idProp]: idFromData});
92
+ willBeReplaced = count > 0;
93
+ }
94
+ // replaceById by replaceOrCreate
95
+ if (willBeReplaced) {
96
+ const count = await countMethod({
97
+ [idProp]: {neq: idFromData},
98
+ [propName]: propValue,
99
+ });
100
+ if (count > 0) throw createError(propName, propValue);
101
+ }
102
+ // create by replaceOrCreate
103
+ else {
104
+ const count = await countMethod({[propName]: propValue});
105
+ if (count > 0) throw createError(propName, propValue);
106
+ }
107
+ }
108
+ // patch
109
+ else if (methodName === 'patch') {
110
+ const count = await countMethod({[propName]: propValue});
111
+ if (count > 0) throw createError(propName, propValue);
112
+ }
113
+ // patchById
114
+ else if (methodName === 'patchById') {
115
+ const count = await countMethod({
116
+ [idProp]: {neq: modelId},
117
+ [propName]: propValue,
118
+ });
119
+ if (count > 0) throw createError(propName, propValue);
120
+ }
121
+ // unsupported method
122
+ else {
123
+ throw new InvalidArgumentError(
124
+ 'The PropertyUniquenessValidator does not ' +
125
+ 'support the adapter method %v.',
126
+ methodName,
127
+ );
128
+ }
129
+ }
130
+ }
131
+ }