Skip to content

Commit 9e8754b

Browse files
authored
Merge pull request #62 from sheiidan/skip-circular-check
Add skipCircularCheck option
2 parents 52cd394 + 5c05897 commit 9e8754b

File tree

4 files changed

+60
-9
lines changed

4 files changed

+60
-9
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@types/es6-shim": "^0.31.32",
3131
"@types/mocha": "^2.2.33",
3232
"@types/node": "0.0.2",
33+
"@types/sinon": "^2.2.2",
3334
"chai": "^3.4.1",
3435
"chai-as-promised": "^6.0.0",
3536
"del": "^2.2.1",

src/ClassTransformOptions.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,10 @@ export interface ClassTransformOptions {
5656
*/
5757
targetMaps?: TargetMap[];
5858

59+
60+
/**
61+
* If set to true then class transformer will perform a circular check. (circular check is turned off by default)
62+
* This option is useful when you know for sure that your types might have a circular dependency.
63+
*/
64+
enableCircularCheck?: boolean;
5965
}

src/TransformOperationExecutor.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export class TransformOperationExecutor {
1111
// Private Properties
1212
// -------------------------------------------------------------------------
1313

14-
private transformedTypes: { level: number, object: Object }[] = [];
14+
private transformedTypesMap = new Map<Object, { level: number, object: Object }>();
1515

1616
// -------------------------------------------------------------------------
1717
// Constructor
@@ -36,7 +36,7 @@ export class TransformOperationExecutor {
3636
const newValue = arrayType && this.transformationType === "plainToClass" ? new (arrayType as any)() : [];
3737
(value as any[]).forEach((subValue, index) => {
3838
const subSource = source ? source[index] : undefined;
39-
if (!this.isCircular(subValue, level)) {
39+
if (!this.options.enableCircularCheck || !this.isCircular(subValue, level)) {
4040
const value = this.transform(subSource, subValue, targetType, undefined, subValue instanceof Map, level + 1);
4141
if (newValue instanceof Set) {
4242
newValue.add(value);
@@ -77,8 +77,10 @@ export class TransformOperationExecutor {
7777
if (!targetType && value.constructor !== Object/* && operationType === "classToPlain"*/) targetType = value.constructor;
7878
if (!targetType && source) targetType = source.constructor;
7979

80-
// add transformed type to prevent circular references
81-
this.transformedTypes.push({ level: level, object: value });
80+
if (this.options.enableCircularCheck) {
81+
// add transformed type to prevent circular references
82+
this.transformedTypesMap.set(value, { level: level, object: value });
83+
}
8284

8385
const keys = this.getKeys(targetType, value);
8486
let newValue: any = source ? source : {};
@@ -157,7 +159,7 @@ export class TransformOperationExecutor {
157159
continue;
158160
}
159161

160-
if (!this.isCircular(subValue, level)) {
162+
if (!this.options.enableCircularCheck || !this.isCircular(subValue, level)) {
161163
let transformKey = this.transformationType === "plainToClass" ? newValueKey : key;
162164
let finalValue = this.transform(subSource, subValue, type, arrayType, isSubValueMap, level + 1);
163165
finalValue = this.applyCustomTransformations(finalValue, targetType, transformKey);
@@ -222,7 +224,8 @@ export class TransformOperationExecutor {
222224

223225
// preventing circular references
224226
private isCircular(object: Object, level: number) {
225-
return !!this.transformedTypes.find(transformed => transformed.object === object && transformed.level < level);
227+
const transformed = this.transformedTypesMap.get(object);
228+
return transformed !== undefined && transformed.level < level;
226229
}
227230

228231
private getReflectedType(target: Function, propertyName: string) {

test/functional/circular-reference-problem.spec.ts

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import "reflect-metadata";
2-
import {classToPlain, classToClass} from "../../src/index";
2+
import {classToPlain, classToClass, plainToClass} from "../../src/index";
33
import {defaultMetadataStorage} from "../../src/storage";
4+
import {TransformOperationExecutor} from "../../src/TransformOperationExecutor";
5+
import {assert} from "chai";
6+
import * as sinon from "sinon";
47

58
describe("circular reference problem", () => {
69

@@ -37,7 +40,7 @@ describe("circular reference problem", () => {
3740
photo1.users = [user];
3841
photo2.users = [user];
3942

40-
const plainUser = classToPlain(user);
43+
const plainUser = classToPlain(user, { enableCircularCheck: true });
4144
plainUser.should.be.eql({
4245
firstName: "Umed Khudoiberdiev",
4346
photos: [{
@@ -86,11 +89,49 @@ describe("circular reference problem", () => {
8689
photo1.users = [user];
8790
photo2.users = [user];
8891

89-
const classUser = classToClass(user);
92+
const classUser = classToClass(user, { enableCircularCheck: true });
9093
classUser.should.not.be.equal(user);
9194
classUser.should.be.instanceOf(User);
9295
classUser.should.be.eql(user);
9396

9497
});
9598

99+
describe("enableCircularCheck option", () => {
100+
class Photo {
101+
id: number;
102+
filename: string;
103+
}
104+
105+
class User {
106+
id: number;
107+
firstName: string;
108+
photos: Photo[];
109+
}
110+
let isCircularSpy: sinon.SinonSpy;
111+
const photo1 = new Photo();
112+
photo1.id = 1;
113+
photo1.filename = "me.jpg";
114+
115+
const user = new User();
116+
user.firstName = "Umed Khudoiberdiev";
117+
user.photos = [photo1];
118+
119+
beforeEach(() => {
120+
isCircularSpy = sinon.spy(TransformOperationExecutor.prototype, "isCircular");
121+
});
122+
123+
afterEach(() => {
124+
isCircularSpy.restore();
125+
});
126+
127+
it("enableCircularCheck option is undefined (default)", () => {
128+
const result = plainToClass<User, Object>(User, user);
129+
sinon.assert.notCalled(isCircularSpy);
130+
});
131+
132+
it("enableCircularCheck option is true", () => {
133+
const result = plainToClass<User, Object>(User, user, { enableCircularCheck: true });
134+
sinon.assert.called(isCircularSpy);
135+
});
136+
});
96137
});

0 commit comments

Comments
 (0)