Skip to content

Commit ed9d51d

Browse files
author
Daan van der Kallen
committed
Add graph parsing
1 parent 4fe84a5 commit ed9d51d

File tree

4 files changed

+154
-7
lines changed

4 files changed

+154
-7
lines changed

src/Model.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ export default class Model {
192192
}
193193
});
194194
if (options.relations) {
195-
this.__parseRelations(options.relations);
195+
this.__parseRelations(options.relations, options.relsFromCache);
196196
}
197197
if (data) {
198198
this.parse(data);
@@ -203,7 +203,7 @@ export default class Model {
203203
}
204204

205205
@action
206-
__parseRelations(activeRelations) {
206+
__parseRelations(activeRelations, relsFromCache = {}) {
207207
this.__activeRelations = activeRelations;
208208
// TODO: No idea why getting the relations only works when it's a Function.
209209
const relations = this.relations && this.relations();
@@ -242,10 +242,20 @@ export default class Model {
242242
RelModel,
243243
`Specified relation "${relName}" does not exist on model.`
244244
);
245+
246+
const cacheData = relsFromCache[relName];
247+
if (cacheData && cacheData.model) {
248+
return cacheData.model;
249+
}
250+
245251
const options = {
246252
relations: otherRelNames,
247253
linkRelations: this.__linkRelations,
248254
};
255+
if (cacheData && cacheData.rels) {
256+
options.relsFromCache = cacheData.rels;
257+
}
258+
249259
if (RelModel.prototype instanceof Store) {
250260
return new RelModel(options);
251261
}

src/Store.js

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import {
1111
result,
1212
uniqBy,
1313
} from 'lodash';
14-
import { invariant } from './utils';
14+
import { invariant, relationsToNestedKeys, camelToSnake } from './utils';
15+
import Model from './Model';
1516
const AVAILABLE_CONST_OPTIONS = [
1617
'relations',
1718
'limit',
@@ -21,6 +22,45 @@ const AVAILABLE_CONST_OPTIONS = [
2122
'linkRelations',
2223
];
2324

25+
function getRelsFromCache(record, rels, repos, relMapping, relPath, relCache) {
26+
const relsFromCache = {};
27+
28+
for (const [rel, subRels] of Object.entries(rels)) {
29+
// First we try to get the related id
30+
const backendRel = camelToSnake(rel);
31+
const relId = record[backendRel];
32+
// We only care about numbers since that means that it
33+
// is filled and is a direct relation.
34+
if (typeof relId !== 'number') {
35+
continue;
36+
}
37+
38+
// Then we see if we can find the model in the cache.
39+
const subRelPath = `${relPath}.${rel}`;
40+
const cachedModel = relCache[`${subRelPath}:${relId}`];
41+
if (cachedModel) {
42+
relsFromCache[rel] = { model: cachedModel };
43+
// If we found it we don't have to dive deeper.
44+
continue;
45+
}
46+
47+
// Then we search for the related data in the repos
48+
const subRecord = repos[relMapping[subRelPath.slice('root.'.length)]].find(({ id }) => id === relId);
49+
if (!subRecord) {
50+
continue;
51+
}
52+
53+
// Now we recurse to see if we can find relations in the cache for this model
54+
relsFromCache[rel] = { rels: getRelsFromCache(
55+
subRecord, subRels,
56+
repos, relMapping,
57+
subRelPath, relCache,
58+
) };
59+
}
60+
61+
return relsFromCache;
62+
}
63+
2464
export default class Store {
2565
// Holds all models
2666
@observable models = [];
@@ -128,19 +168,26 @@ export default class Store {
128168

129169
this.models.replace(
130170
data.map(record => {
131-
const relCacheKey = `${relPath}:${record.id}`
171+
const options = {};
132172

133173
if (this.__linkRelations === 'graph') {
134-
const model = relCache[relCacheKey];
174+
// Return from cache if available
175+
const model = relCache[`${relPath}:${record.id}`];
135176
if (model !== undefined) {
136177
return model;
137178
}
179+
180+
options.relsFromCache = getRelsFromCache(
181+
record, relationsToNestedKeys(this.__activeRelations),
182+
repos, relMapping,
183+
relPath, relCache,
184+
);
138185
}
139186

140187
// TODO: I'm not happy at all about how this looks.
141188
// We'll need to finetune some things, but hey, for now it works.
142189

143-
const model = this._newModel(null);
190+
const model = this._newModel(null, options);
144191
model.fromBackend({
145192
data: record,
146193
repos,
@@ -151,7 +198,18 @@ export default class Store {
151198
});
152199

153200
if (this.__linkRelations === 'graph') {
154-
relCache[relCacheKey] = model;
201+
const toAdd = [[relPath, model]];
202+
while (toAdd.length > 0) {
203+
const [relPath, model] = toAdd.pop();
204+
relCache[`${relPath}:${model.id}`] = model;
205+
206+
for (const rel of model.__activeCurrentRelations) {
207+
const relModel = model[rel];
208+
if (relModel instanceof Model && !relModel.isNew) {
209+
toAdd.push([`${relPath}.${rel}`, relModel]);
210+
}
211+
}
212+
}
155213
}
156214

157215
return model;

src/__tests__/Store.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@ import customersWithTownRestaurantsUnbalanced from './fixtures/customers-with-to
2424
import townsWithRestaurantsAndCustomersNoIdList from './fixtures/towns-with-restaurants-and-customers-no-id-list.json';
2525
import customersWithOldTowns from './fixtures/customers-with-old-towns.json';
2626
import animalsData from './fixtures/animals.json';
27+
import animalsGraphData from './fixtures/animals-graph.json';
2728
import pagination0Data from './fixtures/pagination/0.json';
2829
import pagination1Data from './fixtures/pagination/1.json';
2930
import pagination2Data from './fixtures/pagination/2.json';
3031
import pagination3Data from './fixtures/pagination/3.json';
3132
import pagination4Data from './fixtures/pagination/4.json';
3233

34+
3335
const simpleData = [
3436
{
3537
id: 2,
@@ -1010,3 +1012,34 @@ describe('Pagination', () => {
10101012
});
10111013
});
10121014
});
1015+
1016+
describe('Graph parse', () => {
1017+
let mock;
1018+
beforeEach(() => {
1019+
mock = new MockAdapter(axios);
1020+
});
1021+
afterEach(() => {
1022+
if (mock) {
1023+
mock.restore();
1024+
mock = null;
1025+
}
1026+
});
1027+
1028+
test('animals with owner & town', () => {
1029+
mock.onAny().replyOnce(() => [200, animalsGraphData]);
1030+
1031+
const animalStore = new AnimalStore({
1032+
linkRelations: 'graph',
1033+
relations: ['owner.town'],
1034+
});
1035+
1036+
return animalStore.fetch().then(() => {
1037+
expect(animalStore.map('id')).toEqual([1, 2, 3]);
1038+
1039+
expect(animalStore.at(0).owner).toBe(animalStore.at(1).owner);
1040+
expect(animalStore.at(0).owner).not.toBe(animalStore.at(2).owner);
1041+
1042+
expect(animalStore.at(0).owner.town).toBe(animalStore.at(2).owner.town);
1043+
});
1044+
});
1045+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
{
2+
"data": [
3+
{
4+
"id": 1,
5+
"name": "Foo",
6+
"owner": 1
7+
},
8+
{
9+
"id": 2,
10+
"name": "Bar",
11+
"owner": 1
12+
},
13+
{
14+
"id": 3,
15+
"name": "Baz",
16+
"owner": 2
17+
}
18+
],
19+
"with": {
20+
"person": [
21+
{
22+
"id": 1,
23+
"name": "FOO",
24+
"town": 1
25+
},
26+
{
27+
"id": 2,
28+
"name": "BAR",
29+
"town": 1
30+
}
31+
],
32+
"town": [
33+
{
34+
"id": 1,
35+
"name": "Eindhoven"
36+
}
37+
]
38+
},
39+
"with_mapping": {
40+
"owner": "person",
41+
"owner.town": "town"
42+
},
43+
"meta": {
44+
"total_records": 3
45+
}
46+
}

0 commit comments

Comments
 (0)