diff --git a/src/ClassTransformer.ts b/src/ClassTransformer.ts index c244385b4..6d7b9cc89 100644 --- a/src/ClassTransformer.ts +++ b/src/ClassTransformer.ts @@ -3,6 +3,7 @@ import { TransformOperationExecutor } from './TransformOperationExecutor'; import { TransformationType } from './enums'; import { ClassConstructor } from './interfaces'; import { defaultOptions } from './constants/default-options.constant'; +import { instanceToPlainAsync } from './utils'; export class ClassTransformer { // ------------------------------------------------------------------------- @@ -25,6 +26,29 @@ export class ClassTransformer { return executor.transform(undefined, object, undefined, undefined, undefined, undefined); } + /** + * Converts class (constructor) object to plain (literal) object with support to promise values resolution + */ + + instanceToPlainAsync>(object: T, options?: ClassTransformOptions): Record; + instanceToPlainAsync>( + object: T[], + options?: ClassTransformOptions + ): Record[]; + instanceToPlainAsync>( + object: T | T[], + options?: ClassTransformOptions + ): Record | Record[] { + const executor = new TransformOperationExecutor(TransformationType.CLASS_TO_PLAIN, { + ...defaultOptions, + ...options, + }); + return instanceToPlainAsync( + executor.transform(undefined, object, undefined, undefined, undefined, undefined), + this.instanceToPlain + ); + } + /** * Converts class (constructor) object to plain (literal) object. * Uses given plain object as source object (it means fills given plain object with data from class object). diff --git a/src/index.ts b/src/index.ts index 3bef5422b..1a2de4d54 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,6 +35,18 @@ export function instanceToPlain( return classTransformer.instanceToPlain(object, options); } +/** + * Converts class (constructor) object to plain (literal) object with support to promise values resolution + */ +export function instanceToPlainAsync(object: T, options?: ClassTransformOptions): Record; +export function instanceToPlainAsync(object: T[], options?: ClassTransformOptions): Record[]; +export function instanceToPlainAsync( + object: T | T[], + options?: ClassTransformOptions +): Record | Record[] { + return classTransformer.instanceToPlainAsync(object, options); +} + /** * Converts class (constructor) object to plain (literal) object. * Uses given plain object as source object (it means fills given plain object with data from class object). diff --git a/src/utils/index.ts b/src/utils/index.ts index 382ad2d7f..8e8857ed0 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,2 +1,3 @@ export * from './get-global.util'; export * from './is-promise.util'; +export * from './promise-values.util'; diff --git a/src/utils/promise-values.util.ts b/src/utils/promise-values.util.ts new file mode 100644 index 000000000..43a6dc2b3 --- /dev/null +++ b/src/utils/promise-values.util.ts @@ -0,0 +1,56 @@ +function promisesTracker(value: any, promises: Promise[]) { + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + if (value[i] instanceof Promise) { + promises.push(value[i]); + } else { + promisesTracker(value, promises); + } + } + } + + if (value instanceof Object) { + for (const key of Object.keys(value)) { + if (value[key] instanceof Promise) { + promises.push(value[key]); + } else { + promisesTracker(value[key], promises); + } + } + } + + return value; +} + +async function resolvePromises(value: any) { + if (Array.isArray(value)) { + for (let i = 0; i < value.length; i++) { + if (value[i] instanceof Promise) { + value[i] = await value[i]; + } else { + value[i] = await resolvePromises(value[i]); + } + } + } + + if (value instanceof Object) { + for (const key of Object.keys(value)) { + if (value[key] instanceof Promise) { + value[key] = await value[key]; + } else { + value[key] = await resolvePromises(value[key]); + } + } + } + + return value; +} + +export async function instanceToPlainAsync(value: any, instanceToPlainMethod: any): Promise> { + const valuesPromises: Promise[] = []; + + promisesTracker(instanceToPlainMethod(value), valuesPromises); + await Promise.all(valuesPromises); + + return resolvePromises(value); +} diff --git a/test/functional/transformer-async.spec.ts b/test/functional/transformer-async.spec.ts new file mode 100644 index 000000000..5a773a205 --- /dev/null +++ b/test/functional/transformer-async.spec.ts @@ -0,0 +1,21 @@ +import 'reflect-metadata'; +import { defaultMetadataStorage } from '../../src/storage'; +import { Expose } from '../../src/decorators'; +import { instanceToPlainAsync } from '../../src/index'; + +describe('applying a transformation at a class that contains promise value', () => { + beforeEach(() => defaultMetadataStorage.clear()); + afterEach(() => defaultMetadataStorage.clear()); + + it('should resolve the promise value', async () => { + class User { + @Expose() + name: Promise = new Promise(resolve => { + resolve('Jonathan'); + }); + } + + const plainUser = await instanceToPlainAsync(new User()); + expect(plainUser.name).toEqual('Jonathan'); + }); +});