Browse Source

chore: do not transform an empty values through property transformers

e22m4u 1 year ago
parent
commit
5fc09be51b

+ 3 - 2
README.md

@@ -304,7 +304,7 @@ schema.defineModel({
 Кроме проверки типа, дополнительные условия можно задать с помощью
 валидаторов, через которые будет проходить значение свойства перед
 записью в базу. Исключением являются [пустые значения](#Пустое-значение),
-которые не проходят проверку.
+которые не подлежат проверке.
 
 - `minLength: number` минимальная длинна строки или массива
 - `maxLength: number` максимальная длинна строки или массива
@@ -334,7 +334,8 @@ schema.defineModel({
 
 С помощью трансформеров производится модификация значений определенных
 полей перед записью в базу. Трансформеры позволяют указать какие изменения
-нужно производить с входящими данными.
+нужно производить с входящими данными. Исключением являются
+[пустые значения](#Пустое-значение), которые не подлежат трансформации.
 
 - `trim` удаление пробельных символов с начала и конца строки
 - `toUpperCase` перевод строки в верхний регистр

+ 3 - 2
docs/index.html

@@ -151,7 +151,7 @@
 <a id="md:валидаторы" class="tsd-anchor"></a><h2><a href="#md:валидаторы">Валидаторы</a></h2><p>Кроме проверки типа, дополнительные условия можно задать с помощью
 валидаторов, через которые будет проходить значение свойства перед
 записью в базу. Исключением являются <a href="#md:Пустое-значение">пустые значения</a>,
-которые не проходят проверку.</p>
+которые не подлежат проверке.</p>
 <ul>
 <li><code>minLength: number</code> минимальная длинна строки или массива</li>
 <li><code>maxLength: number</code> максимальная длинна строки или массива</li>
@@ -164,7 +164,8 @@
 </code><button>Copy</button></pre>
 <a id="md:трансформеры" class="tsd-anchor"></a><h2><a href="#md:трансформеры">Трансформеры</a></h2><p>С помощью трансформеров производится модификация значений определенных
 полей перед записью в базу. Трансформеры позволяют указать какие изменения
-нужно производить с входящими данными.</p>
+нужно производить с входящими данными. Исключением являются
+<a href="#md:Пустое-значение">пустые значения</a>, которые не подлежат трансформации.</p>
 <ul>
 <li><code>trim</code> удаление пробельных символов с начала и конца строки</li>
 <li><code>toUpperCase</code> перевод строки в верхний регистр</li>

+ 2 - 2
src/adapter/decorator/data-transformation-decorator.js

@@ -41,13 +41,13 @@ export class DataTransformationDecorator extends Service {
 
     const patch = adapter.patch;
     adapter.patch = function (modelName, modelData, where) {
-      modelData = transformer.transform(modelName, modelData, true);
+      modelData = transformer.transform(modelName, modelData);
       return patch.call(this, modelName, modelData, where);
     };
 
     const patchById = adapter.patchById;
     adapter.patchById = function (modelName, id, modelData, filter) {
-      modelData = transformer.transform(modelName, modelData, true);
+      modelData = transformer.transform(modelName, modelData);
       return patchById.call(this, modelName, id, modelData, filter);
     };
   }

+ 2 - 2
src/adapter/decorator/data-transformation-decorator.spec.js

@@ -80,7 +80,7 @@ describe('DataTransformationDecorator', function () {
     const res = await A.patch('model', modelData);
     expect(res).to.be.eql(transformedData);
     expect(V.transform).to.be.called.once;
-    expect(V.transform).to.be.called.with.exactly('model', modelData, true);
+    expect(V.transform).to.be.called.with.exactly('model', modelData);
   });
 
   it('overrides the "patchById" method and transforms a given data', async function () {
@@ -90,6 +90,6 @@ describe('DataTransformationDecorator', function () {
     const res = await A.patchById('model', 1, modelData);
     expect(res).to.be.eql(transformedData);
     expect(V.transform).to.be.called.once;
-    expect(V.transform).to.be.called.with.exactly('model', modelData, true);
+    expect(V.transform).to.be.called.with.exactly('model', modelData);
   });
 });

+ 17 - 11
src/definition/model/model-data-transformer.js

@@ -1,6 +1,7 @@
 import {Service} from '@e22m4u/js-service';
 import {cloneDeep} from '../../utils/index.js';
 import {isPureObject} from '../../utils/index.js';
+import {EmptyValuesDefiner} from './properties/index.js';
 import {InvalidArgumentError} from '../../errors/index.js';
 import {ModelDefinitionUtils} from './model-definition-utils.js';
 import {PropertyTransformerRegistry} from './properties/index.js';
@@ -14,34 +15,39 @@ export class ModelDataTransformer extends Service {
    *
    * @param {string} modelName
    * @param {object} modelData
-   * @param {boolean} isPartial
    * @returns {object}
    */
-  transform(modelName, modelData, isPartial = false) {
+  transform(modelName, modelData) {
     if (!isPureObject(modelData))
       throw new InvalidArgumentError(
         'The data of the model %v should be an Object, but %v given.',
         modelName,
         modelData,
       );
+    const emptyValuesDefiner = this.getService(EmptyValuesDefiner);
+    const modelDefinitionUtils = this.getService(ModelDefinitionUtils);
     const propDefs =
-      this.getService(
-        ModelDefinitionUtils,
-      ).getPropertiesDefinitionInBaseModelHierarchy(modelName);
+      modelDefinitionUtils.getPropertiesDefinitionInBaseModelHierarchy(
+        modelName,
+      );
+    const propNames = Object.keys(propDefs);
     const transformedData = cloneDeep(modelData);
-    const propNames = Object.keys(isPartial ? modelData : propDefs);
     propNames.forEach(propName => {
       const propDef = propDefs[propName];
       if (!propDef) return;
-      const oldValue = modelData[propName];
-      const newValue = this._transformPropertyValue(
+      const propType =
+        modelDefinitionUtils.getDataTypeFromPropertyDefinition(propDef);
+      const propValue = modelData[propName];
+      const isEmpty = emptyValuesDefiner.isEmpty(propType, propValue);
+      if (isEmpty) return;
+      const newPropValue = this._transformPropertyValue(
         modelName,
         propName,
         propDef,
-        oldValue,
+        propValue,
       );
-      if (oldValue !== newValue) {
-        transformedData[propName] = newValue;
+      if (propValue !== newPropValue) {
+        transformedData[propName] = newPropValue;
       }
     });
     return transformedData;

+ 15 - 83
src/definition/model/model-data-transformer.spec.js

@@ -104,7 +104,7 @@ describe('ModelDataTransformer', function () {
         expect(res).to.be.eql({foo: 'transformed'});
       });
 
-      it('transforms a property value even it is not provided', function () {
+      it('does not transform a property value if it is not provided', function () {
         const schema = new Schema();
         const myTransformer = () => 'transformed';
         schema
@@ -121,10 +121,10 @@ describe('ModelDataTransformer', function () {
         });
         const T = schema.getService(ModelDataTransformer);
         const res = T.transform('model', {});
-        expect(res).to.be.eql({foo: 'transformed'});
+        expect(res).to.be.eql({});
       });
 
-      it('transforms undefined and null values', function () {
+      it('does not transform undefined and null values', function () {
         const schema = new Schema();
         const myTransformer = () => 'transformed';
         schema
@@ -142,30 +142,8 @@ describe('ModelDataTransformer', function () {
         const T = schema.getService(ModelDataTransformer);
         const res1 = T.transform('model', {foo: undefined});
         const res2 = T.transform('model', {foo: null});
-        expect(res1).to.be.eql({foo: 'transformed'});
-        expect(res2).to.be.eql({foo: 'transformed'});
-      });
-
-      it('the parameter "isPartial" prevents to transform values of not provided properties', function () {
-        const schema = new Schema();
-        const myTransformer = () => 'transformed';
-        schema
-          .getService(PropertyTransformerRegistry)
-          .addTransformer('myTransformer', myTransformer);
-        schema.defineModel({
-          name: 'model',
-          properties: {
-            foo: {
-              type: DataType.STRING,
-              transform: 'myTransformer',
-            },
-          },
-        });
-        const T = schema.getService(ModelDataTransformer);
-        const res1 = T.transform('model', {});
-        const res2 = T.transform('model', {}, true);
-        expect(res1).to.be.eql({foo: 'transformed'});
-        expect(res2).to.be.eql({});
+        expect(res1).to.be.eql({foo: undefined});
+        expect(res2).to.be.eql({foo: null});
       });
     });
 
@@ -257,7 +235,7 @@ describe('ModelDataTransformer', function () {
         expect(res).to.be.eql({foo: 'transformed'});
       });
 
-      it('transforms a property value even it is not provided', function () {
+      it('does not transform a property value if it is not provided', function () {
         const schema = new Schema();
         const myTransformer = () => 'transformed';
         schema
@@ -274,7 +252,7 @@ describe('ModelDataTransformer', function () {
         });
         const T = schema.getService(ModelDataTransformer);
         const res = T.transform('model', {});
-        expect(res).to.be.eql({foo: 'transformed'});
+        expect(res).to.be.eql({});
       });
 
       it('transforms undefined and null values', function () {
@@ -295,30 +273,8 @@ describe('ModelDataTransformer', function () {
         const T = schema.getService(ModelDataTransformer);
         const res1 = T.transform('model', {foo: undefined});
         const res2 = T.transform('model', {foo: null});
-        expect(res1).to.be.eql({foo: 'transformed'});
-        expect(res2).to.be.eql({foo: 'transformed'});
-      });
-
-      it('the parameter "isPartial" prevents to transform values of not provided properties', function () {
-        const schema = new Schema();
-        const myTransformer = () => 'transformed';
-        schema
-          .getService(PropertyTransformerRegistry)
-          .addTransformer('myTransformer', myTransformer);
-        schema.defineModel({
-          name: 'model',
-          properties: {
-            foo: {
-              type: DataType.STRING,
-              transform: ['myTransformer'],
-            },
-          },
-        });
-        const T = schema.getService(ModelDataTransformer);
-        const res1 = T.transform('model', {});
-        const res2 = T.transform('model', {}, true);
-        expect(res1).to.be.eql({foo: 'transformed'});
-        expect(res2).to.be.eql({});
+        expect(res1).to.be.eql({foo: undefined});
+        expect(res2).to.be.eql({foo: null});
       });
     });
 
@@ -421,7 +377,7 @@ describe('ModelDataTransformer', function () {
         expect(res).to.be.eql({foo: 'transformed'});
       });
 
-      it('transforms a property value even it is not provided', function () {
+      it('does not transform a property value if it is not provided', function () {
         const schema = new Schema();
         const myTransformer = () => 'transformed';
         schema
@@ -440,7 +396,7 @@ describe('ModelDataTransformer', function () {
         });
         const T = schema.getService(ModelDataTransformer);
         const res = T.transform('model', {});
-        expect(res).to.be.eql({foo: 'transformed'});
+        expect(res).to.be.eql({});
       });
 
       it('transforms undefined and null values', function () {
@@ -463,32 +419,8 @@ describe('ModelDataTransformer', function () {
         const T = schema.getService(ModelDataTransformer);
         const res1 = T.transform('model', {foo: undefined});
         const res2 = T.transform('model', {foo: null});
-        expect(res1).to.be.eql({foo: 'transformed'});
-        expect(res2).to.be.eql({foo: 'transformed'});
-      });
-
-      it('the parameter "isPartial" prevents to transform values of not provided properties', function () {
-        const schema = new Schema();
-        const myTransformer = () => 'transformed';
-        schema
-          .getService(PropertyTransformerRegistry)
-          .addTransformer('myTransformer', myTransformer);
-        schema.defineModel({
-          name: 'model',
-          properties: {
-            foo: {
-              type: DataType.STRING,
-              transform: {
-                myTransformer: true,
-              },
-            },
-          },
-        });
-        const T = schema.getService(ModelDataTransformer);
-        const res1 = T.transform('model', {});
-        const res2 = T.transform('model', {}, true);
-        expect(res1).to.be.eql({foo: 'transformed'});
-        expect(res2).to.be.eql({});
+        expect(res1).to.be.eql({foo: undefined});
+        expect(res2).to.be.eql({foo: null});
       });
     });
 
@@ -501,7 +433,7 @@ describe('ModelDataTransformer', function () {
         name: 'model',
         properties: {
           foo: {
-            type: DataType.STRING,
+            type: DataType.ANY,
             transform: undefined,
           },
         },
@@ -510,7 +442,7 @@ describe('ModelDataTransformer', function () {
       const throwable = v => () => {
         const models = schema.getService(DefinitionRegistry)['_models'];
         models.model.properties.foo.transform = v;
-        T.transform('model', {});
+        T.transform('model', {foo: 'bar'});
       };
       const error = v =>
         format(

+ 1 - 1
src/definition/model/properties/property-validator/builtin/max-length-validator.js

@@ -9,7 +9,7 @@ import {InvalidArgumentError} from '../../../../../errors/index.js';
  * @returns {boolean}
  */
 export function maxLengthValidator(value, options, context) {
-  if (options === false) return true;
+  if (value == null || options === false) return true;
   if (typeof options !== 'number')
     throw new InvalidArgumentError(
       'The validator %v requires the "options" argument ' +

+ 9 - 2
src/definition/model/properties/property-validator/builtin/max-length-validator.spec.js

@@ -8,6 +8,13 @@ describe('maxLengthValidator', function () {
     expect(res).to.be.true;
   });
 
+  it('returns true for undefined and null values', function () {
+    const res1 = maxLengthValidator(undefined, 10, {});
+    const res2 = maxLengthValidator(null, 10, {});
+    expect(res1).to.be.true;
+    expect(res2).to.be.true;
+  });
+
   it('requires the "value" argument as a String or an Array', function () {
     const throwable = v => () =>
       maxLengthValidator(v, 10, {
@@ -23,14 +30,14 @@ describe('maxLengthValidator', function () {
     expect(throwable(0)).to.throw(error('0'));
     expect(throwable(true)).to.throw(error('true'));
     expect(throwable(false)).to.throw(error('false'));
-    expect(throwable(undefined)).to.throw(error('undefined'));
-    expect(throwable(null)).to.throw(error('null'));
     expect(throwable({})).to.throw(error('Object'));
     expect(throwable(() => undefined)).to.throw(error('Function'));
     throwable('str')();
     throwable('')();
     throwable([1, 2, 3])();
     throwable([])();
+    throwable(undefined)();
+    throwable(null)();
   });
 
   it('requires the "options" argument to be a number', function () {

+ 1 - 1
src/definition/model/properties/property-validator/builtin/min-length-validator.js

@@ -9,7 +9,7 @@ import {InvalidArgumentError} from '../../../../../errors/index.js';
  * @returns {boolean}
  */
 export function minLengthValidator(value, options, context) {
-  if (options === false) return true;
+  if (value == null || options === false) return true;
   if (typeof options !== 'number')
     throw new InvalidArgumentError(
       'The validator %v requires the "options" argument ' +

+ 9 - 2
src/definition/model/properties/property-validator/builtin/min-length-validator.spec.js

@@ -8,6 +8,13 @@ describe('minLengthValidator', function () {
     expect(res).to.be.true;
   });
 
+  it('returns true for undefined and null values', function () {
+    const res1 = minLengthValidator(undefined, 10, {});
+    const res2 = minLengthValidator(null, 10, {});
+    expect(res1).to.be.true;
+    expect(res2).to.be.true;
+  });
+
   it('requires the "value" argument as a String or an Array', function () {
     const throwable = v => () =>
       minLengthValidator(v, 0, {
@@ -23,14 +30,14 @@ describe('minLengthValidator', function () {
     expect(throwable(0)).to.throw(error('0'));
     expect(throwable(true)).to.throw(error('true'));
     expect(throwable(false)).to.throw(error('false'));
-    expect(throwable(undefined)).to.throw(error('undefined'));
-    expect(throwable(null)).to.throw(error('null'));
     expect(throwable({})).to.throw(error('Object'));
     expect(throwable(() => undefined)).to.throw(error('Function'));
     throwable('str')();
     throwable('')();
     throwable([1, 2, 3])();
     throwable([])();
+    throwable(undefined)();
+    throwable(null)();
   });
 
   it('requires the "options" argument to be a number', function () {

+ 1 - 1
src/definition/model/properties/property-validator/builtin/regexp-validator.js

@@ -10,7 +10,7 @@ import {InvalidArgumentError} from '../../../../../errors/index.js';
  * @returns {boolean}
  */
 export function regexpValidator(value, options, context) {
-  if (options === false) return true;
+  if (value == null || options === false) return true;
   if (typeof options !== 'string' && !(options instanceof RegExp))
     throw new InvalidArgumentError(
       'The validator %v requires the "options" argument ' +

+ 9 - 2
src/definition/model/properties/property-validator/builtin/regexp-validator.spec.js

@@ -8,6 +8,13 @@ describe('regexpValidator', function () {
     expect(res).to.be.true;
   });
 
+  it('returns true for undefined and null values', function () {
+    const res1 = regexpValidator(undefined, '.*', {});
+    const res2 = regexpValidator(null, '.*', {});
+    expect(res1).to.be.true;
+    expect(res2).to.be.true;
+  });
+
   it('requires the "value" argument to be a string', function () {
     const throwable = v => () =>
       regexpValidator(v, '.*', {
@@ -21,13 +28,13 @@ describe('regexpValidator', function () {
       );
     expect(throwable(true)).to.throw(error('true'));
     expect(throwable(false)).to.throw(error('false'));
-    expect(throwable(undefined)).to.throw(error('undefined'));
-    expect(throwable(null)).to.throw(error('null'));
     expect(throwable({})).to.throw(error('Object'));
     expect(throwable([])).to.throw(error('Array'));
     expect(throwable(() => undefined)).to.throw(error('Function'));
     throwable('str')();
     throwable('')();
+    throwable(undefined)();
+    throwable(null)();
   });
 
   it('requires the "options" argument to be a string or RegExp', function () {