Skip to content

Commit 4c26a29

Browse files
authored
Merge pull request #77 from embermap/assert-must-preload-and-load-all
Assert must preload and loadRecords
2 parents 4a9789c + 370d8e1 commit 4c26a29

9 files changed

Lines changed: 246 additions & 17 deletions

File tree

addon/-private/record-array-query.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,17 @@ export default class RecordArrayQuery {
2626
return promise;
2727
}
2828

29+
trackIncludes() {
30+
let includes = this.params && this.params.include;
31+
let models = this.value;
32+
33+
if (includes && models) {
34+
models
35+
.filter(model => model.trackLoadedIncludes)
36+
.forEach((model) => {
37+
model.trackLoadedIncludes(includes);
38+
});
39+
}
40+
}
41+
2942
}

addon/mixins/loadable-model.js

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,112 @@ export default Mixin.create({
225225
},
226226

227227
/**
228+
A list of models for a given relationship. It's always normalized to a list,
229+
even for belongsTo, null, or unloaded relationships.
230+
231+
@method _getRelationshipModels
232+
@private
233+
*/
234+
_getRelationshipModels(name) {
235+
let reference = this._getReference(name);
236+
let info = this._getRelationshipInfo(name);
237+
let models;
238+
239+
if (info.kind === 'hasMany') {
240+
models = reference.value() || [];
241+
} else if (info.kind === 'belongsTo') {
242+
models = reference.value() ? [ reference.value() ] : [];
243+
}
244+
245+
return models;
246+
},
247+
248+
/**
249+
This is a private method because we may refactor it in the future to have
250+
a difference signature. However, this method is used by other
251+
storefront objects. So, it's really public, but don't use it in app code!
252+
253+
@method trackLoadedIncludes
254+
@param {String} includes A full include path. Example: "author,comments.author,tags"
255+
@private
256+
*/
257+
trackLoadedIncludes(includes) {
258+
includes.split(",").forEach(path => this._trackLoadedIncludePath(path));
259+
},
260+
261+
/**
262+
Tracks a single include path as being loaded.
263+
264+
@method _trackLoadedIncludePath
265+
@param {String} path A single include path. Example: "comments.author"
266+
@private
267+
*/
268+
_trackLoadedIncludePath(path) {
269+
let [firstInclude, ...rest] = path.split(".");
270+
let relationship = camelize(firstInclude);
271+
let reference = this._getReference(relationship);
272+
273+
if (reference) {
274+
this._loadedReferences[relationship] = true;
275+
276+
if (rest.length) {
277+
this._getRelationshipModels(relationship)
278+
.filter(model => model.trackLoadedIncludes)
279+
.forEach(model => model.trackLoadedIncludes(rest.join('.')));
280+
}
281+
}
282+
},
283+
284+
/**
285+
This method can take an include string and see if the graph of objects
286+
in the store related to this model have all loaded each of the elements
287+
in that include string.
288+
289+
@method _graphHasLoaded
290+
@param {String} includes A full include path. Example: "author,comments.author,tags"
291+
@return {Boolean} True if the includes have been loaded, false if not
292+
@private
293+
*/
294+
_graphHasLoaded(includes) {
295+
return includes
296+
.split(",")
297+
.every(path => this._graphHasLoadedPath(path));
298+
},
299+
300+
/**
301+
Checks wether a single include path has been loaded.
302+
303+
@method _graphHasLoadedPath
304+
@param {String} path A single include path. Example: "comments.author"
305+
@return {Boolean} True if the path has been loaded, false if not
306+
@private
307+
*/
308+
_graphHasLoadedPath(includePath) {
309+
let [firstInclude, ...rest] = includePath.split(".");
310+
let relationship = camelize(firstInclude);
311+
let reference = this._getReference(relationship);
312+
let hasLoaded = reference && this._hasLoadedReference(relationship);
313+
314+
if (rest.length === 0) {
315+
return hasLoaded;
316+
317+
} else {
318+
let models = this._getRelationshipModels(relationship);
319+
320+
let childrenHaveLoaded = models.every(model => {
321+
return model.trackLoadedIncludes && model._graphHasLoaded(rest.join("."));
322+
});
323+
324+
return hasLoaded && childrenHaveLoaded;
325+
}
326+
},
327+
328+
/**
329+
Checks if storefront has ever loaded this reference.
330+
228331
@method _hasLoadedReference
332+
@param {String} name Reference or relationshipname name.
333+
@return {Boolean} True if storefront has loaded the reference.
229334
@private
230335
*/
231336
_hasLoadedReference(name) {
@@ -242,10 +347,8 @@ export default Mixin.create({
242347
*/
243348
hasLoaded(includesString) {
244349
let modelName = this.constructor.modelName;
245-
let hasSideloaded = this.get('store').hasLoadedIncludesForRecord(modelName, this.get('id'), includesString);
246-
let hasLoaded = this._hasLoadedReference(camelize(includesString));
247-
248-
return hasLoaded || hasSideloaded;
350+
return this.get('store').hasLoadedIncludesForRecord(modelName, this.get('id'), includesString) ||
351+
this._graphHasLoaded(includesString);
249352
}
250353

251354
});

addon/mixins/loadable-store.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,18 +58,20 @@ export default Mixin.create({
5858
let shouldBlock = options.reload || !query.value;
5959
let shouldBackgroundReload = !options.hasOwnProperty('backgroundReload') || options.backgroundReload;
6060
let promise;
61+
let fetcher;
6162

6263
if (shouldBlock) {
6364
promise = query.run();
65+
fetcher = promise;
6466

6567
} else {
6668
promise = resolve(query.value);
6769

68-
if (shouldBackgroundReload) {
69-
query.run();
70-
}
70+
fetcher = shouldBackgroundReload ? query.run() : resolve();
7171
}
7272

73+
fetcher.then(() => query.trackIncludes());
74+
7375
return promise;
7476
},
7577

tests/dummy/app/models/author.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import DS from 'ember-data';
22

33
export default DS.Model.extend({
4-
5-
comments: DS.hasMany()
4+
5+
name: DS.attr('string'),
6+
7+
comments: DS.hasMany(),
8+
post: DS.belongsTo()
69

710
});

tests/dummy/app/models/comment.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import DS from 'ember-data';
22

33
export default DS.Model.extend({
44

5+
text: DS.attr('string'),
6+
57
post: DS.belongsTo(),
68
author: DS.belongsTo()
79

tests/index.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
{{content-for "head-footer"}}
1818
{{content-for "test-head-footer"}}
1919
</head>
20-
<body>
20+
<body style="background-color: white !important;">
2121
{{content-for "body"}}
2222
{{content-for "test-body"}}
2323

tests/integration/components/assert-must-preload-test.js

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,19 @@ module('Integration | Component | assert must preload', function(hooks) {
3131
// the next line doesn't work in 2.x due to an eslint rule
3232
// eslint-disable-next-line
3333
[ this.major, this.minor ] = Ember.VERSION.split(".");
34+
35+
// setup a bunch of data that our tests will load
36+
let author = this.server.create('author');
37+
let post = this.server.create('post', {
38+
id: 1,
39+
title: 'Post title',
40+
author
41+
});
42+
let comments = this.server.createList('comment', 3, { post });
43+
44+
comments.forEach(comment => {
45+
server.create('author', { comments: [comment] });
46+
});
3447
});
3548

3649
hooks.afterEach(function() {
@@ -41,7 +54,6 @@ module('Integration | Component | assert must preload', function(hooks) {
4154
});
4255

4356
test('it errors if the relationship has not yet be loaded', async function(assert) {
44-
this.server.create('post');
4557
this.post = await run(() => {
4658
return this.store.loadRecord('post', 1);
4759
});
@@ -64,7 +76,6 @@ module('Integration | Component | assert must preload', function(hooks) {
6476
});
6577

6678
test('it errors if one of the relationships has not yet be loaded', async function(assert) {
67-
this.server.create('post');
6879
this.post = await run(() => {
6980
return this.store.loadRecord('post', 1, { include: 'author' });
7081
});
@@ -87,7 +98,6 @@ module('Integration | Component | assert must preload', function(hooks) {
8798
});
8899

89100
test('it errors if a nested relationship has not yet be loaded', async function(assert) {
90-
this.server.create('post');
91101
this.post = await run(() => {
92102
return this.store.loadRecord('post', 1, { include: 'comments' });
93103
});
@@ -110,7 +120,6 @@ module('Integration | Component | assert must preload', function(hooks) {
110120
});
111121

112122
test('it does not error if the relationship was loaded', async function(assert) {
113-
this.server.create('post');
114123
this.post = await run(() => {
115124
return this.store.loadRecord('post', 1, { include: 'comments' });
116125
});
@@ -119,8 +128,52 @@ module('Integration | Component | assert must preload', function(hooks) {
119128
{{assert-must-preload post "comments"}}
120129
`);
121130

122-
// if nothing renders, we're ok
131+
// if anything renders, we're ok
123132
assert.dom('*').hasText('');
124133
});
125134

135+
module('Data loaded with loadRecords', function() {
136+
test('it should not error when all data is loaded', async function(assert) {
137+
let posts = await run(() => {
138+
return this.store.loadRecords('post', { include: 'comments' });
139+
});
140+
141+
this.post = posts.get('firstObject');
142+
143+
await render(hbs`
144+
{{assert-must-preload post "comments"}}
145+
146+
<div data-test-id="title">
147+
{{post.title}}
148+
</div>
149+
`);
150+
151+
assert.dom('[data-test-id="title"]').hasText("Post title");
152+
});
153+
154+
test('it should error is not all data is loaded', async function(assert) {
155+
let posts = await run(() => {
156+
return this.store.loadRecords('post', { include: 'comments,author' });
157+
});
158+
159+
this.post = posts.get('firstObject');
160+
161+
let assertError = function(e) {
162+
let regexp = /You tried to render a .+ that accesses relationships off of a post, but that model didn't have all of its required relationships preloaded ('comments.author')*/;
163+
assert.ok(e.message.match(regexp));
164+
};
165+
166+
if (this.major === "2" && (this.minor === "12" || this.minor === "16")) {
167+
Ember.Logger.error = function() {};
168+
Ember.Test.adapter.exception = assertError;
169+
} else {
170+
Ember.onerror = assertError;
171+
}
172+
173+
await render(hbs`
174+
{{assert-must-preload post "author,comments.author"}}
175+
`);
176+
});
177+
});
178+
126179
});

tests/integration/mixins/loadable-store/load-record-test.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,19 @@ module('Integration | Mixins | LoadableStore | loadRecord', function(hooks) {
1414
models: {
1515
post: Model.extend({
1616
comments: hasMany(),
17+
author: belongsTo(),
1718
tags: hasMany()
1819
}),
1920
comment: Model.extend({
20-
post: belongsTo()
21+
post: belongsTo(),
22+
author: belongsTo()
2123
}),
2224
tag: Model.extend({
2325
posts: hasMany()
26+
}),
27+
author: Model.extend({
28+
comments: hasMany(),
29+
posts: hasMany()
2430
})
2531
},
2632
baseConfig() {

tests/integration/mixins/loadable-store/load-records-test.js

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,19 @@ module('Integration | Mixins | LoadableStore | loadRecords', function(hooks) {
1313
models: {
1414
post: Model.extend({
1515
comments: hasMany(),
16+
author: belongsTo(),
1617
tags: hasMany()
1718
}),
1819
comment: Model.extend({
19-
post: belongsTo()
20+
post: belongsTo(),
21+
author: belongsTo()
2022
}),
2123
tag: Model.extend({
2224
posts: hasMany()
25+
}),
26+
author: Model.extend({
27+
comments: hasMany(),
28+
posts: hasMany()
2329
})
2430
},
2531
baseConfig() {
@@ -139,4 +145,45 @@ module('Integration | Mixins | LoadableStore | loadRecords', function(hooks) {
139145
assert.equal(posts.get('firstObject.id'), serverPost.id);
140146
assert.equal(posts.get('firstObject.comments.length'), 2);
141147
});
148+
149+
module('Tracking includes', function() {
150+
test('it will track an include', async function(assert) {
151+
let serverPost = this.server.create('post', { title: 'My post' });
152+
this.server.createList('comment', 3, { post: serverPost });
153+
154+
let posts = await this.store.loadRecords('post', { include: 'comments' });
155+
156+
assert.ok(posts.get('firstObject').hasLoaded('comments'));
157+
});
158+
159+
test('it will track a dot path include', async function(assert) {
160+
let serverPost = this.server.create('post', { title: 'My post' });
161+
let serverComments = this.server.createList('comment', 3, { post: serverPost });
162+
163+
serverComments.forEach(comment => {
164+
this.server.create('author', { comments: [comment] });
165+
});
166+
167+
let posts = await this.store.loadRecords('post', { include: 'comments.author' });
168+
169+
assert.ok(posts.get('firstObject').hasLoaded('comments.author'));
170+
});
171+
172+
test('it will track multiple includes', async function(assert) {
173+
let serverAuthor = this.server.create('author');
174+
let serverPost = this.server.create('post', {
175+
title: 'My post',
176+
author: serverAuthor
177+
});
178+
let serverComments = this.server.createList('comment', 3, { post: serverPost });
179+
180+
serverComments.forEach(comment => {
181+
this.server.create('author', { comments: [comment] });
182+
});
183+
184+
let posts = await this.store.loadRecords('post', { include: 'author,comments.author' });
185+
186+
assert.ok(posts.get('firstObject').hasLoaded('author,comments.author'));
187+
});
188+
});
142189
});

0 commit comments

Comments
 (0)