Skip to content

Commit 9a7670a

Browse files
Merge pull request #1 from xanthous-tech/fix-circular
Fix Circular Issue
2 parents b980c1f + c3f44ad commit 9a7670a

7 files changed

Lines changed: 121 additions & 87 deletions

File tree

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ module.exports = {
1212
rules: {
1313
'no-inner-declarations': 'off',
1414
'prefer-spread': 'off',
15+
'@typescript-eslint/camelcase': 'warn',
1516
'@typescript-eslint/no-namespace': 'off',
1617
'@typescript-eslint/no-use-before-define': 'off',
1718
'@typescript-eslint/no-non-null-assertion': 'off',

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@xanthous/dgraph-orm",
3-
"version": "0.2.0-alpha",
3+
"version": "0.2.0-alpha3",
44
"description": "dgraph ORM in TypeScript",
55
"main": "dist/index.js",
66
"author": "Simon Liang <simon@x-tech.io>",
@@ -20,7 +20,8 @@
2020
"lint": "eslint ./src --ext .ts",
2121
"lint:fix": "eslint --fix",
2222
"format": "prettier --write",
23-
"test": "jest"
23+
"test": "jest",
24+
"test:debug": "node --inspect-brk ./node_modules/.bin/jest"
2425
},
2526
"repository": {
2627
"type": "git",

src/transaction/transaction-builder.ts

Lines changed: 75 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -25,49 +25,83 @@ export namespace TransactionBuilder {
2525

2626
class TreeMapperBuilder<T = any> {
2727
private readonly _entryType: Constructor<T>;
28-
private _jsonData: IObjectLiteral<any>[];
28+
private _root: IObjectLiteral<any>[];
2929
private _resource = new Map<string, IObjectLiteral<any>>();
3030

3131
constructor(type: Constructor<T>) {
3232
this._entryType = type;
3333
}
3434

35-
addJsonData(data: IObjectLiteral<any> | IObjectLiteral<any>[]): TreeMapperBuilder<T> {
36-
this._jsonData = Array.isArray(data) ? data : [data];
35+
setRoot(data: IObjectLiteral<any> | IObjectLiteral<any>[]): TreeMapperBuilder<T> {
36+
if (!Array.isArray(data) && Private.isNode(data)) {
37+
this._root = [data];
38+
} else {
39+
if (Private.isAllNodes(data as IObjectLiteral<any>[])) {
40+
this._root = data as IObjectLiteral<any>[];
41+
}
42+
}
43+
44+
this.addJsonData(data);
3745
return this;
3846
}
3947

4048
/**
4149
* Walk the resource graph and add all nodes into resource cache by its `uid`.
4250
*/
43-
addResourceData(data: IObjectLiteral<any> | IObjectLiteral<any>[]): TreeMapperBuilder<T> {
51+
addJsonData(data: IObjectLiteral<any> | IObjectLiteral<any>[]): TreeMapperBuilder<T> {
4452
if (data && !(data instanceof Array) && data.uid) {
45-
this._resource.set(data.uid, data);
53+
const { obj, predicates } = Private.separatePredicates(data);
54+
55+
if (this._resource.has(obj.uid)) {
56+
const existingObj = this._resource.get(obj.uid) as IObjectLiteral<any>;
57+
// trying to not modify existing object ref
58+
Object.assign(existingObj, obj);
59+
} else {
60+
this._resource.set(obj.uid, obj);
61+
}
62+
63+
for (const key of predicates.keys()) {
64+
const predicate = predicates.get(key) as IObjectLiteral<any>[];
65+
// store nodes from the predicate
66+
this.addJsonData(predicate);
67+
const existingObj = this._resource.get(obj.uid) as IObjectLiteral<any>;
68+
const pred = existingObj[key] as IObjectLiteral<any>[];
69+
70+
predicate.forEach(p => {
71+
const pObj = this._resource.get(p.uid) as IObjectLiteral<any>;
72+
73+
if (pred.indexOf(pObj) > -1) {
74+
return;
75+
}
76+
77+
pred.push(pObj);
78+
});
79+
}
80+
4681
return this;
4782
}
4883

4984
data.forEach((d: any) => {
50-
this.addResourceData(d);
85+
this.addJsonData(d);
5186
});
5287

5388
return this;
5489
}
5590

5691
build(): ITransaction<T> {
57-
// Do not traverse the json tree if there is no
58-
// resource data.
59-
if (this._resource.size > 0) {
60-
const visited = new Set<string>();
61-
Array.isArray(this._jsonData)
62-
? this._jsonData.map(jd => Private.expand(visited, this._resource, jd))
63-
: Private.expand(visited, this._resource, this._jsonData);
64-
}
65-
66-
if (!Array.isArray(this._jsonData)) {
67-
this._jsonData = [this._jsonData];
68-
}
69-
70-
return new Transaction(this._entryType, this._jsonData);
92+
// eslint-disable-next-line @typescript-eslint/no-var-requires
93+
// const util = require('util');
94+
// console.log(
95+
// util.inspect(
96+
// this._root.map(o => this._resource.get(o.uid)),
97+
// { colors: true, depth: 20 }
98+
// )
99+
// );
100+
101+
return new Transaction(
102+
this._entryType,
103+
this._root.map(o => this._resource.get(o.uid))
104+
);
71105
}
72106
}
73107
}
@@ -76,35 +110,31 @@ export namespace TransactionBuilder {
76110
* Module private statics
77111
*/
78112
namespace Private {
79-
/**
80-
* Visit all nodes in a tree recursively, matching node uid in the resource data and adding extra information.
81-
*
82-
* ### NOTE
83-
* Expand will modify the data in-place.
84-
*/
85-
export function expand(visited: Set<string>, resource: IObjectLiteral<any>, source: IObjectLiteral<any>): void {
86-
if (resource.has(source.uid)) {
87-
Object.assign(source, resource.get(source.uid));
88-
}
113+
export function separatePredicates(
114+
obj: IObjectLiteral<any>
115+
): { obj: IObjectLiteral<any>; predicates: Map<string, IObjectLiteral<any>[]> } {
116+
const predicates: Map<string, IObjectLiteral<any>[]> = new Map();
89117

90-
Object.keys(source).forEach(key => {
91-
if (key === 'dgraph.type') {
92-
return;
93-
}
118+
Object.keys(obj).forEach((key: string) => {
119+
if (isPredicate(obj, key)) {
120+
predicates.set(key, obj[key]);
94121

95-
if (!Array.isArray(source[key])) {
96-
return;
122+
obj[key] = [];
97123
}
124+
});
98125

99-
source[key].forEach((node: any) => {
100-
const visitingKey = `${source.uid}:${key}:${node.uid}`;
101-
if (visited.has(visitingKey)) {
102-
return;
103-
}
126+
return { obj, predicates };
127+
}
104128

105-
visited.add(visitingKey);
106-
return expand(visited, resource, node);
107-
});
108-
});
129+
export function isPredicate(obj: IObjectLiteral<any>, key: string): boolean {
130+
return isAllNodes(obj[key]);
131+
}
132+
133+
export function isAllNodes(obj: IObjectLiteral<any>[]): boolean {
134+
return Array.isArray(obj) && (obj as Array<any>).every(isNode);
135+
}
136+
137+
export function isNode(obj: IObjectLiteral<any>): boolean {
138+
return !!obj.uid;
109139
}
110140
}

src/transaction/transaction.ts

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -133,38 +133,36 @@ export class Transaction<T extends Object, V> implements ITransaction<T> {
133133
/**
134134
* Given a data class definition and plain object return an instance of the data class.
135135
*/
136-
private plainToClassExecutor<T extends Object, V>(
137-
cls: Constructor<T>,
138-
plain: V[],
139-
storage: WeakMap<Object, T[]>
140-
): T[] {
141-
// Bail early if already converted.
142-
if (storage.has(plain)) {
143-
return storage.get(plain)!;
144-
}
136+
private plainToClassExecutor<T extends Object, V>(cls: Constructor<T>, plain: V[], storage: WeakMap<Object, T>): T[] {
137+
return plain.reduce((acc: any[], pln: V) => {
138+
// Bail early if already converted.
139+
if (storage.has(pln)) {
140+
acc.push(storage.get(pln)!);
141+
return acc;
142+
}
145143

146-
// Build the entry class
147-
const instances: T[] = plainToClass(cls, plain, {
148-
enableCircularCheck: true,
149-
strategy: 'exposeAll'
150-
});
144+
// Build the entry class
145+
const ins: T = plainToClass(cls, pln, {
146+
enableCircularCheck: true,
147+
strategy: 'exposeAll'
148+
});
151149

152-
// Keep reference to the instance so in case of circular we can simply get it from storage and complete the circle.
153-
storage.set(plain, instances);
150+
// Keep reference to the instance so in case of circular we can simply get it from storage and complete the circle.
151+
storage.set(pln, ins);
152+
acc.push(ins);
154153

155-
instances.forEach((ins, idx) => {
156154
this.trackProperties(ins);
157155

158156
const predicates = MetadataStorage.Instance.predicates.get(ins.constructor.name);
159157
if (!predicates) {
160-
return;
158+
return acc;
161159
}
162160

163161
// FIXME: If the same uid is referenced in multiple places in the data, currently we will have 2 different instances
164162
// of the same object. We need to make sure we share the instance.
165163
predicates.forEach(pred => {
166164
this.trackPredicate(ins, pred);
167-
const _preds = (plain[idx] as any)[pred.args.name];
165+
const _preds = (pln as any)[pred.args.name];
168166

169167
// If no data available assign a new data.
170168
if (!_preds) {
@@ -175,9 +173,9 @@ export class Transaction<T extends Object, V> implements ITransaction<T> {
175173
(ins as any)[pred.args.propertyName] = this.plainToClassExecutor(pred.args.type(), _preds, storage);
176174
}
177175
});
178-
});
179176

180-
return instances;
177+
return acc;
178+
}, []) as T[];
181179
}
182180

183181
/**

tests/delete.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ describe('Delete handling', function() {
3636

3737
const transaction = TransactionBuilder.of(Person)
3838
.addJsonData(data)
39+
.setRoot({ uid: '0x1' })
3940
.build();
4041

4142
const jane = transaction.tree[0].friends.get()[0];

tests/mutation.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ describe('Mutation handling', () => {
3939
];
4040

4141
const transaction = TransactionBuilder.of(Person)
42-
.addJsonData(data)
42+
.setRoot(data)
4343
.build();
4444

4545
transaction.tree[0].hobbies.get()[0].name = 'New Hobby Name';
@@ -137,7 +137,7 @@ describe('Mutation handling', () => {
137137
];
138138

139139
const transaction = TransactionBuilder.of(Person)
140-
.addJsonData(data)
140+
.setRoot(data)
141141
.build();
142142

143143
transaction.tree[0].name = 'New John';

tests/serde.spec.ts

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describe('Serialize deserialize', () => {
2020
];
2121

2222
const txn = TransactionBuilder.of(Work)
23-
.addJsonData(data)
23+
.setRoot(data)
2424
.build();
2525

2626
const work = txn.tree[0];
@@ -79,7 +79,7 @@ describe('Serialize deserialize', () => {
7979
];
8080

8181
const txn = TransactionBuilder.of(Work)
82-
.addJsonData(data)
82+
.setRoot(data)
8383
.build();
8484

8585
console.log(Utils.toObject(txn.tree[0]));
@@ -123,7 +123,7 @@ describe('Serialize deserialize', () => {
123123
];
124124

125125
const txn = TransactionBuilder.of(Person)
126-
.addJsonData(data)
126+
.setRoot(data)
127127
.build();
128128

129129
expect(txn.tree[0].name).toEqual(data[0]['Person.name']);
@@ -170,7 +170,7 @@ describe('Serialize deserialize', () => {
170170
];
171171

172172
const txn = TransactionBuilder.of(Person)
173-
.addJsonData(data)
173+
.setRoot(data)
174174
.build();
175175

176176
expect(txn.tree[0].name).toEqual(data[0]['Person.name']);
@@ -191,24 +191,20 @@ describe('Serialize deserialize', () => {
191191
const data = [
192192
{
193193
uid: '0x1',
194-
'Parent.name': 'Parent',
195194
'Parent.has_child': [
196195
{
197196
uid: '0x2',
198197
'Parent.has_child': [
199198
{
200199
uid: '0x3',
201-
'Parent.has_child': null
200+
'Parent.has_child': [{ uid: '0x1' }]
202201
}
203202
]
204203
}
205204
] as any[]
206205
}
207206
];
208207

209-
// Circularly reference
210-
data[0]['Parent.has_child'][0]['Parent.has_child'][0]['Parent.has_child'] = data as any;
211-
212208
const resource = [
213209
{
214210
uid: '0x1',
@@ -225,27 +221,34 @@ describe('Serialize deserialize', () => {
225221
];
226222

227223
const txn = TransactionBuilder.of(Parent)
228-
.addJsonData(data)
229-
.addResourceData(resource)
224+
.setRoot(data)
225+
.addJsonData(resource)
230226
.build();
231227

232228
Utils.printObject(txn.tree[0]);
229+
Utils.printObject(
230+
txn.tree[0]
231+
.has_child.get()[0]
232+
.has_child.get()[0]
233+
.has_child.get()[0]
234+
);
233235

234236
expect(txn.tree[0].has_child.get()[0].name).toEqual('Node 0x2');
235237

236238
expect(
237-
txn.tree[0].has_child
238-
.get()[0]
239+
txn.tree[0]
240+
.has_child.get()[0]
241+
.has_child.get()[0]
239242
.has_child.get()[0]
240-
.has_child.get()[0].name
243+
.name
241244
).toEqual('Node 0x1');
242245

243246
expect(
244-
txn.tree[0] ===
245-
txn.tree[0].has_child
246-
.get()[0]
247+
txn.tree[0].name ===
248+
txn.tree[0]
247249
.has_child.get()[0]
248250
.has_child.get()[0]
251+
.has_child.get()[0].name
249252
).toBeTruthy();
250253
});
251254
});

0 commit comments

Comments
 (0)