Browse Source

chore: adds support of asynchronous transformers

e22m4u 1 year ago
parent
commit
9e76e460ac

+ 1 - 1
docs/assets/navigation.js

@@ -1 +1 @@
-window.navigationData = "data:application/octet-stream;base64,H4sIAAAAAAAAA6WZXW/bNhSG/4uug2UN1m7LneO4mNE6NlynRTAMAScd29woUiGpImqx/z5IsiyKH4c0eqv3PQ+/DslD+8/vmYZXnd1m90STXVNBdpVVRB+z2wx4Xarr4ftPR12y7Cr7l/Iiu/3tv6sxEnIhiRZyR+QBtBfiWsK8jRQVSN08cvpSAwelbJzrCNO2wIimgvu6ZWphwqwglQY5BueMKAXq+iRMI9/ceGI/ClIghF5O4GzhQJWWTZA0GDDWHTDBD2ontqAE++rrl2PBeG2GKFHLHO5hTzltZ/QzYbRoF9xlo3a0nbM9PA2uByMuyko3nwmrQXWBvqlwPRjxPQVWqDkjtYKdEMzl2Q6M9gdRK8Kb8DpZhghrzQFFGTpGWvKc1QVgg3QsOO9ru/wzeahL4HohpS9tfK4E6rqC7uDpVhBHu1aMvxIFsDaXP5E24b755tT1JBF3knC1F7JEmYYriYpsSdcTJ5432qOmTIWYU9cF1GhvLzs7HoRelhWDNnWgCOSBx4Qxh3zBNoLrQYmyABw3NWCsjaQlkc0HaFTSrOJ+vKXuNqaQ2hBiT2inMRI/fBEg5pQ2xsoiPhKfOaWNszk+CseK8bewBwk8h8jl4ffh5L5eSltozI23UglFtfBNyKilEcJT63ow4qf8CCVxKf13NJLRHL0rLQPG+nIEibIsA1pR8qI3jhTKNcg9ydtyclCniJu37wzEXHClZZ1PVt+AGDqGWUukI4OIAWa8Wf/9D+R6JOim6kZxEqzon3//9c3bG19JPKaqzfJYYtS5qLlegT6KwqYZUoxyv3g/e/y4e95sl6vZ9un5w+LpebNdbxbb3dPzw2y1sNnRgGiLngrdacTjiXHN0tfmmVqcwzQ4STNwRi3KYURrcIZ2+hyNrhkbzufwPPldMfaprg9jHUMCcc2R9bT1GG9ZANdUNzZn+B6NN18IDsQUoyQNJZYRth7jnYthG3QW0gjBqbbkJNrSOUFOn2PRD6A0FOhseyxRqpAlYfQbFNie9rvS2Xiv/bYYfVqQ29CpGme160fYI2eg1BZeaiqhcJcq5Ivyx2rfQY5SjLIhUlPC7pytehYSCV+oPopau+Oz9ShPsCbhxg3YUujO8dj2rf/tsX3g+FqKhPxgq0Mt/EBKZy0vCE3sxeQkTxo6EvFjbSYMPCEy2ofzm9K/XWw9kYdcwJff6W7EilRx7IpUqeTzW7c/cNy1Dvgu5puvyQDafkgmUeeCd2IcfnKmtnF6d0JkYizbhXQRnJXAszeBGJkR2xfjT5/84ewO+eL8fs9iZNtxOdOzb7ymGNl4Lds8Q4pSQtdi8n2Yk4rqrpYZEfua510SXo/qlPPuFxPBRHuKQuUlDCICgNeuiOqfy9OLYgQ5JgR4AD3XQk7P/RFlyDgE/Xdxwov+yWijux/675pNy/IjDQeComo+2fwjolfQ0HZdFi81Yf74s4xCNrUE+ycQkzLqCEYBg1zjGWB7MBzlh5oRGUhrQ8YgWlJ+aP+HPMCrN7mnDgf11//VLRrA4h4AAA=="
+window.navigationData = "data:application/octet-stream;base64,H4sIAAAAAAAAA6WZXW/bNhSG/4uug2UN1m7LneO4mNE6NlynRTAMAScd29woUiGpImqx/z5ItiSKH4c0eqv3PQ/Jw8Mv+8/vmYZXnd1m90STXVNBdpVVRB+z2wx4Xarr/vtPR12y7Cr7l/Iiu/3tv6sxEnIhiRZyR+QBtBfiWsK8jRQVSN08cvpSAwelbJzrCNO2wIimgvu6ZWphwqwglQY5BueMKAXq+ixMI9/ceGI/ClIghJOcwNnCgSotmyCpN2CsO2CCH9RObEEJ9tXXL8eC8doKUaKWOdzDnnLaZvQzYbRoJ9xlo3a0ncEeToPrwYiLstLNZ8JqUF2gLxWuByO+p8AKNWekVrATgrk824HR/iBqRXgTnifLEGGtOaAoQ8dIS56zugBskI4F531tp38mD3UJXC+k9JWNz5VAXVfQbTzdDOJo14rxV6IA1tbyJ9IW3DdfTl1PEnEnCVd7IUuUabiSqMiSdD1x4rDQHjVlKsScui6gRnt72d7xIPSyrBi0pQNFoA48JozZ1wu2EFwPSpQF4LipAWNtJC2JbD5Ao5KyivvxlrrTmEJqQ4g9oZ3GKPzwQYCYU9oYbxbxkfjMKW0M5vgoHCvG38IeJPAcIoeH34eTT/eltInG3HgrlVBUC19CRi2NEE6t68GIn/IjlMSlnL6jkYzm6FlpGTDWlyNIlGUZ0BslL07GkUK5BrkneXud7NUp4ubtOwMxF1xpWeeT2Tcgho5h1hLpSC9igBlv1n//A7keCbqpulGcBSv6599/ffP2xnclHkvVZnksMepc1FyvQB9FYdMMKUa5X7yfPX7cPW+2y9Vs+/T8YfH0vNmuN4vt7un5YbZa2OxoQLRFzw3dacTjiXHNq6/NM7U4h2lwiqbnjFqUw4jW4Azt/DkaXTPW78/hPPldMfb5Xh/GOoYE4poj82nrMd6yAK6pbmxO/z0ab74QHIgpRkkaSqwibD3GGy7DNmgQ0gjBVFtyEm3p7CDnz7HoB1AaCjTbHkuUKmRJGP0GBbam/a50Nt5rvy1Gn17IbehUjbPa+SPskTNQagsvNZVQuFMV8kX5423fQY5SjLIhUlPC7pylOgiJhC9UH0Wt3fHZepQnWJNw4gZsKXRne2z7dvrtsX3g+FqKhPxgq/1d+IGUzlxeEJrYi8lOnjR0JOLH2kwYeEJktA/Dm9K/XGw9kYccwJef6W7EilRx7IpUqeThrXvacNy5Dvgu5puvyQDafkgmUeeCd2IcfnamtnF+d0IkMZbtQroIZiXw7E0gRjJi+2L86ZM/XN0hX5x/WrMY2XZczvSsG68pRu5+6F3LjRQldfeMqRpjGS9vG2RIUUroiE0+W3NSUd3di0bEvuZ5V9DXozrlvPvFRDDR7shQeQm9iADgtbuQnZ7e00NnBDkmBHgAPddCTs+QEWXIOAT9p3LCi/5haaO7arlrNi3LjzQcCIqq+WQjGREnBQ1t52XxUhPmjx9kFOIsBxPhXQ02oJZg/x4zYQw6glHAINd4CdkeDEf5oWZEBtaFIWMQLSk/tH+KHuDVuzqmDgSl+6MMybbtcXB//Q+7wACfvh8AAA=="

