Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

docs: LinksMode and Polymorphic Relationship Guide, stubs out guide for LegacyMode vs PolarisMode #9764

Merged
merged 22 commits into from
Mar 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
cbefaed
docs: stub out guides for SchemaRecord, LinksMode, PolarisMode etc.
runspired Mar 12, 2025
58a3afd
add linksmode guide
runspired Mar 13, 2025
59d027b
fix annotation
runspired Mar 13, 2025
6bb79f1
Update guides/relationships/features/links-mode.md
runspired Mar 13, 2025
bad2c27
Update guides/relationships/features/links-mode.md
runspired Mar 13, 2025
a61f432
Update guides/relationships/features/links-mode.md
runspired Mar 13, 2025
b27a098
Update guides/relationships/features/links-mode.md
runspired Mar 13, 2025
77e1747
Update guides/relationships/features/links-mode.md
runspired Mar 13, 2025
bb6aff7
Update guides/relationships/features/links-mode.md
runspired Mar 13, 2025
c524062
Update guides/relationships/features/links-mode.md
runspired Mar 13, 2025
3e34074
Update guides/relationships/features/links-mode.md
runspired Mar 13, 2025
40c4354
Update guides/relationships/features/links-mode.md
runspired Mar 13, 2025
8cfe7e0
Update guides/relationships/features/links-mode.md
runspired Mar 13, 2025
d305f6e
Update guides/relationships/features/links-mode.md
runspired Mar 13, 2025
a97886b
Update guides/relationships/features/links-mode.md
runspired Mar 13, 2025
eea07a3
cleanup op
runspired Mar 13, 2025
6dbebee
rewrite future thoughts
runspired Mar 13, 2025
6739b85
stub out more of the guide
runspired Mar 13, 2025
4a764ce
add docs on polymorphism
runspired Mar 13, 2025
5985df5
stub out guide
runspired Mar 13, 2025
9a356ac
add note on interop
runspired Mar 13, 2025
280b100
add reactivity overview
runspired Mar 13, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions guides/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Read [The Manual](./manual/0-index.md)

- [Relationships](./relationships/index.md)
- [Requests](./requests/index.md)
- [Reactivity](./reactive-data/index.md)
- [Typescript](./typescript/index.md)
- [Terminology](./terminology.md)
- [Cookbook](./cookbook/index.md)
Expand Down
25 changes: 25 additions & 0 deletions guides/reactive-data/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Reactive Data

In addition to request and cache management, WarpDrive provides a reactive access
layer for data in the cache.

Data in the cache is conceptualized as belonging to one of three forms

- **Documents** - the response to a request
- **Resources** - a unique cacheable entity within the response to a request
- **Fields** - the data for an individual property on a resource

Each form of data can be accessed and managed reactively through one of two modes

- *(upcoming, default in v6)* [PolarisMode](./polaris/overview.md)
- *(current, default in v5)* [LegacyMode](./legacy/overview.md)

These modes are interopable. The reactive object (record) for a resource in PolarisMode can relate to
a record in LegacyMode and vice-versa. This interopability is true whether the record in LegacyMode is
a SchemaRecord or a Model.

These reactive primitives use fine-grained signals-based reactivity. Currently, we use
glimmer's (Ember's) implementation of `Signal` (`@tracked`) and `Computed` (`@cached`);
however, we've architected our use to be pluggable and will soon enable configuration
of any desired implementation, thus making WarpDrive compatible with any signals compatible
library or framework.
15 changes: 15 additions & 0 deletions guides/reactive-data/legacy/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# LegacyMode

- ⮐ [ReactiveData](../index.md)

---

🚧 This guide is under active construction, it will talk about the below points 🚧

In LegacyMode records are:

- mutable
- local changes immediately reflect app wide
- all the APIs of Model (references, state props, currentState, methods etc)
- limited reactivity for fields (same as Model)
- requires continued use of the `model` and `legacy-compat` packages (though most imports from them can be removed)
17 changes: 17 additions & 0 deletions guides/reactive-data/polaris/overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# PolarisMode

- ⮐ [ReactiveData](../index.md)

---

🚧 This guide is under active construction, it will talk about the below points 🚧

In PolarisMode records are:

- immutable (unless creating or checking out for editing)
- local changes only show where you want them to on the editable version
- none of the APIs of Model (references, state props, currentState, methods etc)
- deep reactivity for fields
- advanced derivations, aliasing and transformations
- [currently] limited support for relationships
- no promise proxies for relationships
231 changes: 231 additions & 0 deletions guides/relationships/features/links-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# LinksMode

- ⮐ [Relationships Guide](../index.md)

---

**LinksMode** is a special feature that can be activated for any `belongsTo` or `hasMany` relationship.

It allows that relationship to be fetched using the standard `request` experience instead of via the legacy `adapter` interface.

LinksMode behaves *slightly* differently depending on whether
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.

> [!TIP]
> The next-generation of reactive data which replaces Model is SchemaRecord.
> SchemaRecord has two modes, Legacy - which emulates all of Model's
> behaviors and APIs, and Polaris - a new experience which we intend
> to make default in Version 6.

## How Does It Work?

### Related Link Becomes Required

