Skip to content

Commit 408d304

Browse files
authored
Merge pull request #20376 from emberjs/deprecate-implicit-loading-route
[RFC-774] Deprecate implicit record loading in Ember Route
2 parents 2f18e27 + 464fe3e commit 408d304

File tree

5 files changed

+150
-69
lines changed

5 files changed

+150
-69
lines changed

packages/@ember/routing/route.ts

Lines changed: 42 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { isProxy, lookupDescriptor } from '@ember/-internals/utils';
1717
import type { AnyFn } from '@ember/-internals/utility-types';
1818
import Controller from '@ember/controller';
1919
import type { ControllerQueryParamType } from '@ember/controller';
20-
import { assert, info, isTesting } from '@ember/debug';
20+
import { assert, deprecate, info, isTesting } from '@ember/debug';
2121
import EngineInstance from '@ember/engine/instance';
2222
import { dependentKeyCompat } from '@ember/object/compat';
2323
import { once } from '@ember/runloop';
@@ -59,6 +59,16 @@ type RouteTransitionState = TransitionState<Route> & {
5959
type MaybeParameters<T> = T extends AnyFn ? Parameters<T> : unknown[];
6060
type MaybeReturnType<T> = T extends AnyFn ? ReturnType<T> : unknown;
6161

62+
interface StoreLike {
63+
find(type: string, value: unknown): unknown;
64+
}
65+
66+
function isStoreLike(store: unknown): store is StoreLike {
67+
return (
68+
typeof store === 'object' && store !== null && typeof (store as StoreLike).find === 'function'
69+
);
70+
}
71+
6272
export const ROUTE_CONNECTIONS = new WeakMap();
6373
const RENDER = Symbol('render');
6474

@@ -1109,15 +1119,6 @@ class Route<Model = unknown> extends EmberObject.extend(ActionHandler, Evented)
11091119
export default Router;
11101120
```
11111121
1112-
The model for the `post` route is `store.findRecord('post', params.post_id)`.
1113-
1114-
By default, if your route has a dynamic segment ending in `_id`:
1115-
1116-
* The model class is determined from the segment (`post_id`'s
1117-
class is `App.Post`)
1118-
* The find method is called on the model class with the value of
1119-
the dynamic segment.
1120-
11211122
Note that for routes with dynamic segments, this hook is not always
11221123
executed. If the route is entered through a transition (e.g. when
11231124
using the `link-to` Handlebars helper or the `transitionTo` method
@@ -1151,12 +1152,21 @@ class Route<Model = unknown> extends EmberObject.extend(ActionHandler, Evented)
11511152
if a promise returned from `model` fails, the error will be
11521153
handled by the `error` hook on `Route`.
11531154
1155+
Note that the legacy behavior of automatically defining a model
1156+
hook when a dynamic segment ending in `_id` is present is
1157+
[deprecated](https://deprecations.emberjs.com/v5.x#toc_deprecate-implicit-route-model).
1158+
You should explicitly define a model hook whenever any segments are
1159+
present.
1160+
11541161
Example
11551162
11561163
```app/routes/post.js
11571164
import Route from '@ember/routing/route';
1165+
import { service } from '@ember/service';
11581166
11591167
export default class PostRoute extends Route {
1168+
@service store;
1169+
11601170
model(params) {
11611171
return this.store.findRecord('post', params.post_id);
11621172
}
@@ -1233,14 +1243,24 @@ class Route<Model = unknown> extends EmberObject.extend(ActionHandler, Evented)
12331243
@param {Object} value the value passed to find
12341244
@private
12351245
*/
1236-
findModel(...args: any[]) {
1237-
// SAFETY: it's all absurd lies; there is no explicit contract for `store`
1238-
// and we allow people to register *anything* here and we call it: GLHF! The
1239-
// fallback path here means we correctly handle the case where there is no
1240-
// explicit store injection on the route subclass.
1241-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
1242-
let store = ('store' in this ? this.store : get(this, '_store')) as any;
1243-
return store.find(...args);
1246+
findModel(type: string, value: unknown) {
1247+
deprecate(
1248+
`The implicit model loading behavior for routes is deprecated. ` +
1249+
`Please define an explicit model hook for ${this.fullRouteName}.`,
1250+
false,
1251+
{
1252+
id: 'deprecate-implicit-route-model',
1253+
for: 'ember-source',
1254+
since: { available: '5.3.0' },
1255+
until: '6.0.0',
1256+
}
1257+
);
1258+
1259+
const store = 'store' in this ? this.store : get(this, '_store');
1260+
assert('Expected route to have a store with a find method', isStoreLike(store));
1261+
1262+
// SAFETY: We don't actually know it will return this, but this code path is also deprecated.
1263+
return store.find(type, value) as Model | PromiseLike<Model> | undefined;
12441264
}
12451265

12461266
/**
@@ -1259,8 +1279,11 @@ class Route<Model = unknown> extends EmberObject.extend(ActionHandler, Evented)
12591279
12601280
```app/routes/photos.js
12611281
import Route from '@ember/routing/route';
1282+
import { service } from '@ember/service';
12621283
12631284
export default class PhotosRoute extends Route {
1285+
@service store;
1286+
12641287
model() {
12651288
return this.store.findAll('photo');
12661289
}
@@ -1564,20 +1587,7 @@ class Route<Model = unknown> extends EmberObject.extend(ActionHandler, Evented)
15641587
return params;
15651588
}
15661589

1567-
/**
1568-
Store property provides a hook for data persistence libraries to inject themselves.
1569-
1570-
By default, this store property provides the exact same functionality previously
1571-
in the model hook.
1572-
1573-
Currently, the required interface is:
1574-
1575-
`store.find(modelName, findArguments)`
1576-
1577-
@property store
1578-
@type {Object}
1579-
@private
1580-
*/
1590+
/** @deprecated Manually define your own store, such as with `@service store` */
15811591
@computed
15821592
protected get _store() {
15831593
const owner = getOwner(this);

packages/@ember/routing/tests/system/route_test.js

Lines changed: 53 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ moduleFor(
2323
}
2424

2525
['@test default store utilizes the container to acquire the model factory'](assert) {
26-
assert.expect(4);
26+
assert.expect(5);
2727

2828
let Post = EmberObject.extend();
2929
let post = {};
@@ -55,12 +55,34 @@ moduleFor(
5555
let owner = buildOwner(ownerOptions);
5656
setOwner(route, owner);
5757

58-
assert.equal(route.model({ post_id: 1 }), post);
59-
assert.equal(route.findModel('post', 1), post, '#findModel returns the correct post');
58+
expectDeprecation(
59+
() =>
60+
ignoreAssertion(() => {
61+
assert.equal(route.model({ post_id: 1 }), post);
62+
assert.equal(route.findModel('post', 1), post, '#findModel returns the correct post');
63+
}),
64+
/The implicit model loading behavior for routes is deprecated./
65+
);
6066

6167
runDestroy(owner);
6268
}
6369

70+
['@test default store can be overridden'](assert) {
71+
runDestroy(route);
72+
73+
let calledFind = false;
74+
route = EmberRoute.extend({
75+
store: {
76+
find() {
77+
calledFind = true;
78+
},
79+
},
80+
}).create();
81+
82+
route.store.find();
83+
assert.true(calledFind, 'store.find was called');
84+
}
85+
6486
["@test assert if 'store.find' method is not found"]() {
6587
runDestroy(route);
6688

@@ -77,21 +99,23 @@ moduleFor(
7799

78100
route = owner.lookup('route:index');
79101

80-
expectAssertion(function () {
81-
route.findModel('post', 1);
82-
}, `You used the dynamic segment \`post_id\` in your route ` +
83-
`\`index\` for which Ember requires you provide a ` +
84-
`data-loading implementation. Commonly, that is done by ` +
85-
`adding a model hook implementation on the route ` +
86-
`(\`model({post_id}) {\`) or by injecting an implemention of ` +
87-
`a data store: \`@service store;\`.\n\n` +
88-
`Rarely, applications may attempt to use a legacy behavior where ` +
89-
`the model class (in this case \`post\`) is resolved and the ` +
90-
`\`find\` method on that class is invoked to load data. In this ` +
91-
`application, a model of \`post\` was found but it did not ` +
92-
`provide a \`find\` method. You should not add a \`find\` ` +
93-
`method to your model. Instead, please implement an appropriate ` +
94-
`\`model\` hook on the \`index\` route.`);
102+
ignoreDeprecation(() =>
103+
expectAssertion(function () {
104+
route.findModel('post', 1);
105+
}, `You used the dynamic segment \`post_id\` in your route ` +
106+
`\`index\` for which Ember requires you provide a ` +
107+
`data-loading implementation. Commonly, that is done by ` +
108+
`adding a model hook implementation on the route ` +
109+
`(\`model({post_id}) {\`) or by injecting an implemention of ` +
110+
`a data store: \`@service store;\`.\n\n` +
111+
`Rarely, applications may attempt to use a legacy behavior where ` +
112+
`the model class (in this case \`post\`) is resolved and the ` +
113+
`\`find\` method on that class is invoked to load data. In this ` +
114+
`application, a model of \`post\` was found but it did not ` +
115+
`provide a \`find\` method. You should not add a \`find\` ` +
116+
`method to your model. Instead, please implement an appropriate ` +
117+
`\`model\` hook on the \`index\` route.`)
118+
);
95119

96120
runDestroy(owner);
97121
}
@@ -109,14 +133,16 @@ moduleFor(
109133

110134
route = owner.lookup('route:index');
111135

112-
expectAssertion(function () {
113-
route.model({ post_id: 1 });
114-
}, `You used the dynamic segment \`post_id\` in your route ` +
115-
`\`index\` for which Ember requires you provide a ` +
116-
`data-loading implementation. Commonly, that is done by ` +
117-
`adding a model hook implementation on the route ` +
118-
`(\`model({post_id}) {\`) or by injecting an implemention of ` +
119-
`a data store: \`@service store;\`.`);
136+
ignoreDeprecation(() =>
137+
expectAssertion(function () {
138+
route.model({ post_id: 1 });
139+
}, `You used the dynamic segment \`post_id\` in your route ` +
140+
`\`index\` for which Ember requires you provide a ` +
141+
`data-loading implementation. Commonly, that is done by ` +
142+
`adding a model hook implementation on the route ` +
143+
`(\`model({post_id}) {\`) or by injecting an implemention of ` +
144+
`a data store: \`@service store;\`.`)
145+
);
120146

121147
runDestroy(owner);
122148
}
@@ -130,9 +156,7 @@ moduleFor(
130156

131157
route = owner.lookup('route:index');
132158

133-
ignoreAssertion(function () {
134-
route.model({ post_id: 1 });
135-
});
159+
ignoreDeprecation(() => ignoreAssertion(() => route.model({ post_id: 1 })));
136160

137161
assert.ok(true, 'no error was raised');
138162

packages/ember/tests/routing/decoupled_basic_test.js

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,17 +131,29 @@ moduleFor(
131131

132132
this.add('model:menu_item', MenuItem);
133133

134+
let SpecialRoute = class extends Route {
135+
model({ menu_item_id }) {
136+
return MenuItem.find(menu_item_id);
137+
}
138+
};
139+
140+
this.add('route:special', SpecialRoute);
141+
134142
this.addTemplate('special', '<p>{{@model.id}}</p>');
135143
this.addTemplate('loading', '<p>LOADING!</p>');
136144

137-
let visited = runTask(() => this.visit('/specials/1'));
138-
this.assertText('LOADING!', 'The app is in the loading state');
145+
let promise;
146+
ignoreDeprecation(() => {
147+
let visited = runTask(() => this.visit('/specials/1'));
148+
this.assertText('LOADING!', 'The app is in the loading state');
139149

140-
resolve(menuItem);
150+
resolve(menuItem);
141151

142-
return visited.then(() => {
143-
this.assertText('1', 'The app is now in the specials state');
152+
promise = visited.then(() => {
153+
this.assertText('1', 'The app is now in the specials state');
154+
});
144155
});
156+
return promise;
145157
}
146158

147159
[`@test The loading state doesn't get entered for promises that resolve on the same run loop`](
@@ -161,6 +173,14 @@ moduleFor(
161173

162174
this.add('model:menu_item', MenuItem);
163175

176+
let SpecialRoute = class extends Route {
177+
model({ menu_item_id }) {
178+
return MenuItem.find(menu_item_id);
179+
}
180+
};
181+
182+
this.add('route:special', SpecialRoute);
183+
164184
this.add(
165185
'route:loading',
166186
Route.extend({
@@ -219,7 +239,9 @@ moduleFor(
219239
})
220240
);
221241

222-
runTask(() => handleURLRejectsWith(this, assert, 'specials/1', 'Setup error'));
242+
ignoreDeprecation(() => {
243+
runTask(() => handleURLRejectsWith(this, assert, 'specials/1', 'Setup error'));
244+
});
223245

224246
resolve(menuItem);
225247
}
@@ -263,6 +285,10 @@ moduleFor(
263285
this.add(
264286
'route:special',
265287
Route.extend({
288+
model({ menu_item_id }) {
289+
return MenuItem.find(menu_item_id);
290+
},
291+
266292
setup() {
267293
throw new Error('Setup error');
268294
},

packages/ember/tests/routing/model_loading_test.js

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,14 @@ moduleFor(
362362
});
363363
this.add('model:menu_item', MenuItem);
364364

365+
let SpecialRoute = class extends Route {
366+
model({ menu_item_id }) {
367+
return MenuItem.find(menu_item_id);
368+
}
369+
};
370+
371+
this.add('route:special', SpecialRoute);
372+
365373
this.router.map(function () {
366374
this.route('home', { path: '/' });
367375
this.route('special', { path: '/specials/:menu_item_id' });
@@ -390,6 +398,13 @@ moduleFor(
390398
});
391399
this.add('model:menu_item', MenuItem);
392400

401+
let SpecialRoute = class extends Route {
402+
model({ menu_item_id }) {
403+
return MenuItem.find(menu_item_id);
404+
}
405+
};
406+
this.add('route:special', SpecialRoute);
407+
393408
this.addTemplate('home', '<h3>Home</h3>');
394409
this.addTemplate('special', '<p>{{@model.id}}</p>');
395410

@@ -813,13 +828,20 @@ moduleFor(
813828
['@test Route model hook finds the same model as a manual find'](assert) {
814829
let post;
815830
let Post = EmberObject.extend();
816-
this.add('model:post', Post);
817831
Post.reopenClass({
818832
find() {
819833
post = this;
820834
return {};
821835
},
822836
});
837+
this.add('model:post', Post);
838+
839+
let PostRoute = class extends Route {
840+
model({ post_id }) {
841+
return Post.find(post_id);
842+
}
843+
};
844+
this.add('route:post', PostRoute);
823845

824846
this.router.map(function () {
825847
this.route('post', { path: '/post/:post_id' });

tests/docs/expected.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,6 @@ module.exports = {
484484
'sort',
485485
'sortBy',
486486
'startRouting',
487-
'store',
488487
'subscribe',
489488
'sum',
490489
'tagName',

0 commit comments

Comments
 (0)