File diff suppressed because it is too large
+ 0 - 0
docs/assets/search.js


File diff suppressed because it is too large
+ 0 - 0
docs/classes/ModelDataTransformer.html


File diff suppressed because it is too large
+ 0 - 0
docs/functions/isPromise.html


File diff suppressed because it is too large
+ 0 - 0
docs/functions/transformPromise.html


File diff suppressed because it is too large
+ 3 - 0
docs/modules.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/PropertyTransformOptions.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/PropertyTransformer.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/PropertyTransformerContext.html


File diff suppressed because it is too large
+ 0 - 0
docs/types/ValueOrPromise.html


+ 1 - 0
package.json

@@ -40,6 +40,7 @@
     "@commitlint/config-conventional": "^18.6.2",
     "@commitlint/config-conventional": "^18.6.2",
     "@types/chai": "^4.3.11",
     "@types/chai": "^4.3.11",
     "@types/chai-as-promised": "^7.1.8",
     "@types/chai-as-promised": "^7.1.8",
+    "@types/chai-spies": "^1.0.6",
     "@types/mocha": "^10.0.6",
     "@types/mocha": "^10.0.6",
     "@typescript-eslint/eslint-plugin": "^7.0.2",
     "@typescript-eslint/eslint-plugin": "^7.0.2",
     "@typescript-eslint/parser": "^7.0.2",
     "@typescript-eslint/parser": "^7.0.2",

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

