From 063ab7eb9ad9293816ce35b08cd6d91175453277 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Wed, 25 Oct 2023 21:24:02 -0700 Subject: [PATCH 1/8] add guide for one-to-none --- guides/relationships.md | 163 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 147 insertions(+), 16 deletions(-) diff --git a/guides/relationships.md b/guides/relationships.md index 9428188ce7a..3ee2364e886 100644 --- a/guides/relationships.md +++ b/guides/relationships.md @@ -1,26 +1,28 @@ # Relationships Guide -**Feature Overview** -- [Inverses]() -- [Polymorphism]() -- [Links vs Identifiers]() -- [Sync vs Async]() - -**Configuration** -- [1:none]() (One To None) -- [1:1]() (One To One) -- [1:Many]() (One To Many) -- [Many:None]() (Many To None) -- [Many:1]() (Many To One) -- [Many:Many]() (Many To Many) - -**Mutating Relationships** +**[Feature Overview](#feature-overview)** +- [Resource Relationships](#resource-relationships) +- [Collection Relationships](#collection-relationships) +- [Inverses](#inverses) +- [Polymorphism](#polymorphism) +- [Links vs Identifiers](#links-vs-identifiers) +- [Sync vs Async](#sync-vs-async) + +**[Configuration](#configuration)** +- [1:none](#one-to-none) (One To None) +- [1:1](#one-to-one) (One To One) +- [1:Many](#one-to-many) (One To Many) +- [Many:None](#many-to-none) (Many To None) +- [Many:1](#one-to-many) (Many To One) +- [Many:Many](#many-to-many) (Many To Many) + +**[Mutating Relationships](#mutating-relationships)** - [Adding/Removing]() - [Saving]() - [Saving Multiple Related Records At Once]() - [Sorting & Filtering]() -**Advanced** +**[Advanced](#advanced)** - [Understanding "the Graph"]() - [Pagination]() - [Directionality]() @@ -31,10 +33,139 @@ ## Feature Overview +### Resource Relationships + +### Collection Relationships + +### Inverses + +### Polymorphism + +### Links vs Identifiers + +### Sync vs Async + --- ## Configuration +### One To None + +Pretend we're building a social network for trail runners 🏃🏃🏾‍♀️, and a TrailRunner (maybe [@runspired](https://github.com/runspired)) can have a favorite Trail to run on . While the TrailRunner has a favorite trail, the trail has no concept of a TrailRunner. + +```mermaid +flowchart LR + A(🌲 TrailRunner.favoriteTrail) ==> B(⛰️ Trail) +``` + +#### Using `@ember-data/model` + +*TrailRunner* + +```ts +import Model, { belongsTo } from '@ember-data/model'; + +export default class TrailRunner extends Model { + @belongsTo('trail', { inverse: null, async: false }) + favoriteTrail; +} +``` + +*Trail* + +```ts +import Model, { attr } from '@ember-data/model'; + +export default class Trail extends Model { + @attr name; +} +``` + +#### Using JSON Schemas + +**Current** + +```json +{ + "kind": "belongsTo", + "name": "favoriteTrail", + "options": { "async": false, "inverse": null }, + "type": "trail", +} +``` + +**🚧 Coming Soon** + +Now that we've deprecated implicit option values, we're able to change defaults. +This means that the next iteration of Schema will be able to reliably use the +The lack of an option like "async" or "inverse" as a false-y value. + +We also are shifting "kind" from "belongsTo" to "resource" to make it more clear +that relationships do not (by default) have directionality or ownership over their +inverse. + +```json +{ + "kind": "resource", + "name": "favoriteTrail", + "type": "trail", +} +``` + +#### Using LegacyCompat Mode for `@warp-drive/schema-record` (🚧 Coming Soon) + +*Trail* + +```ts +import { attr } from '@warp-drive/schema'; + +export default class Trail { + @attr name: string; +} +``` + +*TrailRunner* + +```ts +import { belongsTo } from '@warp-drive/schema'; +import Trail from './trail'; + +export default class TrailRunner { + @belongsTo(Trail) favoriteTrail; +} +``` + +#### Using `@warp-drive/schema-record` (🚧 Coming Soon) + +*Trail* + +```ts +import { field } from '@warp-drive/schema'; + +export default class Trail { + @field name: string; +} +``` + +*TrailRunner* + +```ts +import { resource } from '@warp-drive/schema'; +import Trail from './trail'; + +export default class TrailRunner { + @resource(Trail) favoriteTrail; +} +``` + +### One To One + +### One To Many + +### Many To None + +### Many to Many + --- ## Mutating Relationships From 25dbf1fa07f2902326b27f459262d3b450e0c540 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Wed, 25 Oct 2023 22:48:05 -0700 Subject: [PATCH 2/8] try graph --- guides/relationships.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/relationships.md b/guides/relationships.md index 3ee2364e886..557b978d216 100644 --- a/guides/relationships.md +++ b/guides/relationships.md @@ -54,7 +54,7 @@ Pretend we're building a social network for trail runners 🏃🏃🏾‍♀️, and a TrailRunner (maybe [@runspired](https://github.com/runspired)) can have a favorite Trail to run on . While the TrailRunner has a favorite trail, the trail has no concept of a TrailRunner. ```mermaid -flowchart LR +graph LR A(🌲 TrailRunner.favoriteTrail) ==> B(⛰️ Trail) ``` From 517afb3a94965f3cf2f7a4077808fbe49f22d423 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Wed, 25 Oct 2023 22:50:12 -0700 Subject: [PATCH 3/8] another try --- guides/relationships.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/guides/relationships.md b/guides/relationships.md index 557b978d216..d466f91c06e 100644 --- a/guides/relationships.md +++ b/guides/relationships.md @@ -54,8 +54,8 @@ Pretend we're building a social network for trail runners 🏃🏃🏾‍♀️, and a TrailRunner (maybe [@runspired](https://github.com/runspired)) can have a favorite Trail to run on . While the TrailRunner has a favorite trail, the trail has no concept of a TrailRunner. ```mermaid -graph LR - A(🌲 TrailRunner.favoriteTrail) ==> B(⛰️ Trail) +graph LR; + A(🌲 TrailRunner.favoriteTrail) ==> B(⛰️ Trail); ``` #### Using `@ember-data/model` From 3abf054e4bc923a2fb014beefefd281bfb1402c5 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Wed, 25 Oct 2023 22:51:02 -0700 Subject: [PATCH 4/8] try another arrow --- guides/relationships.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/relationships.md b/guides/relationships.md index d466f91c06e..e8184dcb2ae 100644 --- a/guides/relationships.md +++ b/guides/relationships.md @@ -55,7 +55,7 @@ Pretend we're building a social network for trail runners 🏃🏃🏾‍♀️, ```mermaid graph LR; - A(🌲 TrailRunner.favoriteTrail) ==> B(⛰️ Trail); + A(🌲 TrailRunner.favoriteTrail) --> B(⛰️ Trail); ``` #### Using `@ember-data/model` From d8cff89bc87d260e3e65f7f6c129ddbab43a9679 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Wed, 25 Oct 2023 22:51:40 -0700 Subject: [PATCH 5/8] maybe no icons --- guides/relationships.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guides/relationships.md b/guides/relationships.md index e8184dcb2ae..7258db65f82 100644 --- a/guides/relationships.md +++ b/guides/relationships.md @@ -55,7 +55,7 @@ Pretend we're building a social network for trail runners 🏃🏃🏾‍♀️, ```mermaid graph LR; - A(🌲 TrailRunner.favoriteTrail) --> B(⛰️ Trail); + A(TrailRunner.favoriteTrail) --> B(Trail); ``` #### Using `@ember-data/model` From f1768e6584a75fc774f9cc865ce2f2a871cb656f Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Thu, 26 Oct 2023 00:55:00 -0700 Subject: [PATCH 6/8] refactor shape and add one-to-one docs --- guides/relationships.md | 177 ++------------- .../configuration/0-one-to-none.md | 160 +++++++++++++ .../configuration/1-one-to-one.md | 210 ++++++++++++++++++ guides/relationships/features/inverses.md | 0 4 files changed, 386 insertions(+), 161 deletions(-) create mode 100644 guides/relationships/configuration/0-one-to-none.md create mode 100644 guides/relationships/configuration/1-one-to-one.md create mode 100644 guides/relationships/features/inverses.md diff --git a/guides/relationships.md b/guides/relationships.md index 7258db65f82..600f4008545 100644 --- a/guides/relationships.md +++ b/guides/relationships.md @@ -1,175 +1,30 @@ # Relationships Guide -**[Feature Overview](#feature-overview)** -- [Resource Relationships](#resource-relationships) -- [Collection Relationships](#collection-relationships) -- [Inverses](#inverses) -- [Polymorphism](#polymorphism) -- [Links vs Identifiers](#links-vs-identifiers) -- [Sync vs Async](#sync-vs-async) +## Feature Overview +- [Resource Relationships]() +- [Collection Relationships]() +- [Inverses](./relationships/features/inverses.md) +- [Polymorphism]() +- [Links vs Identifiers]() +- [Sync vs Async]() -**[Configuration](#configuration)** -- [1:none](#one-to-none) (One To None) -- [1:1](#one-to-one) (One To One) -- [1:Many](#one-to-many) (One To Many) -- [Many:None](#many-to-none) (Many To None) -- [Many:1](#one-to-many) (Many To One) -- [Many:Many](#many-to-many) (Many To Many) +## Configuration +- [1:none](./relationships/configuration/0-one-to-none.md) (One To None) +- [1:1](./relationships/configuration/1-one-to-one.md) (One To One) +- [1:Many](./) (One To Many) +- [Many:None]() (Many To None) +- [Many:1]() (Many To One) +- [Many:Many]() (Many To Many) -**[Mutating Relationships](#mutating-relationships)** +## Mutating Relationships - [Adding/Removing]() - [Saving]() - [Saving Multiple Related Records At Once]() - [Sorting & Filtering]() -**[Advanced](#advanced)** +## Advanced - [Understanding "the Graph"]() - [Pagination]() - [Directionality]() - [Compound Foreign Keys]() - [Joins]() - ---- - -## Feature Overview - -### Resource Relationships - -### Collection Relationships - -### Inverses - -### Polymorphism - -### Links vs Identifiers - -### Sync vs Async - ---- - -## Configuration - -### One To None - -Pretend we're building a social network for trail runners 🏃🏃🏾‍♀️, and a TrailRunner (maybe [@runspired](https://github.com/runspired)) can have a favorite Trail to run on . While the TrailRunner has a favorite trail, the trail has no concept of a TrailRunner. - -```mermaid -graph LR; - A(TrailRunner.favoriteTrail) --> B(Trail); -``` - -#### Using `@ember-data/model` - -*TrailRunner* - -```ts -import Model, { belongsTo } from '@ember-data/model'; - -export default class TrailRunner extends Model { - @belongsTo('trail', { inverse: null, async: false }) - favoriteTrail; -} -``` - -*Trail* - -```ts -import Model, { attr } from '@ember-data/model'; - -export default class Trail extends Model { - @attr name; -} -``` - -#### Using JSON Schemas - -**Current** - -```json -{ - "kind": "belongsTo", - "name": "favoriteTrail", - "options": { "async": false, "inverse": null }, - "type": "trail", -} -``` - -**🚧 Coming Soon** - -Now that we've deprecated implicit option values, we're able to change defaults. -This means that the next iteration of Schema will be able to reliably use the -The lack of an option like "async" or "inverse" as a false-y value. - -We also are shifting "kind" from "belongsTo" to "resource" to make it more clear -that relationships do not (by default) have directionality or ownership over their -inverse. - -```json -{ - "kind": "resource", - "name": "favoriteTrail", - "type": "trail", -} -``` - -#### Using LegacyCompat Mode for `@warp-drive/schema-record` (🚧 Coming Soon) - -*Trail* - -```ts -import { attr } from '@warp-drive/schema'; - -export default class Trail { - @attr name: string; -} -``` - -*TrailRunner* - -```ts -import { belongsTo } from '@warp-drive/schema'; -import Trail from './trail'; - -export default class TrailRunner { - @belongsTo(Trail) favoriteTrail; -} -``` - -#### Using `@warp-drive/schema-record` (🚧 Coming Soon) - -*Trail* - -```ts -import { field } from '@warp-drive/schema'; - -export default class Trail { - @field name: string; -} -``` - -*TrailRunner* - -```ts -import { resource } from '@warp-drive/schema'; -import Trail from './trail'; - -export default class TrailRunner { - @resource(Trail) favoriteTrail; -} -``` - -### One To One - -### One To Many - -### Many To None - -### Many to Many - ---- - -## Mutating Relationships - ---- - -## Advanced diff --git a/guides/relationships/configuration/0-one-to-none.md b/guides/relationships/configuration/0-one-to-none.md new file mode 100644 index 00000000000..ba3bbc5c0eb --- /dev/null +++ b/guides/relationships/configuration/0-one-to-none.md @@ -0,0 +1,160 @@ +# One To None Relationships + +Pretend we're building a social network for trail runners 🏃🏃🏾‍♀️, and a TrailRunner (maybe [@runspired](https://github.com/runspired)) can have a favorite Trail to run on . While the TrailRunner has a favorite trail, the trail has no concept of a TrailRunner. + +```mermaid +graph LR; + A(TrailRunner) -. favoriteTrail ..-> B(Trail) +``` + +Such a relationship is singular and unidirectional (it only points to one resource, and only points in one direction). +When a relationship only points in one direction, we say it has no [inverse](../features/inverses.md). + +You'll note that effectively this setup implicitly indicates a "many" relationship. A Trail "implicitly" has many runners (for whom it is their favorite trail). + +Internally, EmberData will keep track of this implicit relationship such that if the trail were to be destroyed in a landslide its deletion would result in removing it as the favoriteTrail for each associated runner. + +Implicit relationships are not available as a public API, because they represent a highly incomplete view of the data, but the book-keeping produces benefits such as +the ability to efficiently disassociate the record from relationships when it is destroyed. + +Here's how we can define such a relationship via various mechanisms. + +- [Using @ember-data/model](#using-ember-datamodel) +- [Using json schemas](#using-json-schemas) +- [🚧 Using @warp-drive/schema-record](#using-warp-driveschema-record-🚧-coming-soon) + - [Legacy Compat Mode](#legacycompat-mode) + +--- + +## Using `@ember-data/model` + +> **Note** Models are currently the primary way that users of EmberData define "schema". +> +> Models are not the only way to define schema today, but they +> are the most immediately available ergonomic way to do so. + +When using Models, EmberData parses schema from them at runtime, +converting static information defined on the class into the json +schema format needed by the rest of the system. + +This is handled by the implementation of the [schema service](https://api.emberjs.com/ember-data/release/classes/SchemaService) provided +by the `@ember-data/model` package. The service converts the class +definitions into the json definitions described in the next section. + +⛰️ *Trail* + +```ts +import Model, { attr } from '@ember-data/model'; + +export default class Trail extends Model { + @attr name; +} +``` + +🌲 *TrailRunner* + +```ts +import Model, { belongsTo } from '@ember-data/model'; + +export default class TrailRunner extends Model { + @belongsTo('trail', { inverse: null, async: false }) + favoriteTrail; +} +``` + +--- + +## Using JSON Schemas + +EmberData doesn't care where your schemas come from, how they are authored, +or how you load them into the system so long as when it asks the [schema service](https://api.emberjs.com/ember-data/release/classes/SchemaService) +for information it gets back field definitions in the right json shape. + +Here, we show how the above trail runner relationship is described by a field definition. + +**Current** + +```json +{ + "kind": "belongsTo", + "name": "favoriteTrail", + "options": { "async": false, "inverse": null }, + "type": "trail", +} +``` + +**🚧 Coming Soon** + +Because we deprecated implicit option values in 4.x, we are now able to change defaults. + +This means that the next iteration of Schema will be able to reliably use +the The lack of an option like "async" or "inverse" as a false-y value. + +We also are shifting the value for "kind" from "belongsTo" to "resource" +to make it more readil clear that relationships do not (by default) have +directionality or ownership over their inverse. + +```json +{ + "kind": "resource", + "name": "favoriteTrail", + "type": "trail", +} +``` + +--- + +## Using `@warp-drive/schema-record` (🚧 Coming Soon) + +Working with schemas in a raw json format is far more flexible, lightweight and +performant than working with bulky classes that need to be shipped across the wire,parsed, and instantiated. Even relatively small apps can quickly find themselves shipping large quantities of JS just to describe their data. + +No one wants to author schemas in raw JSON though (we hope 😬), and the ergonomics of typed data and editor autocomplete based on your schemas are vital to productivity and +code quality. For this, we offer a way to express schemas as typescript using types, classes and decorators which are then compiled into json schemas and typescript interfaces for use by your project. + +⛰️ *Trail* + +```ts +import { field } from '@warp-drive/schema'; + +export class Trail { + @field name: string; +} +``` + +🌲 *TrailRunner* + +```ts +import { resource } from '@warp-drive/schema'; +import { Trail } from './trail'; + +export class TrailRunner { + @resource(Trail) favoriteTrail; +} +``` + +### LegacyCompat Mode + +Support for migrating from `@ember-data/model` on a more granular basis is provided by decorators that preserve the semantics of the quirks of that class. This allows you to begin eliminating models +and adopting other features of schemas sooner. + +⛰️ *Trail* + +```ts +import { attr } from '@warp-drive/schema/legacy'; + +export default class Trail { + @attr name: string; +} +``` + +🌲 *TrailRunner* + +```ts +import { belongsTo } from '@warp-drive/schema/legacy'; +import Trail from './trail'; + +export default class TrailRunner { + @belongsTo(Trail) favoriteTrail; +} +``` diff --git a/guides/relationships/configuration/1-one-to-one.md b/guides/relationships/configuration/1-one-to-one.md new file mode 100644 index 00000000000..71d79c97f3e --- /dev/null +++ b/guides/relationships/configuration/1-one-to-one.md @@ -0,0 +1,210 @@ +# One To One Relationships + +Imagine our social network for trail runners 🏃🏃🏾‍♀️ allows runners to add their other social accounts. For instance, the TrailRunner [@runspired](https://github.com/runspired) might add their Instagram account. + +Here, the coupling goes both ways. In this model, the 📸 Instagram account can only belong to one Trail Runner, and the Trail Runner only has one Instagram Account. Let's take a selfie: + +```mermaid +graph LR; + A(TrailRunner) -. instagram ..-> B(InstagramAccount) + B -. runner .-> A +``` + +There are two ways we can model this relationship: bidirectionally with managed [inverses](../features/inverses.md), or unidirectionally without managed inverses. + +In the bidirectional configuration, changes to one side of the relationship change the other side as well. This includes +both updates from remote state (a payload for the resource received from the API) as well as mutations to the local state +(application code setting a new, unsaved value for the relationship). + +```mermaid +graph LR; + A(TrailRunner.instagram) <==> B(InstagramAccount.runner) +``` + +In the unidirectional configuration, we effectively have two separate distinct [one-to-none](./0-one-to-none.md) relationships. + +```mermaid +graph LR; + A(TrailRunner) -. instagram .-> B(InstagramAccount) +``` + +```mermaid +graph LR; + A(InstagramAccount) -. runner .-> B(TrailRunner) +``` + +With distinct relationships, we may edit one side without affecting the state of the inverse. + +Note, modeling this setup as two "one-to-none" relationships has the advantage of creating an implicit "many" relationship in both directions. Imagine that many runners could have the same instagram account and that at the same time many instagram accounts could belong to the same runner. + +You might be tempted to think of this as a `many-to-many`, but more often this is effectively modeled as two `one-to-none` relationships. + +Head over to [one-to-none](./0-one-to-none.md) if this is the setup that is best for you. Else, here's how we can define such a relationship via various mechanisms. + +- [Using @ember-data/model](#using-ember-datamodel) +- [Using json schemas](#using-json-schemas) +- [🚧 Using @warp-drive/schema-record](#using-warp-driveschema-record-🚧-coming-soon) + - [Legacy Compat Mode](#legacycompat-mode) + +--- + +## Using `@ember-data/model` + +> **Note** Models are currently the primary way that users of EmberData define "schema". +> +> Models are not the only way to define schema today, but they +> are the most immediately available ergonomic way to do so. + +When using Models, EmberData parses schema from them at runtime, +converting static information defined on the class into the json +schema format needed by the rest of the system. + +This is handled by the implementation of the [schema service](https://api.emberjs.com/ember-data/release/classes/SchemaService) provided +by the `@ember-data/model` package. The service converts the class +definitions into the json definitions described in the next section. + +📸 *InstagramAccount* + +```ts +import Model, { belongsTo } from '@ember-data/model'; + +export default class InstagramAccount extends Model { + @belongsTo('trail-runner', { inverse: 'instagram', async: false }) + runner; +} +``` + +🌲 *TrailRunner* + +```ts +import Model, { belongsTo } from '@ember-data/model'; + +export default class TrailRunner extends Model { + @belongsTo('instagram-account', { inverse: 'runner', async: false }) + instagram; +} +``` + +--- + +## Using JSON Schemas + +EmberData doesn't care where your schemas come from, how they are authored, +or how you load them into the system so long as when it asks the [schema service](https://api.emberjs.com/ember-data/release/classes/SchemaService) +for information it gets back field definitions in the right json shape. + +Here, we show how the above trail runner relationship is described by a field definition. + +**Current** + +📸 *InstagramAccount* + +```json +{ + "kind": "belongsTo", + "name": "runner", + "options": { "async": false, "inverse": "instagram" }, + "type": "trail-runner", +} +``` + +🌲 *TrailRunner* + +```json +{ + "kind": "belongsTo", + "name": "instagram", + "options": { "async": false, "inverse": "runner" }, + "type": "instagram-account", +} +``` + +**🚧 Coming Soon** + +Because we deprecated implicit option values in 4.x, we are now able to change defaults. + +This means that the next iteration of Schema will be able to reliably use +the The lack of an option like "async" or "inverse" as a false-y value. + +We also are shifting the value for "kind" from "belongsTo" to "resource" +to make it more readil clear that relationships do not (by default) have +directionality or ownership over their inverse. + +📸 *InstagramAccount* + +```json +{ + "kind": "resource", + "name": "runner", + "type": "trail-runner", +} +``` + +🌲 *TrailRunner* + +```json +{ + "kind": "resource", + "name": "instagram", + "type": "instagram-account", +} +``` + +--- + +## Using `@warp-drive/schema-record` (🚧 Coming Soon) + +Working with schemas in a raw json format is far more flexible, lightweight and +performant than working with bulky classes that need to be shipped across the wire,parsed, and instantiated. Even relatively small apps can quickly find themselves shipping large quantities of JS just to describe their data. + +No one wants to author schemas in raw JSON though (we hope 😬), and the ergonomics of typed data and editor autocomplete based on your schemas are vital to productivity and +code quality. For this, we offer a way to express schemas as typescript using types, classes and decorators which are then compiled into json schemas and typescript interfaces for use by your project. + +📸 *InstagramAccount* + +```ts +import { resource } from '@warp-drive/schema'; +import { TrailRunner } from './trail-runner'; + +export class InstagramAccount { + @resource(TrailRunner) runner; +} +``` + +🌲 *TrailRunner* + +```ts +import { resource } from '@warp-drive/schema'; +import { InstagramAccount } from './instagram-account'; + +export class TrailRunner { + @resource(InstagramAccount) instagram; +} +``` + +### LegacyCompat Mode + +Support for migrating from `@ember-data/model` on a more granular basis is provided by decorators that preserve the semantics of the quirks of that class. This allows you to begin eliminating models +and adopting other features of schemas sooner. + +📸 *InstagramAccount* + +```ts +import { belongsTo } from '@warp-drive/schema/legacy'; +import { TrailRunner } from './trail-runner'; + +export class InstagramAccount { + @belongsTo(TrailRunner) runner; +} +``` + +🌲 *TrailRunner* + +```ts +import { belongsTo } from '@warp-drive/schema/legacy'; +import { InstagramAccount } from './instagram-account'; + +export class TrailRunner { + @belongsTo(InstagramAccount) instagram; +} +``` diff --git a/guides/relationships/features/inverses.md b/guides/relationships/features/inverses.md new file mode 100644 index 00000000000..e69de29bb2d From a955049fc6facf3fc53c19091334fb8cbad4ce6f Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Thu, 26 Oct 2023 01:40:48 -0700 Subject: [PATCH 7/8] more guides and a few stubs and links --- guides/relationships.md | 14 +- .../configuration/0-one-to-none.md | 12 + .../configuration/1-one-to-one.md | 24 +- .../configuration/2-one-to-many.md | 246 ++++++++++++++++++ .../configuration/3-many-to-none.md | 11 + .../configuration/4-many-to-one.md | 10 + .../configuration/5-many-to-many.md | 10 + guides/relationships/features/inverses.md | 9 + 8 files changed, 328 insertions(+), 8 deletions(-) create mode 100644 guides/relationships/configuration/2-one-to-many.md create mode 100644 guides/relationships/configuration/3-many-to-none.md create mode 100644 guides/relationships/configuration/4-many-to-one.md create mode 100644 guides/relationships/configuration/5-many-to-many.md diff --git a/guides/relationships.md b/guides/relationships.md index 600f4008545..8ca33fb5e37 100644 --- a/guides/relationships.md +++ b/guides/relationships.md @@ -1,21 +1,24 @@ # Relationships Guide ## Feature Overview +- [Inverses](./relationships/features/inverses.md) + ## Configuration - [1:none](./relationships/configuration/0-one-to-none.md) (One To None) - [1:1](./relationships/configuration/1-one-to-one.md) (One To One) -- [1:Many](./) (One To Many) -- [Many:None]() (Many To None) -- [Many:1]() (Many To One) -- [Many:Many]() (Many To Many) +- [1:Many](./relationships/configuration/2-one-to-many.md) (One To Many) +- [Many:None](./relationships/configuration/3-many-to-none.md) (Many To None) +- [Many:1](./relationships/configuration/4-many-to-one.md) (Many To One) +- [Many:Many](./relationships/configuration/5-many-to-many.md) (Many To Many) + diff --git a/guides/relationships/configuration/0-one-to-none.md b/guides/relationships/configuration/0-one-to-none.md index ba3bbc5c0eb..335269bab3c 100644 --- a/guides/relationships/configuration/0-one-to-none.md +++ b/guides/relationships/configuration/0-one-to-none.md @@ -1,5 +1,10 @@ # One To None Relationships +- Next → [One To One Relationships](./1-one-to-one.md) +- ⮐ Go Back [Relationships Guide](../../relationships.md) + +--- + Pretend we're building a social network for trail runners 🏃🏃🏾‍♀️, and a TrailRunner (maybe [@runspired](https://github.com/runspired)) can have a favorite Trail to run on . While the TrailRunner has a favorite trail, the trail has no concept of a TrailRunner. ```mermaid @@ -7,6 +12,8 @@ graph LR; A(TrailRunner) -. favoriteTrail ..-> B(Trail) ``` +> **Note** In our charts we use dotted lines for singular relationships and thick solid lines for collection relationships. + Such a relationship is singular and unidirectional (it only points to one resource, and only points in one direction). When a relationship only points in one direction, we say it has no [inverse](../features/inverses.md). @@ -158,3 +165,8 @@ export default class TrailRunner { @belongsTo(Trail) favoriteTrail; } ``` + +--- + +- Next → [One To One Relationships](./1-one-to-one.md) +- ⮐ [Relationships Guide](../../relationships.md) diff --git a/guides/relationships/configuration/1-one-to-one.md b/guides/relationships/configuration/1-one-to-one.md index 71d79c97f3e..0ef8e237d68 100644 --- a/guides/relationships/configuration/1-one-to-one.md +++ b/guides/relationships/configuration/1-one-to-one.md @@ -1,5 +1,11 @@ # One To One Relationships +- Previous ← [One To None Relationships](./0-one-to-none.md) +- Next → [One To Many Relationships](./2-one-to-many.md) +- ⮐ [Relationships Guide](../../relationships.md) + +--- + Imagine our social network for trail runners 🏃🏃🏾‍♀️ allows runners to add their other social accounts. For instance, the TrailRunner [@runspired](https://github.com/runspired) might add their Instagram account. Here, the coupling goes both ways. In this model, the 📸 Instagram account can only belong to one Trail Runner, and the Trail Runner only has one Instagram Account. Let's take a selfie: @@ -18,9 +24,11 @@ both updates from remote state (a payload for the resource received from the API ```mermaid graph LR; - A(TrailRunner.instagram) <==> B(InstagramAccount.runner) + A(TrailRunner.instagram) <-.-> B(InstagramAccount.runner) ``` +> **Note** In our charts we use dotted lines for singular relationships and thick solid lines for collection relationships. + In the unidirectional configuration, we effectively have two separate distinct [one-to-none](./0-one-to-none.md) relationships. ```mermaid @@ -136,6 +144,7 @@ directionality or ownership over their inverse. { "kind": "resource", "name": "runner", + "options": { "inverse": "instagram" }, "type": "trail-runner", } ``` @@ -146,6 +155,7 @@ directionality or ownership over their inverse. { "kind": "resource", "name": "instagram", + "options": { "inverse": "runner" }, "type": "instagram-account", } ``` @@ -194,7 +204,8 @@ import { belongsTo } from '@warp-drive/schema/legacy'; import { TrailRunner } from './trail-runner'; export class InstagramAccount { - @belongsTo(TrailRunner) runner; + @belongsTo(TrailRunner, { inverse: "instagram" }) + runner; } ``` @@ -205,6 +216,13 @@ import { belongsTo } from '@warp-drive/schema/legacy'; import { InstagramAccount } from './instagram-account'; export class TrailRunner { - @belongsTo(InstagramAccount) instagram; + @belongsTo(InstagramAccount, { inverse: "reunnter" }) + instagram; } ``` + +--- + +- Previous ← [One To None Relationships](./0-one-to-none.md) +- Next → [One To Many Relationships](./2-one-to-many.md) +- ⮐ [Relationships Guide](../../relationships.md) diff --git a/guides/relationships/configuration/2-one-to-many.md b/guides/relationships/configuration/2-one-to-many.md new file mode 100644 index 00000000000..c63f1017bf3 --- /dev/null +++ b/guides/relationships/configuration/2-one-to-many.md @@ -0,0 +1,246 @@ +# One To Many Relationships + +- Previous ← [One To One Relationships](./1-one-to-one.md) +- Next → [Many To None Relationships](./3-many-to-none.md) +- ⮐ [Relationships Guide](../../relationships.md) + +--- + +Imagine our social network for trail runners 🏃🏃🏾‍♀️ allows runners to upload their runs as activities. + +In this model, the ActivityData only pertains on one TrailRunner + +```mermaid +graph LR; + A(ActivityData) -. runner ..-> B(TrailRunner) +``` + +While the Trail Runner has many such activies. + +```mermaid +graph LR; + A(TrailRunner) == activities ==> B(ActivityData) + A(TrailRunner) == activities ==> C(ActivityData) + A(TrailRunner) == activities ==> D(ActivityData) +``` + +> **Note** In our charts we use dotted lines for singular relationships and thick solid lines for collection relationships. + +Let's workout! + +There are two ways we can model this relationship: bidirectionally with managed [inverses](../features/inverses.md), or unidirectionally without managed inverses. + +In the bidirectional configuration, changes to one side of the relationship change the other side as well. This includes +both updates from remote state (a payload for the resource received from the API) as well as mutations to the local state +(application code setting a new, unsaved value for the relationship). + +```mermaid +graph LR; + A(TrailRunner.activities) ==> B(ActivityData.runner) + B -.-> A +``` + +In the unidirectional configuration, we effectively have two separate distinct relationships. + +A [many-to-none](./4-many-to-none.md) relationship from TrailRunner to ActivityData. + +```mermaid +graph LR; + A(TrailRunner) == activities ==> B(ActivityData) +``` + +And a [one-to-none](./0-one-to-none.md) relationship from ActivityData to TrailRunner. + +```mermaid +graph LR; + A(ActivityData) -. runner .-> B(TrailRunner) +``` + +With distinct relationships, we may edit one side without affecting the state of the inverse. This is particularly useful +in two situations. + +First, it may be the case that the user has thousands or tens of thousands of activities. In this case, you likely don't want whichever individual activities you happen to load to create an incomplete list of the TrailRunner's activities. It's better to load and work with the activities list in isolation, ideally in a paginated manner. + +Second, it may be the case that runner is able to share the activity data with another runner that forgot to record. By not coupling the relationship, the ActivityData can still be owned by the first runner by included in the second runner's list of activities as well. + +Head over to [many-to-none](./4-many-to-none.md) and [one-to-none](./0-one-to-none.md) if this is the setup that is best for you. Else, here's how we can define such a relationship via various mechanisms. + +- [Using @ember-data/model](#using-ember-datamodel) +- [Using json schemas](#using-json-schemas) +- [🚧 Using @warp-drive/schema-record](#using-warp-driveschema-record-🚧-coming-soon) + - [Legacy Compat Mode](#legacycompat-mode) + +--- + +## Using `@ember-data/model` + +> **Note** Models are currently the primary way that users of EmberData define "schema". +> +> Models are not the only way to define schema today, but they +> are the most immediately available ergonomic way to do so. + +When using Models, EmberData parses schema from them at runtime, +converting static information defined on the class into the json +schema format needed by the rest of the system. + +This is handled by the implementation of the [schema service](https://api.emberjs.com/ember-data/release/classes/SchemaService) provided +by the `@ember-data/model` package. The service converts the class +definitions into the json definitions described in the next section. + +🏃🏾‍♀️ *ActivityData* + +```ts +import Model, { belongsTo } from '@ember-data/model'; + +export default class ActivityData extends Model { + @belongsTo('trail-runner', { inverse: 'activities', async: false }) + runner; +} +``` + +🌲 *TrailRunner* + +```ts +import Model, { hasMany } from '@ember-data/model'; + +export default class TrailRunner extends Model { + @hasMany('activity-data', { inverse: 'runner', async: false }) + activities; +} +``` + +--- + +## Using JSON Schemas + +EmberData doesn't care where your schemas come from, how they are authored, +or how you load them into the system so long as when it asks the [schema service](https://api.emberjs.com/ember-data/release/classes/SchemaService) +for information it gets back field definitions in the right json shape. + +Here, we show how the above trail runner relationship is described by a field definition. + +**Current** + +🏃🏾‍♀️ *ActivityData* + +```json +{ + "kind": "belongsTo", + "name": "runner", + "options": { "async": false, "inverse": "activities" }, + "type": "trail-runner", +} +``` + +🌲 *TrailRunner* + +```json +{ + "kind": "hasMany", + "name": "activities", + "options": { "async": false, "inverse": "runner" }, + "type": "activity-data", +} +``` + +**🚧 Coming Soon** + +Because we deprecated implicit option values in 4.x, we are now able to change defaults. + +This means that the next iteration of Schema will be able to reliably use +the The lack of an option like "async" or "inverse" as a false-y value. + +We also are shifting the value for "kind" from "belongsTo" to "resource" +to make it more readil clear that relationships do not (by default) have +directionality or ownership over their inverse. + +🏃🏾‍♀️ *ActivityData* + +```json +{ + "kind": "resource", + "name": "runner", + "options": { "inverse": "activities" }, + "type": "trail-runner", +} +``` + +🌲 *TrailRunner* + +```json +{ + "kind": "collection", + "name": "activities", + "options": { "inverse": "runner" }, + "type": "activity-data", +} +``` + +--- + +## Using `@warp-drive/schema-record` (🚧 Coming Soon) + +Working with schemas in a raw json format is far more flexible, lightweight and +performant than working with bulky classes that need to be shipped across the wire,parsed, and instantiated. Even relatively small apps can quickly find themselves shipping large quantities of JS just to describe their data. + +No one wants to author schemas in raw JSON though (we hope 😬), and the ergonomics of typed data and editor autocomplete based on your schemas are vital to productivity and +code quality. For this, we offer a way to express schemas as typescript using types, classes and decorators which are then compiled into json schemas and typescript interfaces for use by your project. + +🏃🏾‍♀️ *ActivityData* + +```ts +import { resource } from '@warp-drive/schema'; +import { TrailRunner } from './trail-runner'; + +export class ActivityData { + @resource(TrailRunner, { inverse: "activities" }) + runner; +} +``` + +🌲 *TrailRunner* + +```ts +import { collection } from '@warp-drive/schema'; +import { ActivityData } from './activity-data'; + +export class TrailRunner { + @resource(ActivityData, { inverse: "runner" }) + activities; +} +``` + +### LegacyCompat Mode + +Support for migrating from `@ember-data/model` on a more granular basis is provided by decorators that preserve the semantics of the quirks of that class. This allows you to begin eliminating models +and adopting other features of schemas sooner. + +🏃🏾‍♀️ *ActivityData* + +```ts +import { belongsTo } from '@warp-drive/schema/legacy'; +import { TrailRunner } from './trail-runner'; + +export class ActivityData { + @belongsTo(TrailRunner, { inverse: "activities" }) + runner; +} +``` + +🌲 *TrailRunner* + +```ts +import { hasMany } from '@warp-drive/schema/legacy'; +import { ActivityData } from './activity-data'; + +export class TrailRunner { + @hasMany(ActivityData, { inverse: "runner" }) + activities; +} +``` + +--- + +- Previous ← [One To One Relationships](./1-one-to-one.md) +- Next → [Many To None Relationships](./3-many-to-none.md) +- ⮐ [Relationships Guide](../../relationships.md) diff --git a/guides/relationships/configuration/3-many-to-none.md b/guides/relationships/configuration/3-many-to-none.md new file mode 100644 index 00000000000..362f118afcd --- /dev/null +++ b/guides/relationships/configuration/3-many-to-none.md @@ -0,0 +1,11 @@ +# Many To None Relationships + +- Previous ← [One To Many Relationships](./2-one-to-many.md) +- Next → [Many To One Relationships](./4-many-to-one.md) +- ⮐ [Relationships Guide](../../relationships.md) + +--- + +## 🚧 Under Construction 🚧 + +🔜 Check back soon! diff --git a/guides/relationships/configuration/4-many-to-one.md b/guides/relationships/configuration/4-many-to-one.md new file mode 100644 index 00000000000..93582f60800 --- /dev/null +++ b/guides/relationships/configuration/4-many-to-one.md @@ -0,0 +1,10 @@ +# Many To One Relationships + +- Previous ← [Many To None Relationships](./3-many-to-none.md) +- Next → [Many To Many Relationships](./5-many-to-many.md) +- ⮐ [Relationships Guide](../../relationships.md) + +--- + +Many To One Relationships are the same as [One to Many relationships](./2-one-to-many.md). Just flip the order in +how you conceptualize the relationship. diff --git a/guides/relationships/configuration/5-many-to-many.md b/guides/relationships/configuration/5-many-to-many.md new file mode 100644 index 00000000000..605823ee895 --- /dev/null +++ b/guides/relationships/configuration/5-many-to-many.md @@ -0,0 +1,10 @@ +# Many To Many Relationships + +- Previous ← [Many To One Relationships](./4-one-to-many.md) +- ⮐ [Relationships Guide](../../relationships.md) + +--- + +## 🚧 Under Construction 🚧 + +🔜 Check back soon! diff --git a/guides/relationships/features/inverses.md b/guides/relationships/features/inverses.md index e69de29bb2d..557c5c3d691 100644 --- a/guides/relationships/features/inverses.md +++ b/guides/relationships/features/inverses.md @@ -0,0 +1,9 @@ +# Relationship Inverses + +- ⮐ [Relationships Guide](../../relationships.md) + +--- + +## 🚧 Under Construction 🚧 + +🔜 Check back soon! From cb7f566b223e87a473675e15fec6405f3d292dc7 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Thu, 26 Oct 2023 03:05:37 -0700 Subject: [PATCH 8/8] finish out the configuration section --- guides/relationships.md | 4 + .../configuration/1-one-to-one.md | 17 +- .../configuration/2-one-to-many.md | 2 +- .../configuration/3-many-to-none.md | 164 ++++++++++++- .../configuration/5-many-to-many.md | 218 +++++++++++++++++- guides/relationships/terminology.md | 36 +++ 6 files changed, 435 insertions(+), 6 deletions(-) create mode 100644 guides/relationships/terminology.md diff --git a/guides/relationships.md b/guides/relationships.md index 8ca33fb5e37..066a8aeebc2 100644 --- a/guides/relationships.md +++ b/guides/relationships.md @@ -32,3 +32,7 @@ - [Compound Foreign Keys]() - [Joins]() --> + +# Misc + +- [Terminology](./relationships/terminology.md) diff --git a/guides/relationships/configuration/1-one-to-one.md b/guides/relationships/configuration/1-one-to-one.md index 0ef8e237d68..3b3da9b1ef6 100644 --- a/guides/relationships/configuration/1-one-to-one.md +++ b/guides/relationships/configuration/1-one-to-one.md @@ -45,7 +45,22 @@ With distinct relationships, we may edit one side without affecting the state of Note, modeling this setup as two "one-to-none" relationships has the advantage of creating an implicit "many" relationship in both directions. Imagine that many runners could have the same instagram account and that at the same time many instagram accounts could belong to the same runner. -You might be tempted to think of this as a `many-to-many`, but more often this is effectively modeled as two `one-to-none` relationships. +You might be tempted to think of this as a `many-to-many` or two [many-to-none](./4-many-to-one.md)s, but sometimes this is effectively modeled as two `one-to-none` relationships. + +```mermaid +graph LR; + A(TrailRunner1) -. account .-> D(InstagramAccount1) + B(TrailRunner2) -. account .-> D(InstagramAccount1) + C(TrailRunner3) -. account .-> D(InstagramAccount1) +``` + +```mermaid +graph LR; + A(InstagramAccount1) -. runner .-> D(TrailRunner1) + B(InstagramAccount2) -. runner .-> D(TrailRunner1) + C(InstagramAccount3) -. runner .-> D(TrailRunner1) +``` + Head over to [one-to-none](./0-one-to-none.md) if this is the setup that is best for you. Else, here's how we can define such a relationship via various mechanisms. diff --git a/guides/relationships/configuration/2-one-to-many.md b/guides/relationships/configuration/2-one-to-many.md index c63f1017bf3..41aa5782ac9 100644 --- a/guides/relationships/configuration/2-one-to-many.md +++ b/guides/relationships/configuration/2-one-to-many.md @@ -205,7 +205,7 @@ import { collection } from '@warp-drive/schema'; import { ActivityData } from './activity-data'; export class TrailRunner { - @resource(ActivityData, { inverse: "runner" }) + @collection(ActivityData, { inverse: "runner" }) activities; } ``` diff --git a/guides/relationships/configuration/3-many-to-none.md b/guides/relationships/configuration/3-many-to-none.md index 362f118afcd..b6297e19571 100644 --- a/guides/relationships/configuration/3-many-to-none.md +++ b/guides/relationships/configuration/3-many-to-none.md @@ -6,6 +6,166 @@ --- -## 🚧 Under Construction 🚧 +Imagine our social network for trail runners 🏃🏃🏾‍♀️ allows runners to tag their activities. [#runday](https://www.instagram.com/explore/tags/runday/?hl=en) [#justdoit](https://www.instagram.com/explore/tags/justdoit/?hl=en) -🔜 Check back soon! +In this model, the ActivityData might have multiple tags, but given that millions if not billions if not trillions of activities might use a tag like [#neverstopexploring](https://www.instagram.com/explore/tags/neverstopexploring/?hl=en), it turns out we definitely don't want the tag to keep track of every activity that ever referenced it. + +```mermaid +graph LR; + A(ActivityData) == tags ==> B(Hashtag) +``` + +> **Note** In our charts we use dotted lines for singular relationships and thick solid lines for collection relationships. + +Often `ManyToNone` is used for exactly this sort of case, where conceptually the relationship is [many-to-many](./5-many-to-many.md) in nature, but one side would be so large that modeling it as such is prohibitive. + +Here's how we can define such a relationship via various mechanisms. + +- [Using @ember-data/model](#using-ember-datamodel) +- [Using json schemas](#using-json-schemas) +- [🚧 Using @warp-drive/schema-record](#using-warp-driveschema-record-🚧-coming-soon) + - [Legacy Compat Mode](#legacycompat-mode) + +--- + +## Using `@ember-data/model` + +> **Note** Models are currently the primary way that users of EmberData define "schema". +> +> Models are not the only way to define schema today, but they +> are the most immediately available ergonomic way to do so. + +When using Models, EmberData parses schema from them at runtime, +converting static information defined on the class into the json +schema format needed by the rest of the system. + +This is handled by the implementation of the [schema service](https://api.emberjs.com/ember-data/release/classes/SchemaService) provided +by the `@ember-data/model` package. The service converts the class +definitions into the json definitions described in the next section. + +🏷️ *Hashtag* + +```ts +import Model, { attr } from '@ember-data/model'; + +export default class Hashtag extends Model { + @attr name; +} +``` + +🏃🏾‍♀️ *ActivityData* + +```ts +import Model, { hasMany } from '@ember-data/model'; + +export default class ActivityData extends Model { + @hasMany('hashtag', { async: false, inverse: null }) + tags; +} +``` + +--- + +## Using JSON Schemas + +EmberData doesn't care where your schemas come from, how they are authored, +or how you load them into the system so long as when it asks the [schema service](https://api.emberjs.com/ember-data/release/classes/SchemaService) +for information it gets back field definitions in the right json shape. + +Here, we show how the above trail runner relationship is described by a field definition. + +**Current** + +🏃🏾‍♀️ *ActivityData* + +```json +{ + "kind": "hasMany", + "name": "tags", + "options": { "async": false, "inverse": null }, + "type": "hashtag", +} +``` + +**🚧 Coming Soon** + +Because we deprecated implicit option values in 4.x, we are now able to change defaults. + +This means that the next iteration of Schema will be able to reliably use +the The lack of an option like "async" or "inverse" as a false-y value. + +We also are shifting the value for "kind" from "belongsTo" to "resource" +to make it more readil clear that relationships do not (by default) have +directionality or ownership over their inverse. + +🏃🏾‍♀️ *ActivityData* + +```json +{ + "kind": "collection", + "name": "tags", + "type": "hashtag", +} +``` + +--- + +## Using `@warp-drive/schema-record` (🚧 Coming Soon) + +Working with schemas in a raw json format is far more flexible, lightweight and +performant than working with bulky classes that need to be shipped across the wire,parsed, and instantiated. Even relatively small apps can quickly find themselves shipping large quantities of JS just to describe their data. + +No one wants to author schemas in raw JSON though (we hope 😬), and the ergonomics of typed data and editor autocomplete based on your schemas are vital to productivity and +code quality. For this, we offer a way to express schemas as typescript using types, classes and decorators which are then compiled into json schemas and typescript interfaces for use by your project. + +🏷️ *Hashtag* + +```ts +import { field } from '@warp-drive/schema'; + +export class Hashtag extends Model { + @field name: string; +} +``` + +🏃🏾‍♀️ *ActivityData* + +```ts +import { collection } from '@warp-drive/schema'; +import { Hashtag } from './hashtag'; + +export class ActivityData extends Model { + @collection(Hashtag) tags; +} +``` + +### LegacyCompat Mode + +Support for migrating from `@ember-data/model` on a more granular basis is provided by decorators that preserve the semantics of the quirks of that class. This allows you to begin eliminating models +and adopting other features of schemas sooner. + +🏷️ *Hashtag* + +```ts +import { attr } from '@warp-drive/schema/legacy'; + +export class Hashtag extends Model { + @attr name: string; +} +``` + +🏃🏾‍♀️ *ActivityData* + +```ts +import { hasMany } from '@warp-drive/schema/legacy'; +import { Hashtag } from './hashtag'; + +export class ActivityData extends Model { + @hasMany(Hashtag) tags; +} + +--- + +- Previous ← [One To Many Relationships](./2-one-to-many.md) +- Next → [Many To One Relationships](./4-many-to-one.md) +- ⮐ [Relationships Guide](../../relationships.md) diff --git a/guides/relationships/configuration/5-many-to-many.md b/guides/relationships/configuration/5-many-to-many.md index 605823ee895..9c67a2d048f 100644 --- a/guides/relationships/configuration/5-many-to-many.md +++ b/guides/relationships/configuration/5-many-to-many.md @@ -5,6 +5,220 @@ --- -## 🚧 Under Construction 🚧 +Imagine our social network for trail runners 🏃🏃🏾‍♀️ allows runners to connect with friends, other trail runners! -🔜 Check back soon! +```mermaid +graph LR; + A(TrailRunner) <== friends ==> B(TrailRunner) +``` + +> **Note** In our charts we use dotted lines for singular relationships and thick solid lines for collection relationships. + +Or, maybe more accurately since this is a [*reflexive*](../terminology.md#reflexive) relationship: + +```mermaid +graph LR; + A(TrailRunner) <== friends ==> A +``` + +There are two ways we can model this relationship: bidirectionally with managed [inverses](../features/inverses.md), or unidirectionally without managed inverses. + +In the bidirectional configuration, changes to one side of the relationship change the other side as well. This includes +both updates from remote state (a payload for the resource received from the API) as well as mutations to the local state +(application code setting a new, unsaved value for the relationship). + +```mermaid +graph LR; + A(TrailRunnerA.friends) <==> B(TrailRunnerB.friends) +``` + +In the unidirectional configuration, we effectively have two separate distinct [many-to-none](./3-many-to-none.md) relationships. + +```mermaid +graph LR; + A(TrailRunnerA) == friends ==> B(TrailRunnerB) +``` + +```mermaid +graph LR; + A(TrailRunnerB) == friends ==> B(TrailRunnerA) +``` + +With distinct relationships, we may edit one side without affecting the state of the inverse. This is especially useful +when the collections might be very large, paginated, or not +bidirectional in nature. + +For an example of a non-bidirectional relationship of this sort, it might be that Chris lists Thomas as a friend, but sadly Thomas does not feel the same. This Thomas being in Chris' friends does not mean that Chris should be in the list of Thomas' friends. + +Head over to [many-to-none](./3-many-to-none.md) if this is the setup that is best for you. Else, here's how we can define such a relationship via various mechanisms. + +- [Using @ember-data/model](#using-ember-datamodel) +- [Using json schemas](#using-json-schemas) +- [🚧 Using @warp-drive/schema-record](#using-warp-driveschema-record-🚧-coming-soon) + - [Legacy Compat Mode](#legacycompat-mode) + +--- + +## Using `@ember-data/model` + +> **Note** Models are currently the primary way that users of EmberData define "schema". +> +> Models are not the only way to define schema today, but they +> are the most immediately available ergonomic way to do so. + +When using Models, EmberData parses schema from them at runtime, +converting static information defined on the class into the json +schema format needed by the rest of the system. + +This is handled by the implementation of the [schema service](https://api.emberjs.com/ember-data/release/classes/SchemaService) provided +by the `@ember-data/model` package. The service converts the class +definitions into the json definitions described in the next section. + +🌲 *TrailRunner* + +```ts +import Model, { hasMany } from '@ember-data/model'; + +export default class TrailRunner extends Model { + @hasMany('trail-runner', { inverse: 'friends', async: false }) + friends; +} +``` + +Note, the [many-to-none](./3-many-to-none.md) variation of this would be: + +```ts +import Model, { hasMany } from '@ember-data/model'; + +export default class TrailRunner extends Model { + @hasMany('trail-runner', { inverse: null, async: false }) + friends; +} +``` + +--- + +## Using JSON Schemas + +EmberData doesn't care where your schemas come from, how they are authored, +or how you load them into the system so long as when it asks the [schema service](https://api.emberjs.com/ember-data/release/classes/SchemaService) +for information it gets back field definitions in the right json shape. + +Here, we show how the above trail runner relationship is described by a field definition. + +**Current** + +🌲 *TrailRunner* + +```json +{ + "kind": "hasMany", + "name": "friends", + "options": { "async": false, "inverse": "friends" }, + "type": "trail-runner", +} +``` + +Note, the [many-to-none](./3-many-to-none.md) variation of this would be: + +```json +{ + "kind": "hasMany", + "name": "friends", + "options": { "async": false, "inverse": null }, + "type": "trail-runner", +} +``` + +**🚧 Coming Soon** + +Because we deprecated implicit option values in 4.x, we are now able to change defaults. + +This means that the next iteration of Schema will be able to reliably use +the The lack of an option like "async" or "inverse" as a false-y value. + +We also are shifting the value for "kind" from "belongsTo" to "resource" +to make it more readil clear that relationships do not (by default) have +directionality or ownership over their inverse. + +🌲 *TrailRunner* + +```json +{ + "kind": "collection", + "name": "friends", + "options": { "inverse": "friends" }, + "type": "trail-runner", +} +``` + +Note, the [many-to-none](./3-many-to-none.md) variation of this would be: + +```json +{ + "kind": "collection", + "name": "friends", + "type": "trail-runner", +} +``` + +--- + +## Using `@warp-drive/schema-record` (🚧 Coming Soon) + +Working with schemas in a raw json format is far more flexible, lightweight and +performant than working with bulky classes that need to be shipped across the wire,parsed, and instantiated. Even relatively small apps can quickly find themselves shipping large quantities of JS just to describe their data. + +No one wants to author schemas in raw JSON though (we hope 😬), and the ergonomics of typed data and editor autocomplete based on your schemas are vital to productivity and +code quality. For this, we offer a way to express schemas as typescript using types, classes and decorators which are then compiled into json schemas and typescript interfaces for use by your project. + +🌲 *TrailRunner* + +```ts +import { collection } from '@warp-drive/schema'; + +export class TrailRunner { + @collection(TrailRunner, { inverse: "friends" }) + friends; +} +``` + +Note, the [many-to-none](./3-many-to-none.md) variation of this would be: + +```ts +import { collection } from '@warp-drive/schema'; + +export class TrailRunner { + @collection(TrailRunner) friends; +} +``` + +### LegacyCompat Mode + +Support for migrating from `@ember-data/model` on a more granular basis is provided by decorators that preserve the semantics of the quirks of that class. This allows you to begin eliminating models +and adopting other features of schemas sooner. + +🌲 *TrailRunner* + +```ts +import { hasMany } from '@warp-drive/schema/legacy'; + +export class TrailRunner { + @hasMany(TrailRunner, { inverse: "friends" }) + friends; +} +``` + +Note, the [many-to-none](./3-many-to-none.md) variation of this would be: + +```ts +import { hasMany } from '@warp-drive/schema/legacy'; + +export class TrailRunner { + @hasMany(TrailRunner) friends; +} + +--- + +- Previous ← [Many To One Relationships](./4-one-to-many.md) +- ⮐ [Relationships Guide](../../relationships.md) diff --git a/guides/relationships/terminology.md b/guides/relationships/terminology.md new file mode 100644 index 00000000000..0c66dd2593c --- /dev/null +++ b/guides/relationships/terminology.md @@ -0,0 +1,36 @@ +# Relationship Terminology + +- ⮐ [Relationships Guide](../../relationships.md) + +--- + +## Inverse + +The "inverse" of a relationship is the compound key generated by considering both the related `resource` `type` and the `name` of the matching field on the related `resource`. + +E.g. if Users have Pets and Pets have Owners, then the +inverse of the field `user.pets` is `pet.owners`, and the inverse of `pet.owners` is similarly `user.pets`. + +`to-none` relationships have no inverse. We often refer to this as a `null inverse`. + +## Self Referential + +Self referential relationships are relationships that point back at the same resource type. + +E.g. if a Person has parents and children, the link between `person.parents` and `person.children` is self-referential because both sides belong to the `person` resource type. + +## Reflexive + +Reflexive relationships are self-referential relationships that point back at the same property in addition to the same type. + +For instance, if a User has friends, where `user.friends`` is its own inverse. + +## Circular + +A circular relationship is a self-referential or reflexive relationship for which the *value* is the same on both sides. + +For instance, if Ego is his own best friend, then `ego.bestFriend === ego` + +## Polymorphic + +Polymorphic relationsips can be satisfied by multiple resource types. For instance, a user may have many vehicles, and each vehicle might be of type `car` or `boat` or `airplane` etc.