Skip to content

Commit ee2c917

Browse files
docs: LinksMode and Polymorphic Relationship Guide, stubs out guide for LegacyMode vs PolarisMode (#9764)
* docs: stub out guides for SchemaRecord, LinksMode, PolarisMode etc. * add linksmode guide * fix annotation * Update guides/relationships/features/links-mode.md Co-authored-by: Krystan HuffMenne <kmenne+github@gmail.com> * Update guides/relationships/features/links-mode.md Co-authored-by: Krystan HuffMenne <kmenne+github@gmail.com> * Update guides/relationships/features/links-mode.md Co-authored-by: Krystan HuffMenne <kmenne+github@gmail.com> * Update guides/relationships/features/links-mode.md Co-authored-by: Krystan HuffMenne <kmenne+github@gmail.com> * Update guides/relationships/features/links-mode.md Co-authored-by: Krystan HuffMenne <kmenne+github@gmail.com> * Update guides/relationships/features/links-mode.md Co-authored-by: Krystan HuffMenne <kmenne+github@gmail.com> * Update guides/relationships/features/links-mode.md Co-authored-by: Krystan HuffMenne <kmenne+github@gmail.com> * Update guides/relationships/features/links-mode.md Co-authored-by: Krystan HuffMenne <kmenne+github@gmail.com> * Update guides/relationships/features/links-mode.md Co-authored-by: Krystan HuffMenne <kmenne+github@gmail.com> * Update guides/relationships/features/links-mode.md Co-authored-by: Krystan HuffMenne <kmenne+github@gmail.com> * Update guides/relationships/features/links-mode.md Co-authored-by: Krystan HuffMenne <kmenne+github@gmail.com> * Update guides/relationships/features/links-mode.md Co-authored-by: Krystan HuffMenne <kmenne+github@gmail.com> * cleanup op * rewrite future thoughts * stub out more of the guide * add docs on polymorphism * stub out guide * add note on interop * add reactivity overview --------- Co-authored-by: Krystan HuffMenne <kmenne+github@gmail.com>
1 parent d69135c commit ee2c917

File tree

7 files changed

+556
-0
lines changed

7 files changed

+556
-0
lines changed

guides/index.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Read [The Manual](./manual/0-index.md)
77

88
- [Relationships](./relationships/index.md)
99
- [Requests](./requests/index.md)
10+
- [Reactivity](./reactive-data/index.md)
1011
- [Typescript](./typescript/index.md)
1112
- [Terminology](./terminology.md)
1213
- [Cookbook](./cookbook/index.md)

guides/reactive-data/index.md

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Reactive Data
2+
3+
In addition to request and cache management, WarpDrive provides a reactive access
4+
layer for data in the cache.
5+
6+
Data in the cache is conceptualized as belonging to one of three forms
7+
8+
- **Documents** - the response to a request
9+
- **Resources** - a unique cacheable entity within the response to a request
10+
- **Fields** - the data for an individual property on a resource
11+
12+
Each form of data can be accessed and managed reactively through one of two modes
13+
14+
- *(upcoming, default in v6)* [PolarisMode](./polaris/overview.md)
15+
- *(current, default in v5)* [LegacyMode](./legacy/overview.md)
16+
17+
These modes are interopable. The reactive object (record) for a resource in PolarisMode can relate to
18+
a record in LegacyMode and vice-versa. This interopability is true whether the record in LegacyMode is
19+
a SchemaRecord or a Model.
20+
21+
These reactive primitives use fine-grained signals-based reactivity. Currently, we use
22+
glimmer's (Ember's) implementation of `Signal` (`@tracked`) and `Computed` (`@cached`);
23+
however, we've architected our use to be pluggable and will soon enable configuration
24+
of any desired implementation, thus making WarpDrive compatible with any signals compatible
25+
library or framework.
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# LegacyMode
2+
3+
-[ReactiveData](../index.md)
4+
5+
---
6+
7+
🚧 This guide is under active construction, it will talk about the below points 🚧
8+
9+
In LegacyMode records are:
10+
11+
- mutable
12+
- local changes immediately reflect app wide
13+
- all the APIs of Model (references, state props, currentState, methods etc)
14+
- limited reactivity for fields (same as Model)
15+
- requires continued use of the `model` and `legacy-compat` packages (though most imports from them can be removed)
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# PolarisMode
2+
3+
-[ReactiveData](../index.md)
4+
5+
---
6+
7+
🚧 This guide is under active construction, it will talk about the below points 🚧
8+
9+
In PolarisMode records are:
10+
11+
- immutable (unless creating or checking out for editing)
12+
- local changes only show where you want them to on the editable version
13+
- none of the APIs of Model (references, state props, currentState, methods etc)
14+
- deep reactivity for fields
15+
- advanced derivations, aliasing and transformations
16+
- [currently] limited support for relationships
17+
- no promise proxies for relationships
+231
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
# LinksMode
2+
3+
-[Relationships Guide](../index.md)
4+
5+
---
6+
7+
**LinksMode** is a special feature that can be activated for any `belongsTo` or `hasMany` relationship.
8+
9+
It allows that relationship to be fetched using the standard `request` experience instead of via the legacy `adapter` interface.
10+
11+
LinksMode behaves *slightly* differently depending on whether
12+
you are using Model (including via [LegacyMode](../../reactive-data/legacy/overview.md)) or [PolarisMode](../../reactive-data/polaris/overview.md). We'll explain this nuance below.
13+
14+
> [!TIP]
15+
> The next-generation of reactive data which replaces Model is SchemaRecord.
16+
> SchemaRecord has two modes, Legacy - which emulates all of Model's
17+
> behaviors and APIs, and Polaris - a new experience which we intend
18+
> to make default in Version 6.
19+
20+
## How Does It Work?
21+
22+
### Related Link Becomes Required
23+
24+
Relationships in WarpDrive are stored using the same top-level structure as resource documents, a structure
25+
adopted from the [JSON:API](https://jsonapi.org) specification.
26+
27+
- **data**: the membership (state of) the relationship
28+
- **links** *(optional)*: an object containing various links for fetching and managing the relationship
29+
- **meta** *(optional)*: an object of arbitrary extra information about the relationship
30+
31+
This is roughly described by the interface below:
32+
33+
```ts
34+
interface Relationship {
35+
meta?: Record<string, Value>;
36+
links?: Links;
37+
}
38+
39+
interface ResourceRelationship extends Relationship {
40+
data: { type: string; id: string | null; lid: string } | null;
41+
}
42+
43+
interface CollectionRelationship extends Relationship {
44+
data: { type: string; id: string | null; lid: string }[]
45+
}
46+
```
47+
48+
When `linksMode` is activated for a relationship, it is required that a related link is present.
49+
50+
```ts
51+
interface LinksModeRelationship {
52+
meta?: Record<string, Value>;
53+
54+
// no longer optional
55+
links: {
56+
related: string | { href: string };
57+
58+
// other links as desired
59+
}
60+
}
61+
```
62+
63+
### Related Links May Be Provided by Handlers
64+
65+
This means that, in order to use links mode, a relationship payload given to the cache MUST contain this related link.
66+
67+
If your API does not provide this link, a [request handler](https://api.emberjs.com/ember-data/release/classes/%3Cinterface%3E%20handler/) could be utilized to decorate an API response to add them provided that your handlers (or your API) are able to understand that link.
68+
69+
Note that this approach can even work if your API requires you to send a POST request to fetch the relationship. [This blog post](https://runspired.com/2025/02/26/exploring-advanced-handlers.html) contains an overview of advanced request handling to achieve a similar aim for pagination.
70+
71+
### When a Relationship Is Fetched, the Related Link Is Used
72+
73+
Fetching a relationship via any of `relationship.reload`, `reference.reload`, `reference.load` or `await record.relationship` will issue a request to your handler chain. That request will look like the following:
74+
75+
```ts
76+
interface FetchRelationshipRequest {
77+
op: 'findHasMany' | 'findBelongsTo';
78+
store: Store;
79+
url: string; // the related link
80+
method: 'GET';
81+
records: StableRecordIdentifier[]; // the current membership of the relationship
82+
data: {
83+
field: LegacyBelongsToField | LegacyHasManyField;
84+
links: Links;
85+
meta: Meta;
86+
options: unknown; // any options passed to `reload` or `load`
87+
record: StableRecordIdentifier; // the parent record
88+
};
89+
90+
// tells the store to not automatically convert the response into something reactive
91+
// since the reactive relationship class itself will do that
92+
[EnableHydration]: false;
93+
}
94+
```
95+
96+
The three most important things in this request are:
97+
98+
- the `op` code: this is how the cache will know to use the response to update the state of a relationship
99+
- `data.field`: this is how the cache will know which field it should update
100+
- `data.record`: this is how the cache will know which record to associate the response to.
101+
102+
The normalized API response (what your handler must return either directly from your API or with some normalization on the client) that should be passed to the JSON:API cache should be a standard JSON:API document.
103+
104+
The contents of `data` will be inserted into the resource cache and the list of records contained therein will be used to update the state of the relationship. The `meta` and `links` of the response will become the `meta` and `links` available for the
105+
relationship as well.
106+
107+
Sideloads (included records) are valid to include in these responses.
108+
109+
## Activating LinksMode
110+
111+
LinksMode is activated by adding `linksMode: true` to the relationship's options.
112+
113+
Read on below for examples and nuances specific to Model vs SchemaRecord
114+
115+
### For a Relationship on a Model
116+
117+
```ts
118+
import Model, { belongsTo, hasMany } from '@ember-data/model';
119+
120+
export default class User extends Model {
121+
@belongsTo('address', {
122+
async: false,
123+
inverse: 'residents',
124+
linksMode: true
125+
})
126+
homeAddress;
127+
}
128+
```
129+
130+
This works for both `async` and `non-async` relationships and only changes the fetching behavior of the field it is defined on. For instance, in the example above, `homeAddress` is fetched in links mode while `<Address>.residents` might still be using the legacy adapter experience.
131+
132+
### For a SchemaRecord in LegacyMode
133+
134+
```ts
135+
import type { ResourceSchema } from '@warp-drive/core-types/schema/fields';
136+
137+
const UserSchema = {
138+
type: 'user',
139+
// this is what puts the record instance into LegacyMode
140+
legacy: true,
141+
fields: [
142+
{
143+
kind: 'belongsTo',
144+
name: 'homeAddress',
145+
options: {
146+
async: false,
147+
inverse: 'residents',
148+
linksMode: true
149+
}
150+
}
151+
]
152+
} satisfies ResourceSchema;
153+
```
154+
155+
The behavior of a relationship for a SchemaRecord in LegacyMode is always identical to that of a the same
156+
relationship defined on a Model.
157+
158+
### For a SchemaRecord in PolarisMode
159+
160+
```ts
161+
import type { ResourceSchema } from '@warp-drive/core-types/schema/fields';
162+
163+
const UserSchema = {
164+
type: 'user',
165+
fields: [
166+
{
167+
kind: 'belongsTo',
168+
name: 'homeAddress',
169+
options: {
170+
async: false,
171+
inverse: 'residents',
172+
linksMode: true
173+
}
174+
}
175+
]
176+
} satisfies ResourceSchema;
177+
```
178+
179+
The only difference here is that we don't mark the resource schemas as `legacy`. This puts us in the standard/default mode (`polaris`);
180+
181+
When using PolarisMode, `hasMany` and `belongsTo` relationships have additional constraints:
182+
183+
- 1. They MUST use linksMode. Nothing except linksMode is supported.
184+
- 2. They MUST be `async: false`. Async relationships will never be supported in PolarisMode (read more on this below)
185+
- 3. There is no `autofetch` behavior (because relationships are `async: false`)
186+
187+
You can load this link to fetch the relationship, though it is less easy to because the utility methods and links are
188+
not as readily exposed via references as they are with Model.
189+
190+
For `belongsTo` this is a particularly large drawback. `belongsTo` has no mechanism by which to expose its links or a reload method. There are work arounds via the cache API / via derivations if needed, but cumbersome.
191+
192+
For `hasMany`, this restriction is not too difficult as it can be loaded via its link by calling `reload`, e.g. `user.friends.reload()`. As with hasMany in LegacyMode, its links are also available via `user.friends.links`.
193+
194+
This makes PolarisMode relationships intentionally limited. This limitation is not permanent – there is a replacement
195+
in the works for `belongsTo` and `hasMany` that aligns relationships with the intended Polaris experience.
196+
197+
In the meantime, we've enabled synchronous linksMode relationships in order to allow folks to experiment with the polaris experience while still staying generally aligned with the direction relationships will evolve.
198+
199+
If this limitation is too great we would recommend continuing to use `LegacyMode` until the full story for
200+
relationships in PolarisMode is shipped.
201+
202+
#### What To Expect from PolarisMode Relationships in the Future
203+
204+
We intend to replace `belongsTo` and `hasMany` fields with the (as yet not implemented)
205+
`resource` and `collection` fields.
206+
207+
These fields will have no `autofetch` behavior, and no async proxy. There will still be `sync` and `async`
208+
variations of the field but this flag will take on a better meaning.
209+
210+
An `async` relationship represents a POTENTIALLY asynchronous boundary in your API, meaning that even if
211+
sometimes the data for that relationship is included as a sideload, it may not always be and may require
212+
its own request. Async collection relationships can be paginated.
213+
214+
A `sync` relationship represents an ALWAYS synchronous boundary, meaning that the full state of the relationship
215+
is ALWAYS included as a sideload and cannot ever be loaded as its own request. Sync relationships can never be
216+
paginated, and generally require use of a request which fetches their parent record to get updated state.
217+
218+
In LegacyMode, sync relationships gave direct access to the record or array while async relationships gave access
219+
to a promisified proxy to the record/array.
220+
221+
In PolarisMode using `resource` and `collection`, sync relationships will also give direct access while async
222+
relationships will instead provide access to a [reactive document](https://api.emberjs.com/ember-data/release/classes/Document).
223+
224+
So for instance, if `user.homeAddress` were `async: false`, then its value would be an instance of an `Address` record.
225+
But if `user.homeAddress` were `asunc: true`, it would instead be a reactive class with `links`, `meta` and (only-if-loaded) `data`.
226+
227+
- `user.homeAddress.links` would provide access to its associated links
228+
- `user.homeAddress.meta` would provide access to any associated meta
229+
- `user.homeAddress.data` would provide access to the address record instance IF (and only if) the relationship data had been included as part of the response for a parent record previously OR fetched explicitly via its link.
230+
231+

0 commit comments

Comments
 (0)