@@ -22,32 +22,32 @@ export class DataTransformationDecorator extends Service {
     const transformer = this.getService(ModelDataTransformer);
     const transformer = this.getService(ModelDataTransformer);
 
 
     const create = adapter.create;
     const create = adapter.create;
-    adapter.create = function (modelName, modelData, filter) {
-      modelData = transformer.transform(modelName, modelData);
+    adapter.create = async function (modelName, modelData, filter) {
+      modelData = await transformer.transform(modelName, modelData);
       return create.call(this, modelName, modelData, filter);
       return create.call(this, modelName, modelData, filter);
     };
     };
 
 
     const replaceById = adapter.replaceById;
     const replaceById = adapter.replaceById;
-    adapter.replaceById = function (modelName, id, modelData, filter) {
-      modelData = transformer.transform(modelName, modelData);
+    adapter.replaceById = async function (modelName, id, modelData, filter) {
+      modelData = await transformer.transform(modelName, modelData);
       return replaceById.call(this, modelName, id, modelData, filter);
       return replaceById.call(this, modelName, id, modelData, filter);
     };
     };
 
 
     const replaceOrCreate = adapter.replaceOrCreate;
     const replaceOrCreate = adapter.replaceOrCreate;
-    adapter.replaceOrCreate = function (modelName, modelData, filter) {
-      modelData = transformer.transform(modelName, modelData);
+    adapter.replaceOrCreate = async function (modelName, modelData, filter) {
+      modelData = await transformer.transform(modelName, modelData);
       return replaceOrCreate.call(this, modelName, modelData, filter);
       return replaceOrCreate.call(this, modelName, modelData, filter);
     };
     };
 
 
     const patch = adapter.patch;
     const patch = adapter.patch;
-    adapter.patch = function (modelName, modelData, where) {
-      modelData = transformer.transform(modelName, modelData, true);
+    adapter.patch = async function (modelName, modelData, where) {
+      modelData = await transformer.transform(modelName, modelData, true);
       return patch.call(this, modelName, modelData, where);
       return patch.call(this, modelName, modelData, where);
     };
     };
 
 
     const patchById = adapter.patchById;
     const patchById = adapter.patchById;
-    adapter.patchById = function (modelName, id, modelData, filter) {
-      modelData = transformer.transform(modelName, modelData, true);
+    adapter.patchById = async function (modelName, id, modelData, filter) {
+      modelData = await transformer.transform(modelName, modelData, true);
       return patchById.call(this, modelName, id, modelData, filter);
       return patchById.call(this, modelName, id, modelData, filter);
     };
     };
   }
   }

+ 150 - 52
src/adapter/decorator/data-transformation-decorator.spec.js

@@ -4,38 +4,52 @@ import {Adapter} from '../adapter.js';
 import {Schema} from '../../schema.js';
 import {Schema} from '../../schema.js';
 import {ModelDataTransformer} from '../../definition/index.js';
 import {ModelDataTransformer} from '../../definition/index.js';
 
 
-const S = new Schema();
-S.defineModel({name: 'model'});
+const MODEL_NAME = 'myModel';
+const MODEL_DATA = {kind: 'modelData'};
+const TRANSFORMED_DATA = {kind: 'transformedData'};
+const WHERE_CLAUSE = {kind: {existed: true}};
+const FILTER_CLAUSE = {where: WHERE_CLAUSE};
+const DUMMY_ID = 1;
 
 
 class TestAdapter extends Adapter {
 class TestAdapter extends Adapter {
-  // eslint-disable-next-line no-unused-vars
   create(modelName, modelData, filter = undefined) {
   create(modelName, modelData, filter = undefined) {
+    expect(modelName).to.be.eq(MODEL_NAME);
+    expect(modelData).to.be.eql(TRANSFORMED_DATA);
+    expect(filter).to.be.eql(FILTER_CLAUSE);
     return Promise.resolve(modelData);
     return Promise.resolve(modelData);
   }
   }
-
-  // eslint-disable-next-line no-unused-vars
   replaceById(modelName, id, modelData, filter = undefined) {
   replaceById(modelName, id, modelData, filter = undefined) {
+    expect(modelName).to.be.eq(MODEL_NAME);
+    expect(id).to.be.eq(DUMMY_ID);
+    expect(modelData).to.be.eql(TRANSFORMED_DATA);
+    expect(filter).to.be.eql(FILTER_CLAUSE);
     return Promise.resolve(modelData);
     return Promise.resolve(modelData);
   }
   }
-
-  // eslint-disable-next-line no-unused-vars
   replaceOrCreate(modelName, modelData, filter = undefined) {
   replaceOrCreate(modelName, modelData, filter = undefined) {
+    expect(modelName).to.be.eq(MODEL_NAME);
+    expect(modelData).to.be.eql(TRANSFORMED_DATA);
+    expect(filter).to.be.eql(FILTER_CLAUSE);
     return Promise.resolve(modelData);
     return Promise.resolve(modelData);
   }
   }
-
-  // eslint-disable-next-line no-unused-vars
   patch(modelName, modelData, where = undefined) {
   patch(modelName, modelData, where = undefined) {
+    expect(modelName).to.be.eq(MODEL_NAME);
+    expect(modelData).to.be.eql(TRANSFORMED_DATA);
+    expect(where).to.be.eql(WHERE_CLAUSE);
     return Promise.resolve(modelData);
     return Promise.resolve(modelData);
   }
   }
-
-  // eslint-disable-next-line no-unused-vars
   patchById(modelName, id, modelData, filter = undefined) {
   patchById(modelName, id, modelData, filter = undefined) {
+    expect(modelName).to.be.eq(MODEL_NAME);
+    expect(id).to.be.eq(DUMMY_ID);
+    expect(modelData).to.be.eql(TRANSFORMED_DATA);
+    expect(filter).to.be.eql(FILTER_CLAUSE);
     return Promise.resolve(modelData);
     return Promise.resolve(modelData);
   }
   }
 }
 }
 
 
