Skip to content

Commit a87e56d

Browse files
authored
Merge pull request #80 from gempain/feature/expose-object-to-transform
Feature/expose object to transform
2 parents 7343716 + 63d0efe commit a87e56d

10 files changed

+131
-68
lines changed

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,8 @@ Library will handle proper transformation automatically.
568568

569569
## Additional data transformation
570570

571+
### Basic usage
572+
571573
You can perform additional data transformation using `@Transform` decorator.
572574
For example, you want to make your `Date` object to be a `moment` object when you are
573575
transforming object from plain to class:
@@ -591,6 +593,20 @@ Now when you call `plainToClass` and send a plain representation of the Photo ob
591593
it will convert a date value in your photo object to moment date.
592594
`@Transform` decorator also supports groups and versioning.
593595

596+
### Advanced usage
597+
598+
The `@Transform` decorator is given more arguments to let you configure how you want the transformation to be done.
599+
600+
```
601+
@Transform((value, obj, type) => value)
602+
```
603+
604+
| Argument | Description
605+
|--------------------|---------------------------------------------------------------------------------|
606+
| `value` | The property value before the transformation.
607+
| `obj` | The transformation source object.
608+
| `type` | The transformation type.
609+
594610
## Other decorators
595611
| Signature | Example | Description
596612
|--------------------|------------------------------------------|---------------------------------------------|

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
"typescript": "^2.0.10"
6060
},
6161
"scripts": {
62-
"test": "gulp tests"
62+
"test": "gulp tests",
63+
"package": "gulp package"
6364
}
6465
}

src/ClassTransformer.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import {ClassTransformOptions} from "./ClassTransformOptions";
2-
import {TransformOperationExecutor} from "./TransformOperationExecutor";
2+
import {TransformOperationExecutor, TransformationType} from "./TransformOperationExecutor";
33

4-
export type ClassType<T> = { new (...args: any[]): T; };
4+
export type ClassType<T> = {
5+
new (...args: any[]): T;
6+
};
57

