|
| 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