+const S = new Schema();
+S.defineModel({name: MODEL_NAME});
 const A = S.getService(TestAdapter);
 const A = S.getService(TestAdapter);
-const V = S.getService(ModelDataTransformer);
+const T = S.getService(ModelDataTransformer);
 const sandbox = chai.spy.sandbox();
 const sandbox = chai.spy.sandbox();
 
 
 describe('DataTransformationDecorator', function () {
 describe('DataTransformationDecorator', function () {
@@ -43,53 +57,137 @@ describe('DataTransformationDecorator', function () {
     sandbox.restore();
     sandbox.restore();
   });
   });
 
 
-  it('overrides the "create" method and transforms a given data', async function () {
-    const modelData = {kind: 'modelData'};
-    const transformedData = {kind: 'transformedData'};
-    sandbox.on(V, 'transform', () => transformedData);
-    const res = await A.create('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);
+  describe('overrides the "create" method', function () {
+    it('transforms the given data', async function () {
+      sandbox.on(T, 'transform', () => TRANSFORMED_DATA);
+      const res = await A.create(MODEL_NAME, MODEL_DATA, FILTER_CLAUSE);
+      expect(res).to.be.eql(TRANSFORMED_DATA);
+      expect(T.transform).to.be.called.once;
+      expect(T.transform).to.be.called.with.exactly(MODEL_NAME, MODEL_DATA);
+    });
+
+    it('resolves the transformation promise', async function () {
+      sandbox.on(T, 'transform', () => Promise.resolve(TRANSFORMED_DATA));
+      const res = await A.create(MODEL_NAME, MODEL_DATA, FILTER_CLAUSE);
+      expect(res).to.be.eql(TRANSFORMED_DATA);
+      expect(T.transform).to.be.called.once;
+      expect(T.transform).to.be.called.with.exactly(MODEL_NAME, MODEL_DATA);
+    });
   });
   });
 
 
-  it('overrides the "replaceById" method and transforms a given data', async function () {
-    const modelData = {kind: 'modelData'};
-    const transformedData = {kind: 'transformedData'};
-    sandbox.on(V, 'transform', () => transformedData);
-    const res = await A.replaceById('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);
+  describe('overrides the "replaceById" method', function () {
+    it('transforms the given data', async function () {
+      sandbox.on(T, 'transform', () => TRANSFORMED_DATA);
+      const res = await A.replaceById(
+        MODEL_NAME,
+        DUMMY_ID,
+        MODEL_DATA,
+        FILTER_CLAUSE,
+      );
+      expect(res).to.be.eql(TRANSFORMED_DATA);
+      expect(T.transform).to.be.called.once;
+      expect(T.transform).to.be.called.with.exactly(MODEL_NAME, MODEL_DATA);
+    });
+
+    it('resolves the transformation promise', async function () {
+      sandbox.on(T, 'transform', () => Promise.resolve(TRANSFORMED_DATA));
+      const res = await A.replaceById(
+        MODEL_NAME,
+        DUMMY_ID,
+        MODEL_DATA,
+        FILTER_CLAUSE,
+      );
+      expect(res).to.be.eql(TRANSFORMED_DATA);
+      expect(T.transform).to.be.called.once;
+      expect(T.transform).to.be.called.with.exactly(MODEL_NAME, MODEL_DATA);
+    });
   });
   });
 
 
-  it('overrides the "replaceOrCreate" method and transforms a given data', async function () {
-    const modelData = {kind: 'modelData'};
-    const transformedData = {kind: 'transformedData'};
-    sandbox.on(V, 'transform', () => transformedData);
-    const res = await A.replaceOrCreate('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);
+  describe('overrides the "replaceOrCreate" method', function () {
+    it('transforms the given data', async function () {
+      sandbox.on(T, 'transform', () => TRANSFORMED_DATA);
+      const res = await A.replaceOrCreate(
+        MODEL_NAME,
+        MODEL_DATA,
+        FILTER_CLAUSE,
+      );
+      expect(res).to.be.eql(TRANSFORMED_DATA);
+      expect(T.transform).to.be.called.once;
+      expect(T.transform).to.be.called.with.exactly(MODEL_NAME, MODEL_DATA);
+    });
+
+    it('resolves the transformation promise', async function () {
+      sandbox.on(T, 'transform', () => Promise.resolve(TRANSFORMED_DATA));
+      const res = await A.replaceOrCreate(
+        MODEL_NAME,
+        MODEL_DATA,
+        FILTER_CLAUSE,
+      );
+      expect(res).to.be.eql(TRANSFORMED_DATA);
+      expect(T.transform).to.be.called.once;
+      expect(T.transform).to.be.called.with.exactly(MODEL_NAME, MODEL_DATA);
+    });
   });
   });
 
 
-  it('overrides the "patch" method and transforms a given data', async function () {
-    const modelData = {kind: 'modelData'};
-    const transformedData = {kind: 'transformedData'};
-    sandbox.on(V, 'transform', () => transformedData);
-    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);
+  describe('overrides the "patch" method', function () {
+    it('transforms the given data', async function () {
+      sandbox.on(T, 'transform', () => TRANSFORMED_DATA);
+      const res = await A.patch(MODEL_NAME, MODEL_DATA, WHERE_CLAUSE);
+      expect(res).to.be.eql(TRANSFORMED_DATA);
+      expect(T.transform).to.be.called.once;
+      expect(T.transform).to.be.called.with.exactly(
+        MODEL_NAME,
+        MODEL_DATA,
+        true,
+      );
+    });
+
+    it('resolves the transformation promise', async function () {
+      sandbox.on(T, 'transform', () => Promise.resolve(TRANSFORMED_DATA));
+      const res = await A.patch(MODEL_NAME, MODEL_DATA, WHERE_CLAUSE);
+      expect(res).to.be.eql(TRANSFORMED_DATA);
+      expect(T.transform).to.be.called.once;
+      expect(T.transform).to.be.called.with.exactly(
+        MODEL_NAME,
+        MODEL_DATA,
+        true,
+      );
+    });
   });
   });
 
 
-  it('overrides the "patchById" method and transforms a given data', async function () {
-    const modelData = {kind: 'modelData'};
-    const transformedData = {kind: 'transformedData'};
-    sandbox.on(V, 'transform', () => transformedData);
-    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);
+  describe('overrides the "patchById" method', function () {
+    it('transforms the given data', async function () {
+      sandbox.on(T, 'transform', () => TRANSFORMED_DATA);
+      const res = await A.patchById(
+        MODEL_NAME,
+        DUMMY_ID,
+        MODEL_DATA,
+        FILTER_CLAUSE,
+      );
+      expect(res).to.be.eql(TRANSFORMED_DATA);
+      expect(T.transform).to.be.called.once;
+      expect(T.transform).to.be.called.with.exactly(
+        MODEL_NAME,
+        MODEL_DATA,
+        true,
+      );
+    });
+
+    it('resolves the transformation promise', async function () {
+      sandbox.on(T, 'transform', () => Promise.resolve(TRANSFORMED_DATA));
+      const res = await A.patchById(
+        MODEL_NAME,
+        DUMMY_ID,
+        MODEL_DATA,
+        FILTER_CLAUSE,
+      );
+      expect(res).to.be.eql(TRANSFORMED_DATA);
+      expect(T.transform).to.be.called.once;
+      expect(T.transform).to.be.called.with.exactly(
+        MODEL_NAME,
+        MODEL_DATA,
+        true,
+      );
+    });
   });
   });
 });
 });

