Skip to content

Commit 022f63b

Browse files
authored
fix: prevent error during transformation when object has property with Promise (#262)
1 parent 5e332af commit 022f63b

File tree

4 files changed

+98
-1
lines changed

4 files changed

+98
-1
lines changed

src/TransformOperationExecutor.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { defaultMetadataStorage } from './storage';
22
import { TypeHelpOptions, TypeOptions, ClassTransformOptions, TypeMetadata } from './interfaces';
33
import { TransformationType } from './enums';
4-
import { getGlobal } from './utils';
4+
import { getGlobal, isPromise } from './utils';
55

66
function instantiateArrayType(arrayType: Function): Array<any> | Set<any> {
77
const array = new (arrayType as any)();
@@ -64,6 +64,7 @@ export class TransformOperationExecutor {
6464
if (!targetType.options.keepDiscriminatorProperty)
6565
delete subValue[targetType.options.discriminator.property];
6666
}
67+
6768
if (this.transformationType === TransformationType.CLASS_TO_CLASS) {
6869
realTargetType = subValue.constructor;
6970
}
@@ -116,7 +117,16 @@ export class TransformOperationExecutor {
116117
} else if (!!getGlobal().Buffer && (targetType === Buffer || value instanceof Buffer) && !isMap) {
117118
if (value === null || value === undefined) return value;
118119
return Buffer.from(value);
120+
} else if (isPromise(value) && !isMap) {
121+
return new Promise((resolve, reject) => {
122+
value.then(
123+
(data: any) => resolve(this.transform(undefined, data, targetType, undefined, undefined, level + 1)),
124+
reject
125+
);
126+
});
119127
} else if (!isMap && value !== null && typeof value === 'object' && typeof value.then === 'function') {
128+
// Note: We should not enter this, as promise has been handled above
129+
// This option simply returns the Promise preventing a JS error from happening and should be an inaccessible path.
120130
return value; // skip promise transformation
121131
} else if (typeof value === 'object' && value !== null) {
122132
// try to guess the type

src/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export * from './get-global.util';
2+
export * from './is-promise.util';

src/utils/is-promise.util.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export function isPromise<T>(p: any): p is Promise<T> {
2+
return p !== null && typeof p === 'object' && typeof p.then === 'function';
3+
}

test/functional/promise-field.spec.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import 'reflect-metadata';
2+
import { defaultMetadataStorage } from '../../src/storage';
3+
import { plainToClass, Type, classToPlain } from '../../src';
4+
5+
describe('promise field', () => {
6+
it('should transform plan to class with promise field', async () => {
7+
defaultMetadataStorage.clear();
8+
9+
class PromiseClass {
10+
promise: Promise<string>;
11+
}
12+
13+
const plain = {
14+
promise: Promise.resolve('hi'),
15+
};
16+
17+
const instance = plainToClass(PromiseClass, plain);
18+
expect(instance.promise).toBeInstanceOf(Promise);
19+
const value = await instance.promise;
20+
expect(value).toBe('hi');
21+
});
22+
23+
it('should transform class with promise field to plain', async () => {
24+
class PromiseClass {
25+
promise: Promise<string>;
26+
27+
constructor(promise: Promise<any>) {
28+
this.promise = promise;
29+
}
30+
}
31+
32+
const instance = new PromiseClass(Promise.resolve('hi'));
33+
const plain = classToPlain(instance) as any;
34+
expect(plain).toHaveProperty('promise');
35+
const value = await plain.promise;
36+
expect(value).toBe('hi');
37+
});
38+
39+
it('should clone promise result', async () => {
40+
defaultMetadataStorage.clear();
41+
42+
class PromiseClass {
43+
promise: Promise<string[]>;
44+
}
45+
46+
const array = ['hi', 'my', 'name'];
47+
const plain = {
48+
promise: Promise.resolve(array),
49+
};
50+
51+
const instance = plainToClass(PromiseClass, plain);
52+
const value = await instance.promise;
53+
expect(value).toEqual(array);
54+
55+
// modify transformed array to prove it's not referencing original array
56+
value.push('is');
57+
expect(value).not.toEqual(array);
58+
});
59+
60+
it('should support Type decorator', async () => {
61+
class PromiseClass {
62+
@Type(() => InnerClass)
63+
promise: Promise<InnerClass>;
64+
}
65+
66+
class InnerClass {
67+
position: string;
68+
69+
constructor(position: string) {
70+
this.position = position;
71+
}
72+
}
73+
74+
const plain = {
75+
promise: Promise.resolve(new InnerClass('developer')),
76+
};
77+
78+
const instance = plainToClass(PromiseClass, plain);
79+
const value = await instance.promise;
80+
expect(value).toBeInstanceOf(InnerClass);
81+
expect(value.position).toBe('developer');
82+
});
83+
});

0 commit comments

Comments
 (0)