Relationships in WarpDrive are stored using the same top-level structure as resource documents, a structure
adopted from the [JSON:API](https://jsonapi.org) specification.

- **data**: the membership (state of) the relationship
- **links** *(optional)*: an object containing various links for fetching and managing the relationship
- **meta** *(optional)*: an object of arbitrary extra information about the relationship

This is roughly described by the interface below:

```ts
interface Relationship {
meta?: Record<string, Value>;
links?: Links;
}

interface ResourceRelationship extends Relationship {
data: { type: string; id: string | null; lid: string } | null;
}

interface CollectionRelationship extends Relationship {
data: { type: string; id: string | null; lid: string }[]
}
```

When `linksMode` is activated for a relationship, it is required that a related link is present.

```ts
interface LinksModeRelationship {
meta?: Record<string, Value>;

// no longer optional
links: {
related: string | { href: string };

// other links as desired
}
}
```

### Related Links May Be Provided by Handlers

This means that, in order to use links mode, a relationship payload given to the cache MUST contain this related link.

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.

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.

### When a Relationship Is Fetched, the Related Link Is Used

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:

```ts
interface FetchRelationshipRequest {
op: 'findHasMany' | 'findBelongsTo';
store: Store;
url: string; // the related link
method: 'GET';
records: StableRecordIdentifier[]; // the current membership of the relationship
data: {
field: LegacyBelongsToField | LegacyHasManyField;
links: Links;
meta: Meta;
options: unknown; // any options passed to `reload` or `load`
record: StableRecordIdentifier; // the parent record
};

// tells the store to not automatically convert the response into something reactive
// since the reactive relationship class itself will do that
[EnableHydration]: false;
}
```

The three most important things in this request are:

- the `op` code: this is how the cache will know to use the response to update the state of a relationship
- `data.field`: this is how the cache will know which field it should update
- `data.record`: this is how the cache will know which record to associate the response to.

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.

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
relationship as well.

Sideloads (included records) are valid to include in these responses.

## Activating LinksMode

LinksMode is activated by adding `linksMode: true` to the relationship's options.

Read on below for examples and nuances specific to Model vs SchemaRecord

### For a Relationship on a Model

```ts
import Model, { belongsTo, hasMany } from '@ember-data/model';

export default class User extends Model {
@belongsTo('address', {
async: false,
inverse: 'residents',
linksMode: true
})
homeAddress;
}
```

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.

### For a SchemaRecord in LegacyMode

```ts
import type { ResourceSchema } from '@warp-drive/core-types/schema/fields';

const UserSchema = {
type: 'user',
// this is what puts the record instance into LegacyMode
legacy: true,
fields: [
{
kind: 'belongsTo',
name: 'homeAddress',
options: {
async: false,
inverse: 'residents',
linksMode: true
}
}
]
} satisfies ResourceSchema;
```

The behavior of a relationship for a SchemaRecord in LegacyMode is always identical to that of a the same
relationship defined on a Model.

### For a SchemaRecord in PolarisMode

```ts
import type { ResourceSchema } from '@warp-drive/core-types/schema/fields';

const UserSchema = {
type: 'user',
fields: [
{
kind: 'belongsTo',
name: 'homeAddress',
options: {
async: false,
inverse: 'residents',
linksMode: true
}
}
]
} satisfies ResourceSchema;
```

The only difference here is that we don't mark the resource schemas as `legacy`. This puts us in the standard/default mode (`polaris`);

When using PolarisMode, `hasMany` and `belongsTo` relationships have additional constraints:

- 1. They MUST use linksMode. Nothing except linksMode is supported.
- 2. They MUST be `async: false`. Async relationships will never be supported in PolarisMode (read more on this below)
- 3. There is no `autofetch` behavior (because relationships are `async: false`)

You can load this link to fetch the relationship, though it is less easy to because the utility methods and links are
not as readily exposed via references as they are with Model.

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.

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

This makes PolarisMode relationships intentionally limited. This limitation is not permanent – there is a replacement
in the works for `belongsTo` and `hasMany` that aligns relationships with the intended Polaris experience.

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.

If this limitation is too great we would recommend continuing to use `LegacyMode` until the full story for
relationships in PolarisMode is shipped.

#### What To Expect from PolarisMode Relationships in the Future

We intend to replace `belongsTo` and `hasMany` fields with the (as yet not implemented)
`resource` and `collection` fields.

These fields will have no `autofetch` behavior, and no async proxy. There will still be `sync` and `async`
variations of the field but this flag will take on a better meaning.

An `async` relationship represents a POTENTIALLY asynchronous boundary in your API, meaning that even if
sometimes the data for that relationship is included as a sideload, it may not always be and may require
its own request. Async collection relationships can be paginated.

A `sync` relationship represents an ALWAYS synchronous boundary, meaning that the full state of the relationship
is ALWAYS included as a sideload and cannot ever be loaded as its own request. Sync relationships can never be
paginated, and generally require use of a request which fetches their parent record to get updated state.

In LegacyMode, sync relationships gave direct access to the record or array while async relationships gave access
to a promisified proxy to the record/array.

In PolarisMode using `resource` and `collection`, sync relationships will also give direct access while async
relationships will instead provide access to a [reactive document](https://api.emberjs.com/ember-data/release/classes/Document).

So for instance, if `user.homeAddress` were `async: false`, then its value would be an instance of an `Address` record.
But if `user.homeAddress` were `asunc: true`, it would instead be a reactive class with `links`, `meta` and (only-if-loaded) `data`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

asunc (typo)


- `user.homeAddress.links` would provide access to its associated links
- `user.homeAddress.meta` would provide access to any associated meta
- `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.


Loading
Loading