+ 2 - 1
src/definition/model/model-data-transformer.d.ts

@@ -1,5 +1,6 @@
 import {ModelData} from '../../types.js';
 import {ModelData} from '../../types.js';
 import {Service} from '@e22m4u/js-service';
 import {Service} from '@e22m4u/js-service';
+import {ValueOrPromise} from '../../types.js';
 
 
 /**
 /**
  * Model data transformer.
  * Model data transformer.
@@ -11,5 +12,5 @@ export declare class ModelDataTransformer extends Service {
    * @param modelName
    * @param modelName
    * @param modelData
    * @param modelData
    */
    */
-  transform(modelName: string, modelData: ModelData): ModelData;
+  transform(modelName: string, modelData: ModelData): ValueOrPromise<ModelData>;
 }
 }

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

@@ -1,6 +1,7 @@
 import {Service} from '@e22m4u/js-service';
 import {Service} from '@e22m4u/js-service';
 import {cloneDeep} from '../../utils/index.js';
 import {cloneDeep} from '../../utils/index.js';
 import {isPureObject} from '../../utils/index.js';
 import {isPureObject} from '../../utils/index.js';
+import {transformPromise} from '../../utils/index.js';
 import {EmptyValuesDefiner} from './properties/index.js';
 import {EmptyValuesDefiner} from './properties/index.js';
 import {InvalidArgumentError} from '../../errors/index.js';
 import {InvalidArgumentError} from '../../errors/index.js';
 import {ModelDefinitionUtils} from './model-definition-utils.js';
 import {ModelDefinitionUtils} from './model-definition-utils.js';