68
export class ClassTransformer {
79

@@ -15,7 +17,7 @@ export class ClassTransformer {
1517
classToPlain<T extends Object>(object: T, options?: ClassTransformOptions): Object;
1618
classToPlain<T extends Object>(object: T[], options?: ClassTransformOptions): Object[];
1719
classToPlain<T extends Object>(object: T|T[], options?: ClassTransformOptions): Object|Object[] {
18-
const executor = new TransformOperationExecutor("classToPlain", options || {});
20+
const executor = new TransformOperationExecutor(TransformationType.CLASS_TO_PLAIN, options || {});
1921
return executor.transform(undefined, object, undefined, undefined, undefined, undefined);
2022
}
2123

@@ -27,7 +29,7 @@ export class ClassTransformer {
2729
classToPlainFromExist<T extends Object, P>(object: T, plainObject: P, options?: ClassTransformOptions): T;
2830
classToPlainFromExist<T extends Object, P>(object: T, plainObjects: P[], options?: ClassTransformOptions): T[];
2931
classToPlainFromExist<T extends Object, P>(object: T, plainObject: P|P[], options?: ClassTransformOptions): T|T[] {
30-
const executor = new TransformOperationExecutor("classToPlain", options || {});
32+
const executor = new TransformOperationExecutor(TransformationType.CLASS_TO_PLAIN, options || {});
3133
return executor.transform(plainObject, object, undefined, undefined, undefined, undefined);
3234
}
3335

@@ -37,7 +39,7 @@ export class ClassTransformer {
3739
plainToClass<T extends Object, V extends Array<any>>(cls: ClassType<T>, plain: V, options?: ClassTransformOptions): T[];
3840
plainToClass<T extends Object, V>(cls: ClassType<T>, plain: V, options?: ClassTransformOptions): T;
3941
plainToClass<T extends Object, V>(cls: ClassType<T>, plain: V|V[], options?: ClassTransformOptions): T|T[] {
40-
const executor = new TransformOperationExecutor("plainToClass", options || {});
42+
const executor = new TransformOperationExecutor(TransformationType.PLAIN_TO_CLASS, options || {});
4143
return executor.transform(undefined, plain, cls, undefined, undefined, undefined);
4244
}
4345

@@ -49,7 +51,7 @@ export class ClassTransformer {
4951
plainToClassFromExist<T extends Object, V extends Array<any>>(clsObject: T, plain: V, options?: ClassTransformOptions): T;
5052
plainToClassFromExist<T extends Object, V>(clsObject: T, plain: V, options?: ClassTransformOptions): T[];
5153
plainToClassFromExist<T extends Object, V>(clsObject: T, plain: V|V[], options?: ClassTransformOptions): T|T[] {
52-
const executor = new TransformOperationExecutor("plainToClass", options || {});
54+
const executor = new TransformOperationExecutor(TransformationType.PLAIN_TO_CLASS, options || {});
5355
return executor.transform(clsObject, plain, undefined, undefined, undefined, undefined);
5456
}
5557

@@ -59,7 +61,7 @@ export class ClassTransformer {
5961
classToClass<T>(object: T, options?: ClassTransformOptions): T;
6062
classToClass<T>(object: T[], options?: ClassTransformOptions): T[];
6163
classToClass<T>(object: T|T[], options?: ClassTransformOptions): T|T[] {
62-
const executor = new TransformOperationExecutor("classToClass", options || {});
64+
const executor = new TransformOperationExecutor(TransformationType.CLASS_TO_CLASS, options || {});
6365
return executor.transform(undefined, object, undefined, undefined, undefined, undefined);
6466
}
6567

@@ -71,7 +73,7 @@ export class ClassTransformer {
7173
classToClassFromExist<T>(object: T, fromObject: T, options?: ClassTransformOptions): T;
7274
classToClassFromExist<T>(object: T, fromObjects: T[], options?: ClassTransformOptions): T[];
7375
classToClassFromExist<T>(object: T, fromObject: T|T[], options?: ClassTransformOptions): T|T[] {
74-
const executor = new TransformOperationExecutor("classToClass", options || {});
76+
const executor = new TransformOperationExecutor(TransformationType.CLASS_TO_CLASS, options || {});
7577
return executor.transform(fromObject, object, undefined, undefined, undefined, undefined);
7678
}
7779

@@ -100,4 +102,4 @@ export class ClassTransformer {
100102
return this.plainToClass(cls, jsonObject, options);
101103
}
102104

103-
}
105+
}

src/TransformOperationExecutor.ts

Lines changed: 28 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import {ClassTransformOptions} from "./ClassTransformOptions";
22
import {defaultMetadataStorage} from "./storage";
33
import {TypeOptions} from "./metadata/ExposeExcludeOptions";
4-
import {ExposeMetadata} from "./metadata/ExposeMetadata";
54

6-
export type TransformationType = "plainToClass"|"classToPlain"|"classToClass";
5+
export enum TransformationType {
6+
PLAIN_TO_CLASS,
7+
CLASS_TO_PLAIN,
8+
CLASS_TO_CLASS
9+
}
710

811
export class TransformOperationExecutor {
912

1013
// -------------------------------------------------------------------------
1114
// Private Properties
1215
// -------------------------------------------------------------------------
1316

14-
private transformedTypesMap = new Map<Object, { level: number, object: Object }>();
17+
private transformedTypesMap = new Map<Object, {level: number, object: Object}>();
1518

1619
// -------------------------------------------------------------------------
1720
// Constructor
@@ -33,7 +36,7 @@ export class TransformOperationExecutor {
3336
level: number = 0) {
3437

3538
if (value instanceof Array || value instanceof Set) {
36-
const newValue = arrayType && this.transformationType === "plainToClass" ? new (arrayType as any)() : [];
39+
const newValue = arrayType && this.transformationType === TransformationType.PLAIN_TO_CLASS ? new (arrayType as any)() : [];
3740
(value as any[]).forEach((subValue, index) => {
3841
const subSource = source ? source[index] : undefined;
3942
if (!this.options.enableCircularCheck || !this.isCircular(subValue, level)) {
@@ -43,7 +46,7 @@ export class TransformOperationExecutor {
4346
} else {
4447
newValue.push(value);
4548
}
46-
} else if (this.transformationType === "classToClass") {
49+
} else if (this.transformationType === TransformationType.CLASS_TO_CLASS) {
4750
if (newValue instanceof Set) {
4851
newValue.add(subValue);
4952
} else {
@@ -74,17 +77,17 @@ export class TransformOperationExecutor {
7477
} else if (value instanceof Object) {
7578

7679
// try to guess the type
77-
if (!targetType && value.constructor !== Object/* && operationType === "classToPlain"*/) targetType = value.constructor;
80+
if (!targetType && value.constructor !== Object/* && TransformationType === TransformationType.CLASS_TO_PLAIN*/) targetType = value.constructor;
7881
if (!targetType && source) targetType = source.constructor;
7982

8083
if (this.options.enableCircularCheck) {
8184
// add transformed type to prevent circular references
82-
this.transformedTypesMap.set(value, { level: level, object: value });
85+
this.transformedTypesMap.set(value, {level: level, object: value});
8386
}
8487

8588
const keys = this.getKeys(targetType, value);
8689
let newValue: any = source ? source : {};
87-
if (!source && (this.transformationType === "plainToClass" || this.transformationType === "classToClass")) {
90+
if (!source && (this.transformationType === TransformationType.PLAIN_TO_CLASS || this.transformationType === TransformationType.CLASS_TO_CLASS)) {
8891
if (isMap) {
8992
newValue = new Map();
9093
} else if (targetType) {
@@ -99,14 +102,14 @@ export class TransformOperationExecutor {
99102

100103
let valueKey = key, newValueKey = key, propertyName = key;
101104
if (!this.options.ignoreDecorators && targetType) {
102-
if (this.transformationType === "plainToClass") {
105+
if (this.transformationType === TransformationType.PLAIN_TO_CLASS) {
103106
const exposeMetadata = defaultMetadataStorage.findExposeMetadataByCustomName(targetType, key);
104107
if (exposeMetadata) {
105108
propertyName = exposeMetadata.propertyName;
106109
newValueKey = exposeMetadata.propertyName;
107110
}
108111

109-
} else if (this.transformationType === "classToPlain" || this.transformationType === "classToClass") {
112+
} else if (this.transformationType === TransformationType.CLASS_TO_PLAIN || this.transformationType === TransformationType.CLASS_TO_CLASS) {
110113
const exposeMetadata = defaultMetadataStorage.findExposeMetadata(targetType, key);
111114
if (exposeMetadata && exposeMetadata.options && exposeMetadata.options.name)
112115
newValueKey = exposeMetadata.options.name;
@@ -131,7 +134,7 @@ export class TransformOperationExecutor {
131134
} else if (targetType) {
132135
const metadata = defaultMetadataStorage.findTypeMetadata(targetType, propertyName);
133136
if (metadata) {
134-
const options: TypeOptions = { newObject: newValue, object: value, property: propertyName };
137+
const options: TypeOptions = {newObject: newValue, object: value, property: propertyName};
135138
type = metadata.typeFunction(options);
136139
isSubValueMap = isSubValueMap || metadata.reflectedType === Map;
137140
} else if (this.options.targetMaps) { // try to find a type in target maps
@@ -143,34 +146,34 @@ export class TransformOperationExecutor {
143146

144147
// if value is an array try to get its custom array type
145148
const arrayType = value[valueKey] instanceof Array ? this.getReflectedType(targetType, propertyName) : undefined;
146-
// const subValueKey = operationType === "plainToClass" && newKeyName ? newKeyName : key;
149+
// const subValueKey = TransformationType === TransformationType.PLAIN_TO_CLASS && newKeyName ? newKeyName : key;
147150
const subSource = source ? source[valueKey] : undefined;
148151

149152
// if its deserialization then type if required
150153
// if we uncomment this types like string[] will not work
151-
// if (this.transformationType === "plainToClass" && !type && subValue instanceof Object && !(subValue instanceof Date))
154+
// if (this.transformationType === TransformationType.PLAIN_TO_CLASS && !type && subValue instanceof Object && !(subValue instanceof Date))
152155
// throw new Error(`Cannot determine type for ${(targetType as any).name }.${propertyName}, did you forget to specify a @Type?`);
153156

154157
// if newValue is a source object that has method that match newKeyName then skip it
155158
if (newValue.constructor.prototype) {
156159
const descriptor = Object.getOwnPropertyDescriptor(newValue.constructor.prototype, newValueKey);
157-
if ((this.transformationType === "plainToClass" || this.transformationType === "classToClass")
158-
&& (newValue[newValueKey] instanceof Function || (descriptor && !descriptor.set))) // || operationType === "classToClass"
160+
if ((this.transformationType === TransformationType.PLAIN_TO_CLASS || this.transformationType === TransformationType.CLASS_TO_CLASS)
161+
&& (newValue[newValueKey] instanceof Function || (descriptor && !descriptor.set))) // || TransformationType === TransformationType.CLASS_TO_CLASS
159162
continue;
160163
}
161164

162165
if (!this.options.enableCircularCheck || !this.isCircular(subValue, level)) {
163-
let transformKey = this.transformationType === "plainToClass" ? newValueKey : key;
166+
let transformKey = this.transformationType === TransformationType.PLAIN_TO_CLASS ? newValueKey : key;
164167
let finalValue = this.transform(subSource, subValue, type, arrayType, isSubValueMap, level + 1);
165-
finalValue = this.applyCustomTransformations(finalValue, targetType, transformKey);
168+
finalValue = this.applyCustomTransformations(finalValue, targetType, transformKey, value, this.transformationType);
166169
if (newValue instanceof Map) {
167170
newValue.set(newValueKey, finalValue);
168171
} else {
169172
newValue[newValueKey] = finalValue;
170173
}
171-
} else if (this.transformationType === "classToClass") {
174+
} else if (this.transformationType === TransformationType.CLASS_TO_CLASS) {
172175
let finalValue = subValue;
173-
finalValue = this.applyCustomTransformations(finalValue, targetType, key);
176+
finalValue = this.applyCustomTransformations(finalValue, targetType, key, value, this.transformationType);
174177
if (newValue instanceof Map) {
175178
newValue.set(newValueKey, finalValue);
176179
} else {
@@ -186,7 +189,7 @@ export class TransformOperationExecutor {
186189
}
187190
}
188191

189-
private applyCustomTransformations(value: any, target: Function, key: string) {
192+
private applyCustomTransformations(value: any, target: Function, key: string, obj: any, transformationType: TransformationType) {
190193
let metadatas = defaultMetadataStorage.findTransformMetadatas(target, key, this.transformationType);
191194

192195
// apply versioning options
@@ -209,14 +212,12 @@ export class TransformOperationExecutor {
209212
});
210213
} else {
211214
metadatas = metadatas.filter(metadata => {
212-
return !metadata.options ||
213-
!metadata.options.groups ||
214-
!metadata.options.groups.length;
215+
return !metadata.options || !metadata.options.groups || !metadata.options.groups.length;
215216
});
216217
}
217218

218219
metadatas.forEach(metadata => {
219-
value = metadata.transformFn(value);
220+
value = metadata.transformFn(value, obj, transformationType);
220221
});
221222

222223
return value;
@@ -255,7 +256,7 @@ export class TransformOperationExecutor {
255256

256257
// add all exposed to list of keys
257258
let exposedProperties = defaultMetadataStorage.getExposedProperties(target, this.transformationType);
258-
if (this.transformationType === "plainToClass") {
259+
if (this.transformationType === TransformationType.PLAIN_TO_CLASS) {
259260
exposedProperties = exposedProperties.map(key => {
260261
const exposeMetadata = defaultMetadataStorage.findExposeMetadata(target, key);
261262
if (exposeMetadata && exposeMetadata.options && exposeMetadata.options.name) {
@@ -298,10 +299,7 @@ export class TransformOperationExecutor {
298299
} else {
299300
keys = keys.filter(key => {
300301
const exposeMetadata = defaultMetadataStorage.findExposeMetadata(target, key);
301-
return !exposeMetadata ||
302-
!exposeMetadata.options ||
303-
!exposeMetadata.options.groups ||
304-
!exposeMetadata.options.groups.length;
302+
return !exposeMetadata || !exposeMetadata.options || !exposeMetadata.options.groups || !exposeMetadata.options.groups.length;
305303
});
306304
}
307305
}
@@ -338,4 +336,4 @@ export class TransformOperationExecutor {
338336
return this.options.groups.some(optionGroup => groups.indexOf(optionGroup) !== -1);
339337
}
340338

341-
}
339+
}

src/decorators.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@ import {ExposeOptions, ExcludeOptions, TypeOptions, TransformOptions} from "./me
66
import {ExcludeMetadata} from "./metadata/ExcludeMetadata";
77
import {TransformMetadata} from "./metadata/TransformMetadata";
88
import {ClassTransformOptions} from "./ClassTransformOptions";
9+
import {TransformationType} from "./TransformOperationExecutor";
910

1011
/**
1112
* Defines a custom logic for value transformation.
1213
*/
13-
export function Transform(transformFn: (value: any) => any, options?: TransformOptions) {
14+
export function Transform(transformFn: (value: any, obj: any, transformationType: TransformationType) => any, options?: TransformOptions) {
1415
return function(target: any, key: string) {
1516
const metadata = new TransformMetadata(target.constructor, key, transformFn, options);
1617
defaultMetadataStorage.addTransformMetadata(metadata);
@@ -60,10 +61,10 @@ export function TransformClassToPlain(params?: ClassTransformOptions): Function
6061
return function (target: Function, propertyKey: string, descriptor: PropertyDescriptor) {
6162
const classTransformer: ClassTransformer = new ClassTransformer();
6263
const originalMethod = descriptor.value;
63-
64+
6465
descriptor.value = function(...args: any[]) {
6566
const result: any = originalMethod.apply(this, args);
66-
const isPromise = !!result && (typeof result === "object" || typeof result === "function") && typeof result.then === "function";
67+
const isPromise = !!result && (typeof result === "object" || typeof result === "function") && typeof result.then === "function";
6768

6869
return isPromise ? result.then((data: any) => classTransformer.classToPlain(data, params)) : classTransformer.classToPlain(result, params);
6970
};
@@ -78,12 +79,12 @@ export function TransformClassToClass(params?: ClassTransformOptions): Function
7879
return function (target: Function, propertyKey: string, descriptor: PropertyDescriptor) {
7980
const classTransformer: ClassTransformer = new ClassTransformer();
8081
const originalMethod = descriptor.value;
81-
82+
8283
descriptor.value = function(...args: any[]) {
8384
const result: any = originalMethod.apply(this, args);
8485
const isPromise = !!result && (typeof result === "object" || typeof result === "function") && typeof result.then === "function";
8586

8687
return isPromise ? result.then((data: any) => classTransformer.classToClass(data, params)) : classTransformer.classToClass(result, params);
8788
};
8889
};
89-
}
90+
}

0 commit comments

Comments
 (0)