@@ -16,7 +17,7 @@ export class ModelDataTransformer extends Service {
    * @param {string} modelName
    * @param {string} modelName
    * @param {object} modelData
    * @param {object} modelData
    * @param {boolean} isPartial
    * @param {boolean} isPartial
-   * @returns {object}
+   * @returns {object|Promise<object>}
    */
    */
   transform(modelName, modelData, isPartial = false) {
   transform(modelName, modelData, isPartial = false) {
     if (!isPureObject(modelData))
     if (!isPureObject(modelData))
@@ -33,25 +34,27 @@ export class ModelDataTransformer extends Service {
       );
       );
     const propNames = Object.keys(isPartial ? modelData : propDefs);
     const propNames = Object.keys(isPartial ? modelData : propDefs);
     const transformedData = cloneDeep(modelData);
     const transformedData = cloneDeep(modelData);
-    propNames.forEach(propName => {
+    return propNames.reduce((transformedDataOrPromise, propName) => {
       const propDef = propDefs[propName];
       const propDef = propDefs[propName];
-      if (!propDef) return;
+      if (!propDef) return transformedDataOrPromise;
       const propType =
       const propType =
         modelDefinitionUtils.getDataTypeFromPropertyDefinition(propDef);
         modelDefinitionUtils.getDataTypeFromPropertyDefinition(propDef);
       const propValue = modelData[propName];
       const propValue = modelData[propName];
       const isEmpty = emptyValuesDefiner.isEmpty(propType, propValue);
       const isEmpty = emptyValuesDefiner.isEmpty(propType, propValue);
-      if (isEmpty) return;
-      const newPropValue = this._transformPropertyValue(
+      if (isEmpty) return transformedDataOrPromise;
+      const newPropValueOrPromise = this._transformPropertyValue(
         modelName,
         modelName,
         propName,
         propName,
         propDef,
         propDef,
         propValue,
         propValue,
       );
       );
-      if (propValue !== newPropValue) {
-        transformedData[propName] = newPropValue;
-      }
-    });
-    return transformedData;
+      return transformPromise(newPropValueOrPromise, newPropValue => {
+        return transformPromise(transformedDataOrPromise, resolvedData => {
+          if (newPropValue !== propValue) resolvedData[propName] = newPropValue;
+          return resolvedData;
+        });
+      });
+    }, transformedData);
   }
   }
 
 
   /**
   /**
@@ -80,15 +83,21 @@ export class ModelDataTransformer extends Service {
     if (transformDef && typeof transformDef === 'string') {
     if (transformDef && typeof transformDef === 'string') {
       return transformFn(propValue, transformDef);
       return transformFn(propValue, transformDef);
     } else if (Array.isArray(transformDef)) {
     } else if (Array.isArray(transformDef)) {
-      return transformDef.reduce(
-        (value, transformerName) => transformFn(value, transformerName),
+      return transformDef.reduce((valueOrPromise, transformerName) => {
+        return transformPromise(valueOrPromise, value => {
+          return transformFn(value, transformerName);
+        });
+      }, propValue);
+    } else if (transformDef !== null && typeof transformDef === 'object') {
+      return Object.keys(transformDef).reduce(
+        (valueOrPromise, transformerName) => {
+          const transformerOptions = transformDef[transformerName];
+          return transformPromise(valueOrPromise, value => {
+            return transformFn(value, transformerName, transformerOptions);
+          });
+        },
         propValue,
         propValue,
       );
       );
-    } else if (transformDef !== null && typeof transformDef === 'object') {
-      return Object.keys(transformDef).reduce((value, transformerName) => {
-        const transformerOptions = transformDef[transformerName];
-        return transformFn(value, transformerName, transformerOptions);
-      }, propValue);
     } else {
     } else {
       throw new InvalidArgumentError(
       throw new InvalidArgumentError(
         'The provided option "transform" of the property %v in the model %v ' +
         'The provided option "transform" of the property %v in the model %v ' +

+ 118 - 0
src/definition/model/model-data-transformer.spec.js

@@ -165,6 +165,40 @@ describe('ModelDataTransformer', function () {
         const res = T.transform('model', {}, true);
         const res = T.transform('model', {}, true);
         expect(res).to.be.eql({});
         expect(res).to.be.eql({});
       });
       });
+
+      it('transforms the property value by its asynchronous transformer', async function () {
+        const schema = new Schema();
+        const myTransformer1 = (value, options) => {
+          expect(options).to.be.undefined;
+          return Promise.resolve(`${value}2`);
+        };
+        const myTransformer2 = (value, options) => {
+          expect(options).to.be.undefined;
+          return Promise.resolve(`${value}3`);
+        };
+        schema
+          .getService(PropertyTransformerRegistry)
+          .addTransformer('myTransformer1', myTransformer1)
+          .addTransformer('myTransformer2', myTransformer2);
+        schema.defineModel({
+          name: 'model',
+          properties: {
+            foo: {
+              type: DataType.STRING,
+              transform: 'myTransformer1',
+            },
+            bar: {
+              type: DataType.STRING,
+              transform: 'myTransformer2',
+            },
+          },
+        });
+        const T = schema.getService(ModelDataTransformer);
+        const promise = T.transform('model', {foo: '1', bar: '2'});
+        expect(promise).to.be.instanceof(Promise);
+        const res = await promise;
+        expect(res).to.be.eql({foo: '12', bar: '23'});
+      });
     });
     });
 
 
     describe('the option "transform" with an array value', function () {
     describe('the option "transform" with an array value', function () {
@@ -316,6 +350,45 @@ describe('ModelDataTransformer', function () {
         const res = T.transform('model', {}, true);
         const res = T.transform('model', {}, true);
         expect(res).to.be.eql({});
         expect(res).to.be.eql({});
       });
       });
+
+      it('transforms the property value by its asynchronous transformers', async function () {
+        const schema = new Schema();
+        const myTransformer1 = (value, options) => {
+          expect(options).to.be.undefined;
+          return Promise.resolve(`${value}2`);
+        };
+        const myTransformer2 = (value, options) => {
+          expect(options).to.be.undefined;
+          return Promise.resolve(`${value}3`);
+        };
+        const myTransformer3 = (value, options) => {
+          expect(options).to.be.undefined;
+          return Promise.resolve(`${value}4`);
+        };
+        schema
+          .getService(PropertyTransformerRegistry)
+          .addTransformer('myTransformer1', myTransformer1)
+          .addTransformer('myTransformer2', myTransformer2)
+          .addTransformer('myTransformer3', myTransformer3);
+        schema.defineModel({
+          name: 'model',
+          properties: {
+            foo: {
+              type: DataType.STRING,
+              transform: ['myTransformer1', 'myTransformer2'],
+            },
+            bar: {
+              type: DataType.STRING,
+              transform: ['myTransformer2', 'myTransformer3'],
+            },
+          },
+        });
+        const T = schema.getService(ModelDataTransformer);
+        const promise = T.transform('model', {foo: '1', bar: '2'});
+        expect(promise).to.be.instanceof(Promise);
+        const res = await promise;
+        expect(res).to.be.eql({foo: '123', bar: '234'});
+      });
     });
     });
 
 
     describe('the option "transform" with an object value', function () {
     describe('the option "transform" with an object value', function () {
@@ -484,6 +557,51 @@ describe('ModelDataTransformer', function () {
         const res = T.transform('model', {}, true);
         const res = T.transform('model', {}, true);
         expect(res).to.be.eql({});
         expect(res).to.be.eql({});
       });
       });
+
+      it('transforms the property value by its asynchronous transformers', async function () {
+        const schema = new Schema();
+        const myTransformer1 = (value, options) => {
+          expect(options).to.be.eq('foo');
+          return Promise.resolve(`${value}2`);
+        };
+        const myTransformer2 = (value, options) => {
+          expect(options).to.be.eq('bar');
+          return Promise.resolve(`${value}3`);
+        };
+        const myTransformer3 = (value, options) => {
+          expect(options).to.be.eq('baz');
+          return Promise.resolve(`${value}4`);
+        };
+        schema
+          .getService(PropertyTransformerRegistry)
+          .addTransformer('myTransformer1', myTransformer1)
+          .addTransformer('myTransformer2', myTransformer2)
+          .addTransformer('myTransformer3', myTransformer3);
+        schema.defineModel({
+          name: 'model',
+          properties: {
+            foo: {
+              type: DataType.STRING,
+              transform: {
+                myTransformer1: 'foo',
+                myTransformer2: 'bar',
+              },
+            },
+            bar: {
+              type: DataType.STRING,
+              transform: {
+                myTransformer2: 'bar',
+                myTransformer3: 'baz',
+              },
+            },
+          },
+        });
+        const T = schema.getService(ModelDataTransformer);
+        const promise = T.transform('model', {foo: '1', bar: '2'});
+        expect(promise).to.be.instanceof(Promise);
+        const res = await promise;
+        expect(res).to.be.eql({foo: '123', bar: '234'});
+      });
     });
     });
 
 
     it('the option "transform" requires a non-empty String, an Array or an Object', function () {
     it('the option "transform" requires a non-empty String, an Array or an Object', function () {

+ 3 - 1
src/definition/model/properties/property-transformer/property-transformer.d.ts

@@ -1,3 +1,5 @@
+import {ValueOrPromise} from '../../../../types.js';
+
 /**
 /**
  * Property transformer context.
  * Property transformer context.
  */
  */
@@ -14,7 +16,7 @@ export declare type PropertyTransformer = (
   value: unknown,
   value: unknown,
   options: unknown,
   options: unknown,
   context: PropertyTransformerContext,
   context: PropertyTransformerContext,
-) => boolean;
+) => ValueOrPromise<unknown>;
 
 
 /**
 /**
  * Property transform options.
  * Property transform options.

+ 7 - 0
src/types.d.ts

@@ -36,3 +36,10 @@ export declare type Flatten<T> = Identity<{[k in keyof T]: T[k]}>;
 export interface Constructor<T = unknown> {
 export interface Constructor<T = unknown> {
   new (...args: any[]): T;
   new (...args: any[]): T;
 }
 }
+
+/**
+ * Representing a value or promise. This type is used
+ * to represent results of synchronous/asynchronous
+ * resolution of values.
+ */
+export type ValueOrPromise<T> = T | PromiseLike<T>;

+ 2 - 0
src/utils/index.d.ts

@@ -1,4 +1,5 @@
 export * from './is-ctor.js';
 export * from './is-ctor.js';
+export * from './is-promise.js';
 export * from './capitalize.js';
 export * from './capitalize.js';
 export * from './clone-deep.js';
 export * from './clone-deep.js';
 export * from './singularize.js';
 export * from './singularize.js';
@@ -7,6 +8,7 @@ export * from './get-ctor-name.js';
 export * from './is-pure-object.js';
 export * from './is-pure-object.js';
 export * from './string-to-regexp.js';
 export * from './string-to-regexp.js';
 export * from './get-value-by-path.js';
 export * from './get-value-by-path.js';
+export * from './transform-promise.js';
 export * from './select-object-keys.js';
 export * from './select-object-keys.js';
 export * from './exclude-object-keys.js';
 export * from './exclude-object-keys.js';
 export * from './get-decorator-target-type.js';
 export * from './get-decorator-target-type.js';

+ 2 - 0
src/utils/index.js

@@ -1,4 +1,5 @@
 export * from './is-ctor.js';
 export * from './is-ctor.js';
+export * from './is-promise.js';
 export * from './capitalize.js';
 export * from './capitalize.js';
 export * from './clone-deep.js';
 export * from './clone-deep.js';
 export * from './singularize.js';
 export * from './singularize.js';
@@ -7,6 +8,7 @@ export * from './get-ctor-name.js';
 export * from './is-pure-object.js';
 export * from './is-pure-object.js';
 export * from './string-to-regexp.js';
 export * from './string-to-regexp.js';
 export * from './get-value-by-path.js';
 export * from './get-value-by-path.js';
+export * from './transform-promise.js';
 export * from './select-object-keys.js';
 export * from './select-object-keys.js';
 export * from './exclude-object-keys.js';
 export * from './exclude-object-keys.js';
 export * from './get-decorator-target-type.js';
 export * from './get-decorator-target-type.js';

+ 10 - 0
src/utils/is-promise.d.ts

@@ -0,0 +1,10 @@
+/**
+ * Check whether a value is a Promise-like
+ * instance. Recognizes both native promises
+ * and third-party promise libraries.
+ *
+ * @param value
+ */
+export declare function isPromise<T>(
+  value: T | PromiseLike<T> | undefined
+): value is PromiseLike<T>;

+ 13 - 0
src/utils/is-promise.js

@@ -0,0 +1,13 @@
+/**
+ * Check whether a value is a Promise-like
+ * instance. Recognizes both native promises
+ * and third-party promise libraries.
+ *
+ * @param {*} value
+ * @returns {boolean}
+ */
+export function isPromise(value) {
+  if (!value) return false;
+  if (typeof value !== 'object') return false;
+  return typeof value.then === 'function';
+}

+ 23 - 0
src/utils/is-promise.spec.js

@@ -0,0 +1,23 @@
+import {expect} from 'chai';
+import {describe} from 'mocha';
+import {isPromise} from './is-promise.js';
+
+describe('isPromise', () => {
+  it('returns true if the given value has type of PromiseLike', function () {
+    expect(isPromise(Promise.resolve())).to.be.true;
+    expect(isPromise('string')).to.be.false;
+    expect(isPromise('')).to.be.false;
+    expect(isPromise(10)).to.be.false;
+    expect(isPromise(0)).to.be.false;
+    expect(isPromise(true)).to.be.false;
+    expect(isPromise(false)).to.be.false;
+    expect(isPromise(undefined)).to.be.false;
+    expect(isPromise(null)).to.be.false;
+    expect(isPromise({foo: 'bar'})).to.be.false;
+    expect(isPromise({})).to.be.false;
+    expect(isPromise([1, 2, 3])).to.be.false;
+    expect(isPromise([])).to.be.false;
+    expect(isPromise(NaN)).to.be.false;
+    expect(isPromise(() => 10)).to.be.false;
+  });
+});

+ 13 - 0
src/utils/transform-promise.d.ts

@@ -0,0 +1,13 @@
+import {ValueOrPromise} from '../types.js';
+
+/**
+ * Transform a value or promise with a function that
+ * produces a new value or promise.
+ *
+ * @param valueOrPromise
+ * @param transformer
+ */
+export declare function transformPromise<T, V>(
+  valueOrPromise: ValueOrPromise<T>,
+  transformer: (val: T) => ValueOrPromise<V>,
+): ValueOrPromise<V>;

+ 15 - 0
src/utils/transform-promise.js

@@ -0,0 +1,15 @@
+import {isPromise} from './is-promise.js';
+
+/**
+ * Transform a value or promise with a function that
+ * produces a new value or promise.
+ *
+ * @param {*} valueOrPromise
+ * @param {Function} transformer
+ * @returns {*}
+ */
+export function transformPromise(valueOrPromise, transformer) {
+  return isPromise(valueOrPromise)
+    ? valueOrPromise.then(transformer)
+    : transformer(valueOrPromise);
+}

+ 20 - 0
src/utils/transform-promise.spec.js

@@ -0,0 +1,20 @@
+import {expect} from 'chai';
+import {describe} from 'mocha';
+import {transformPromise} from './transform-promise.js';
+
+describe('transformPromise', function () {
+  it('transforms the given value', function () {
+    const value = 'my-value';
+    const transformer = v => v.toUpperCase();
+    const result = transformPromise(value, transformer);
+    expect(result).to.be.eq('MY-VALUE');
+  });
+
+  it('transforms the given promise', async function () {
+    const promise = Promise.resolve('my-value');
+    const transformer = v => v.toUpperCase();
+    const result = await transformPromise(promise, transformer);
+    await promise;
+    expect(result).to.be.eq('MY-VALUE');
+  });
+});

Some files were not shown because too many files changed in this diff