diff --git a/website/docs/api/index.md b/website/docs/api/index.md index 68ac4324..9e0254d6 100644 --- a/website/docs/api/index.md +++ b/website/docs/api/index.md @@ -7,30 +7,124 @@ sidebar_position: 0.5 custom_edit_url: null --- -## Core Packages +Orbit is distributed on npm through the +[@orbit](https://www.npmjs.com/org/orbit) organization in several packages. +This API reference is organized according to these packages. -- [@orbit/core](./api/core) -- [@orbit/coordinator](./api/coordinator) -- [@orbit/data](./api/data) -- [@orbit/identity-map](./api/identity-map) -- [@orbit/immutable](./api/immutable) -- [@orbit/serializers](./api/serializers) -- [@orbit/utils](./api/utils) -- [@orbit/validators](./api/validators) +## Core libraries -### Buckets +Orbit consists of the following core libraries: -- [@orbit/indexeddb-bucket](./api/indexeddb-bucket) -- [@orbit/local-storage-bucket](./api/local-storage-bucket) +- [@orbit/core](./core/index.md) - A core + set of primitives for performing, tracking, and responding to asynchronous + tasks, including: -## Record-based Packages + - An event system that allows listeners to engage with the fulfillment of + events by returning promises. -- [@orbit/records](./api/records) -- [@orbit/record-cache](./api/record-cache) + - An asynchronous task processing queue. -### Sources + - A log that tracks a history of changes and allows for revision and + interrogation. -- [@orbit/indexeddb](./api/indexeddb) -- [@orbit/jsonapi](./api/jsonapi) -- [@orbit/local-storage](./api/local-storage) -- [@orbit/memory](./api/memory) + - A bucket interface for persisting state. Used by logs and queues. + +- [@orbit/data](./data/index.md) - Applies the core Orbit primitives to data + sources. Includes the following: + + - [`Source`](./data/classes/Source.md) - a base class that can be used to + abstract any source of data. Sources can be decorated as + [`@queryable`](./data/interfaces/Queryable.md), + [`@updatable`](./data/interfaces/Updatable.md), and/or + [`@syncable`](./data/interfaces/Syncable.md). + + - [`Transform`](./data/interfaces/Transform.md) - composed of any number of + [`Operation`](./data/interfaces/Operation.md)s, a transform represents a set + of mutations to be applied transactionally. + + - [`Query`](./data/interfaces/Query.md) - composed of one or more + [`QueryExpression`](./data/interfaces/QueryExpression.md)s, a query + represents a request for data. + +- [@orbit/records](./records/index.md) - Extends the general data concepts from + [@orbit/data](./data/index.md) to make record-specific classes and interfaces. + These include: + + - `RecordSource` - a base class that extends `Source`. + + - `RecordSchema` - define models, including attributes and + relationships. + + - Operations that are specific to records (e.g. `addRecord`, `removeRecord`, + `addToHasMany`, etc.). + + - Query expressions that are specific to records (e.g. `findRecord`, + `findRecords`, etc.). + + - Tranform and query builders that use chainable terms to create operations + and expressions. + +- [@orbit/record-cache](./record-cache/index.md) - Everything you need to build + your own caches that hold data records (useful within record-specific + sources). + +- [@orbit/coordinator](./coordinator/index.md) - + A coordinator and set of coordination strategies for managing data flow and + keeping Orbit Data sources in sync. + +- [@orbit/serializers](./serializers/index.md) - A base set of serializers for + serializing / deserializing data types. + +- [@orbit/validators](./validators/index.md) - A set of validators for + validating primitive data and utilities for building higher order validators. + +- [@orbit/identity-map](./identity-map/index.md) - A simple identity map to + manage model instances. + +- [@orbit/immutable](./immutable/index.md) - A lightweight library of immutable + data structures. + +- [@orbit/utils](./utils/index.md) - A common set of utility functions used by + Orbit libs. + +## Record-specific data sources + +All of the following sources are based on [@orbit/records](./records/index.md). +They provide uniform interfaces to query and mutate record-specific data: + +- [@orbit/memory](./memory/index.md) - An in-memory data source that supports + complex querying and updating. Because memory sources maintain data in + immutable data structures, they can be efficiently forked. Forked memory + sources can diverge from the master memory source, and then the changes can be + merged later. + +- [@orbit/jsonapi](./jsonapi/index.md) - Provides full CRUD support, including + complex querying, for a RESTful API that conforms to the + [JSON:API](http://jsonapi.org/) specification. + +- [@orbit/local-storage](./local-storage/index.md) - Persists records to local + storage. + +- [@orbit/indexeddb](./indexeddb/index.md) - Persists records to IndexedDB. + +These standard sources can provide guidance for building your own custom sources +as well. + +## Persistence buckets + +Buckets are used to persist application state, such as queued requests and +change logs. Standard buckets include: + +- [@orbit/local-storage-bucket](./local-storage-bucket/index.md) - Persists + state to local storage. + +- [@orbit/indexeddb-bucket](./indexeddb-bucket/index.md) - Persists state to + IndexedDB. + +## Additional libraries + +Some additional libraries related to Orbit, but not covered by these docs, +include: + +- [ember-orbit](https://github.com/orbitjs/ember-orbit) - An Ember.js data + layer heavily inspired by Ember Data. diff --git a/website/docs/coordination.md b/website/docs/coordination.md index 2f0544cb..8155716b 100644 --- a/website/docs/coordination.md +++ b/website/docs/coordination.md @@ -101,7 +101,7 @@ strategies. ### Request strategies -Request strategies participate in the [request flow](./data-flows). Every +Request strategies participate in the [request flow](./data-flows.md). Every request strategy should be defined with: - `source` - the name of the observed source @@ -174,7 +174,7 @@ coordinator.addStrategy( ### Sync strategies -Sync strategies participate in the [sync flow](./data-flows). Every +Sync strategies participate in the [sync flow](./data-flows.md). Every sync strategy should be defined with: - `source` - the name of the observed source diff --git a/website/docs/data-sources.md b/website/docs/data-sources.md index c1c64ae3..66289836 100644 --- a/website/docs/data-sources.md +++ b/website/docs/data-sources.md @@ -26,16 +26,16 @@ understanding of the domain-specific data they manage. Let's create a simple schema and memory source: ```javascript -import { Schema } from "@orbit/data"; -import MemorySource from "@orbit/memory"; +import { RecordSchema } from '@orbit/records'; +import { MemorySource } from '@orbit/memory'; // Create a schema -const schema = new Schema({ +const schema = new RecordSchema({ models: { planet: { attributes: { - name: { type: "string" }, - classification: { type: "string" } + name: { type: 'string' }, + classification: { type: 'string' } } } } @@ -64,17 +64,17 @@ Let's look at an example of a simple mutation triggered by a call to `update`: ```javascript // Define a record const jupiter = { - type: "planet", - id: "jupiter", + type: 'planet', + id: 'jupiter', attributes: { - name: "Jupiter", - classification: "gas giant" + name: 'Jupiter', + classification: 'gas giant' } }; // Observe and log all transforms -memory.on("transform", t => { - console.log("transform", t); +memory.on('transform', (t) => { + console.log('transform', t); }); // Check the size of the transform log before updates @@ -82,7 +82,7 @@ console.log(`transforms: ${memory.transformLog.length}`); // Update the memory source with a transform that adds a record memory - .update(t => t.addRecord(jupiter)) + .update((t) => t.addRecord(jupiter)) .then(() => { // Verify that the transform log has grown console.log(`transforms: ${memory.transformLog.length}`); @@ -92,29 +92,31 @@ memory The following should be logged as a result: ```javascript -"transforms: 0", - "transform", +'transforms: 0', + 'transform', { operations: [ { - op: "addRecord", + op: 'addRecord', record: { - type: "planet", - id: "jupiter", + type: 'planet', + id: 'jupiter', attributes: { - name: "Jupiter", - classification: "gas giant" + name: 'Jupiter', + classification: 'gas giant' } } } ], options: undefined, - id: "05e5d20e-02c9-42c4-a083-99662c647fd1" + id: '05e5d20e-02c9-42c4-a083-99662c647fd1' }, - "transforms: 1"; + 'transforms: 1'; ``` -> Want to learn more about updating data? [See the guide](./updating-data) +:::info +Want to learn more about updating data? [See the guide](./updating-data.md) +::: ## Standard interfaces @@ -127,16 +129,14 @@ sources: - `Queryable` - Allows sources to be queried via a `query` method that receives a query expression and returns a recordset as a result. -- `Pushable` - Allows sources to be updated via a `push` method that takes a - transform and returns the results of the change and its side effects as an - array of transforms. - -- `Pullable` - Allows sources to be queried via a `pull` method that takes a - query expression and returns the results as an array of transforms. - - `Syncable` - Applies a transform or transforms to a source via a `sync` method. +:::caution +The `Pullable` and `Pushable` interfaces have been deprecated in +v0.17 and are scheduled to be removed in v0.18. +::: + ### Events All of the interfaces above emit events that share a common pattern. For an @@ -174,21 +174,20 @@ be ignored by the emitter. ### Data flows -The `Updatable`, `Queryable`, `Pushable`, and `Pullable` interfaces all -participate in the "request flow", in which requests are made upstream and data -flows back down. +The `Updatable` and `Queryable` interfaces participate in the "request flow", in +which requests are made upstream and data flows back down. The `Syncable` interface participates in the "sync flow", in which data flowing downstream is synchronized with other sources. -> Want to learn more about data flows? [See the guide](./data-flows) +> Want to learn more about data flows? [See the guide](./data-flows.md) ### Developer-facing interfaces -Generally speaking, only the `Updatable` and `Queryable` interfaces are designed -to be used directly by developers in most applications. The other interfaces are -used to coordinate data requests and synchronization between sources. +Generally speaking, developers will primarily interact the `Updatable` and +`Queryable` interfaces. The `Syncable` interface is used primarily via +coordination strategies. -> See guides that cover [querying data](./querying-data), -> [updating data](./updating-data), and -> [configuring coordination strategies](./coordination). +> See guides that cover [querying data](./querying-data.md), +> [updating data](./updating-data.md), and +> [configuring coordination strategies](./coordination.md). diff --git a/website/docs/getting-started.md b/website/docs/getting-started.md index d359eea0..4e1f2268 100644 --- a/website/docs/getting-started.md +++ b/website/docs/getting-started.md @@ -2,7 +2,8 @@ title: Getting started --- -This brief tutorial walks through using Orbit to manage data in a client-side +This brief tutorial walks through using Orbit to manage +[record-specific](/intro.md#record-specific-primitives) data in a client-side application. Sticking with the "orbit" theme, this application will track some objects orbiting in our own solar system. @@ -13,25 +14,26 @@ Schemas are used to define the models and relationships for an application. Let's start by defining a schema for our solar system's data: ```javascript -import { Schema } from "@orbit/data"; +import { RecordSchema } from '@orbit/records'; -const schema = new Schema({ +const schema = new RecordSchema({ models: { planet: { attributes: { - name: { type: "string" }, - classification: { type: "string" } + name: { type: 'string' }, + classification: { type: 'string' }, + atmosphere: { type: 'boolean' } }, relationships: { - moons: { kind: "hasMany", type: "moon", inverse: "planet" } + moons: { kind: 'hasMany', type: 'moon', inverse: 'planet' } } }, moon: { attributes: { - name: { type: "string" } + name: { type: 'string' } }, relationships: { - planet: { kind: "hasOne", type: "planet", inverse: "moons" } + planet: { kind: 'hasOne', type: 'planet', inverse: 'moons' } } } } @@ -53,7 +55,7 @@ schema. Let's create an in-memory source as our first data source: ```javascript -import MemorySource from "@orbit/memory"; +import { MemorySource } from '@orbit/memory'; const memory = new MemorySource({ schema }); ``` @@ -64,43 +66,43 @@ We can now load some data into our memory source and then query its contents: ```javascript const earth = { - type: "planet", - id: "earth", + type: 'planet', + id: 'earth', attributes: { - name: "Earth", - classification: "terrestrial", + name: 'Earth', + classification: 'terrestrial', atmosphere: true } }; const venus = { - type: "planet", - id: "venus", + type: 'planet', + id: 'venus', attributes: { - name: "Venus", - classification: "terrestrial", + name: 'Venus', + classification: 'terrestrial', atmosphere: true } }; const theMoon = { - type: "moon", - id: "theMoon", + type: 'moon', + id: 'theMoon', attributes: { - name: "The Moon" + name: 'The Moon' }, relationships: { - planet: { data: { type: "planet", id: "earth" } } + planet: { data: { type: 'planet', id: 'earth' } } } }; -await memory.update(t => [ +await memory.update((t) => [ t.addRecord(venus), t.addRecord(earth), t.addRecord(theMoon) ]); -let planets = await memory.query(q => q.findRecords("planet").sort("name")); +let planets = await memory.query((q) => q.findRecords('planet').sort('name')); console.log(planets); ``` @@ -109,25 +111,25 @@ The following output should be logged: ```javascript [ { - type: "planet", - id: "earth", + type: 'planet', + id: 'earth', attributes: { - name: "Earth", - classification: "terrestrial", + name: 'Earth', + classification: 'terrestrial', atmosphere: true }, relationships: { moons: { - data: [{ type: "moon", id: "theMoon" }] + data: [{ type: 'moon', id: 'theMoon' }] } } }, { - type: "planet", - id: "venus", + type: 'planet', + id: 'venus', attributes: { - name: "Venus", - classification: "terrestrial", + name: 'Venus', + classification: 'terrestrial', atmosphere: true } } @@ -142,9 +144,10 @@ the [JSON:API](http://jsonapi.org/) specification. Every record has an identity established by a `type` and `id` pair. Relationship linkage is specified in a `data` object via identities. -In order to add records to the memory source, we call `memory.update()` and pass an array -of operations. Passing a function to `update` provides us with a transform -builder (`t`), which we use to create an array of `addRecord` operations. +In order to add records to the memory source, we call `memory.update()` and pass +an array of operations. Passing a function to `update` provides us with a +transform builder (`t`), which we use to create an array of `addRecord` +operations. Note that we added the relationship between the moon and the planet on just the moon record. However, when we query the planet, we can see that the inverse @@ -154,7 +157,7 @@ to the memory source's cache passes through a schema consistency check. Let's look at how the memory source is queried: ```javascript -let planets = await memory.query(q => q.findRecords("planet").sort("name")); +let planets = await memory.query((q) => q.findRecords('planet').sort('name')); ``` Because we pass a function to `query`, Orbit provides us with a query builder @@ -168,11 +171,11 @@ involved). Here's an example of a more complex query that filters, sorts, and paginates: ```javascript -let planets = await memory.query(q => +let planets = await memory.query((q) => q - .findRecords("planet") - .filter({ attribute: "classification", value: "terrestrial" }) - .sort({ attribute: "name", order: "descending" }) + .findRecords('planet') + .filter({ attribute: 'classification', value: 'terrestrial' }) + .sort({ attribute: 'name', order: 'descending' }) .page({ offset: 0, limit: 10 }) ); ``` @@ -187,18 +190,16 @@ For example: ```javascript // Results will be returned synchronously by querying the cache -let planets = memory.cache.query(q => q.findRecords("planet").sort("name")); +let planets = memory.cache.query((q) => q.findRecords('planet').sort('name')); ``` By querying the cache instead of the memory source, you're not allowing other sources to participate in the fulfillment of the query. Continue reading to understand how requests to sources can be "coordinated". -
- -Want to experiment with some of the concepts presented so far? - -See [Part 1 of this example in CodeSandbox](https://codesandbox.io/s/orbitjs-v016-getting-started-part-1-wurdu?previewwindow=console). +:::tip Want to experiment? +See [Part 1 of this example in CodeSandbox](https://codesandbox.io/s/orbitjs-v017-getting-started-part-1-q4n3s?previewwindow=console). +::: ## Defining a backup source @@ -209,12 +210,12 @@ whole planet or moon! 😱 Let's create a browser storage source to keep data around locally: ```javascript -import IndexedDBSource from "@orbit/indexeddb"; +import { IndexedDBSource } from '@orbit/indexeddb'; const backup = new IndexedDBSource({ schema, - name: "backup", - namespace: "solarsystem" + name: 'backup', + namespace: 'solarsystem' }); ``` @@ -224,7 +225,7 @@ Every time a source is transformed, it emits a `transform` event. It's simple to observe these events directly: ```javascript -memory.on("transform", transform => { +memory.on('transform', (transform) => { console.log(transform); }); ``` @@ -233,7 +234,7 @@ It's possible to pipe changes that occur in one source into another via the `sync` method: ```javascript -memory.on("transform", transform => { +memory.on('transform', (transform) => { backup.sync(transform); }); ``` @@ -244,7 +245,7 @@ source without also being backed up, we should return the promise in the event handler: ```javascript -memory.on("transform", transform => { +memory.on('transform', (transform) => { return backup.sync(transform); }); ``` @@ -252,7 +253,7 @@ memory.on("transform", transform => { Or more simply: ```javascript -memory.on("transform", transform => backup.sync(transform)); +memory.on('transform', (transform) => backup.sync(transform)); ``` With this single line of code we've guaranteed that every change to the @@ -269,15 +270,15 @@ sources to which it applies a set of coordination strategies. A coordinator could be configured to handle the above scenario as follows: ```javascript -import Coordinator, { SyncStrategy } from "@orbit/coordinator"; +import { Coordinator, SyncStrategy } from '@orbit/coordinator'; const coordinator = new Coordinator({ sources: [memory, backup] }); const backupMemorySync = new SyncStrategy({ - source: "memory", - target: "backup", + source: 'memory', + target: 'backup', blocking: true }); @@ -312,44 +313,43 @@ If we want our app to restore all of its data from browser storage when it first boots, we could perform the following: ```javascript -let transform = await backup.pull(q => q.findRecords()); -await memory.sync(transform); +let allRecords = await backup.query((q) => q.findRecords()); +await memory.sync((t) => allRecords.map((r) => t.addRecord(r))); await coordinator.activate(); ``` -This code first pulls all the records from backup and then syncs them with the -main memory source _before_ activating the coordinator. In this way, the -coordination strategy that backs up the memory source won't be enabled until -after the restore is complete. +This code first queries all the records from the backup source and then syncs +them with the main memory source _before_ activating the coordinator. In this +way, the coordination strategy that backs up the memory source won't be enabled +until after the restore is complete. We now have an application which has data fully contained in the browser. Any data that's entered can be accessed while offline and will even persist across browser refreshes. -
- -Want to experiment more? - -See [Part 2 of this example in CodeSandbox](https://codesandbox.io/s/orbitjs-v016-getting-started-part-2-pd2z3?previewwindow=console). +:::tip Want to experiment? +See [Part 2 of this example in CodeSandbox](https://codesandbox.io/s/orbitjs-v017-getting-started-part-2-vt4ct?previewwindow=console). +::: ## Communicating with a server -Most apps can't exist in the vacuum of a browser—data tends to be far more -useful when it's shared with a server. +Most apps can't exist in the vacuum of a browser—data tends to be far +more useful when it's shared with a server. Let's say that we have a web server that conforms with the [JSON:API](http://jsonapi.org/) specification. We can use Orbit's -`JSONAPISource` to allow our app to communicate with that server. +[`JSONAPISource`](./api/jsonapi/classes/JSONAPISource.md) to allow our app to +communicate with that server. We'll start by creating a new `remote` source: ```javascript -import JSONAPISource from "@orbit/jsonapi"; +import { JSONAPISource } from '@orbit/jsonapi'; const remote = new JSONAPISource({ schema, - name: "remote", - host: "http://api.example.com" + name: 'remote', + host: 'http://api.example.com' }); ``` @@ -363,16 +363,16 @@ And then we can add strategies to ensure that queries and updates made against the memory source are processed by the remote server: ```javascript -import { RequestStrategy, SyncStrategy } from "@orbit/coordinator"; +import { RequestStrategy, SyncStrategy } from '@orbit/coordinator'; // Query the remote server whenever the memory source is queried coordinator.addStrategy( new RequestStrategy({ - source: "memory", - on: "beforeQuery", + source: 'memory', + on: 'beforeQuery', - target: "remote", - action: "query", + target: 'remote', + action: 'query', blocking: false }) @@ -381,11 +381,11 @@ coordinator.addStrategy( // Update the remote server whenever the memory source is updated coordinator.addStrategy( new RequestStrategy({ - source: "memory", - on: "beforeUpdate", + source: 'memory', + on: 'beforeUpdate', - target: "remote", - action: "update", + target: 'remote', + action: 'update', blocking: false }) @@ -394,17 +394,17 @@ coordinator.addStrategy( // Sync all changes received from the remote server to the memory source coordinator.addStrategy( new SyncStrategy({ - source: "remote", - target: "memory", + source: 'remote', + target: 'memory', blocking: false }) ); ``` -These strategies are all non-blocking, which means that the memory source will be -updated / queried optimistically without waiting for responses from the server. -Once the server responses are received, they will then be sync'd back with the -memory source. +These strategies are all non-blocking, which means that the memory source will +be updated / queried optimistically without waiting for responses from the +server. Once the server responses are received, they will then be sync'd back +with the memory source. This set of coordination strategies is certainly not yet production ready. We will need exception handling in our strategies to tell Orbit how to handle @@ -412,9 +412,10 @@ network errors (e.g. retry after X secs) as well as other types of exceptions. Optimistic server requests paired with an in-browser backup can work well for some kinds of applications. For other applications, it's more appropriate to use -blocking strategies that tie the success of memory source requests to a successful -round trip to the server. Still other applications might choose to mix -strategies, so that only certain updates are blocking (e.g. a store purchase). +blocking strategies that tie the success of memory source requests to a +successful round trip to the server. Still other applications might choose to +mix strategies, so that only certain updates are blocking (e.g. a store +purchase). Orbit allows for filtering, exception handling, and more in strategies to enable any of these options. We'll dive deeper into these topics in the rest of @@ -431,11 +432,11 @@ In order to persist this state, we can create a "bucket" that can be shared among our sources: ```javascript -import LocalStorageBucket from "@orbit/local-storage-bucket"; -import IndexedDBBucket, { supportsIndexedDB } from "@orbit/indexeddb-bucket"; +import { LocalStorageBucket } from '@orbit/local-storage-bucket'; +import { IndexedDBBucket, supportsIndexedDB } from '@orbit/indexeddb-bucket'; const BucketClass = supportsIndexedDB() ? IndexedDBBucket : LocalStorageBucket; -const bucket = new BucketClass({ namespace: "my-app" }); +const bucket = new BucketClass({ namespace: 'my-app' }); ``` Note that the above code favors using an IndexedDB-based bucket and only falls @@ -448,8 +449,8 @@ For instance: const backup = new IndexedDBSource({ bucket, schema, - name: "backup", - namespace: "solarsystem" + name: 'backup', + namespace: 'solarsystem' }); const memory = new MemorySource({ bucket, schema }); diff --git a/website/docs/intro.md b/website/docs/intro.md index 9b6435a2..8450ea63 100644 --- a/website/docs/intro.md +++ b/website/docs/intro.md @@ -11,17 +11,17 @@ Orbit is written in [Typescript](https://www.typescriptlang.org) and distributed on [npm](https://www.npmjs.com/org/orbit) as packages containing a variety of module formats and ES language levels. Most Orbit packages are isomorphic—they can run in modern browsers as well as in the [Node.js](https://nodejs.org/) -runtime. +runtime. Each package is described in depth in [Orbit's API +Reference](./api/index.md). -## Quick links +:::tip Quick links -
+Looking a quick code walkthrough? Check out the [Getting Started +Guide](./getting-started.md). -Looking a quick code walkthrough? Check out the [Getting Started Guide](./getting-started). +Upgrading from v0.16? [Learn what's new](./whats-new.md). -Upgrading from v0.16? [Learn what's new](./whats-new). - -
+::: ## Orbit's goals @@ -123,38 +123,27 @@ changes to be logged, diff'd, sync’d, and even reverted. Orbit's core primitives were developed to align with the goals and constraints enumerated above. -### Records - -Records are used to represent data in a normalized form. Each record has a -`type` and `id`, which together establish its identity. Records may also include -other fields, such as attributes and relationships with other records. - -### Schema - -A `Schema` defines all the models in a given domain. Each `Model` defines the -characteristics for records of a given type. - ### Source Every source of data, from an in-memory store to an IndexedDB database to a REST -server, is represented as a `Source`. Sources vary widely in their capabilities: -some may support interfaces for updating and/or querying records, while other -sources may simply broadcast changes. Schemas provide sources with an -understanding of the data they manage. +server, is represented as a [`Source`](./api/data/interfaces/Source.md). Sources +vary widely in their capabilities: some may support interfaces for updating +and/or querying data, while other sources may simply broadcast changes. ### Transform -A `Transform` is used to represent a set of record mutations, or "operations". -Each `Operation` represents a single change to a record or relationship (e.g. -adding a record, updating a field, deleting a relationship, etc.). Transforms -must be applied atomically—all operations succeed or fail together. +A [`Transform`](./api/data/interfaces/Transform.md) is used to represent a set +of record mutations, or "operations". Each +[`Operation`](./api/data/interfaces/Operation.md) represents a single mutation. +Transforms must be applied atomically—all operations succeed or fail +together. ### Query -The contents of sources can be interrogated using a `Query`. Orbit comes with a -standard set of query expressions for finding records and related records. These -expressions can be paired with refinements (e.g. filters, sort order, etc.). A -query builder is provided to improve the ergonomics of composing queries. +The contents of sources can be interrogated using a +[`Query`]((./api/data/interfaces/Query.md)), which is composed of one or more +expressions. Each [`QueryExpression`](./api/data/interfaces/QueryExpression.md) +represents a request for a single piece or collection of data. ### Log @@ -179,6 +168,22 @@ applies coordination strategies to keep them in sync, handle problems, perform logging, and more. Strategies can be customized to observe only certain events on specific sources. +### Record-specific primitives + +While the above primitives allow you to work with any data, Orbit only becomes +truly practical to use once you've settled on a particular shape of normalized +data. And currently, all of Orbit's standard sources use a normalized form of +data called "records". + +Each [`Record`](./api/records/interfaces/Record.md) has a `type` and `id`, which +together establish its identity. Records may also include other fields, such as +attributes and relationships with other records. + +A [`RecordSchema`](./api/records/classes/RecordSchema.md) defines all the models +in a given domain. And within this schema, a +[`ModelDefinition`](./api/records/classes/ModelDefinition.md) defines the +characteristics for records of a given type. + ## The Orbit Philosophy The primitives in Orbit were developed to be as composable and interchangeable diff --git a/website/docs/memory-sources.md b/website/docs/memory-sources.md index eb80dcc4..f446d19d 100644 --- a/website/docs/memory-sources.md +++ b/website/docs/memory-sources.md @@ -7,8 +7,8 @@ length throughout this guide already. Memory sources implement the `Updatable`, `Queryable`, and `Syncable` interfaces, and are the primary interface through which developers will interact with an Orbit application. -Instead of re-explaining [querying](./querying-data) and -[updating](./updating-data) memory sources, this section explores some unique +Instead of re-explaining [querying](./querying-data.md) and +[updating](./updating-data.md) memory sources, this section explores some unique capabilities of memory sources and their inner workings. ## Cache @@ -67,7 +67,7 @@ a high fidelity log of changes to a memory source, observe its cache's `patch` e ### Querying cache data -As has been [discussed](./querying-data), the contents of a cache can be +As has been [discussed](./querying-data.md), the contents of a cache can be queried directly and synchronously, using the same query expressions that can be applied to other sources. diff --git a/website/docs/modeling-data.md b/website/docs/modeling-data.md index 656a4c10..510cd2cf 100644 --- a/website/docs/modeling-data.md +++ b/website/docs/modeling-data.md @@ -39,6 +39,15 @@ Here's an example record that represents a planet: } ``` +:::caution + +Just like [JSON:API resource +fields](https://jsonapi.org/format/#document-resource-object-fields), all the +fields in an Orbit record share the same namespace and must be unique. A record +can not have an attribute and relationship with the same name, nor can it have +an attribute or relationship named `type` or `id`. +::: + ### Identity Each record's identity is established by a union of the following fields: @@ -53,21 +62,31 @@ Applications can take one of the following approaches to managing identity: 1. Auto-generate IDs, typically as v4 UUIDs, and then use the same IDs locally and remotely. -2. Auto-generate IDs locally and map those IDs to canonical IDs (or "keys") +2. Remotely generate IDs and only reference records by those IDs. + +3. Auto-generate IDs locally and map those IDs to canonical IDs (or "keys") generated remotely. -3. Remotely generate IDs and don't reference records until those IDs have been - assigned. +The first approach is the most straightforward, flexible, and requires the least +configuration. However, it is not feasible when working with servers that do not +accept client-generated IDs. + +The second approach only works if you never need to generate _new_ records +with Orbit, only reference existing ones generated remotely. -The first two approaches are "optimistic" and allow for offline usage, while -the third is "pessimistic" and requires persistent connectivity. +The third approach is a pragmatic blend of local and remote generated IDs. +Although mapping IDs requires more configuration and complexity than having a +single ID for each record, this approach does not constrain the capabilities of +your application. -> Note: It's possible to mix these approaches for different types of records -> (i.e. models) within a given application. +:::tip +It's possible to mix these approaches for different types of records +(i.e. models) within a given application. +::: ### Keys -When using locally-generated IDs, Orbit uses "keys" to support mapping between +When pairing locally-generated IDs, Orbit uses "keys" to support mapping between local and remote IDs. Remote IDs should be kept in a `keys` object at the root of a record. @@ -115,16 +134,16 @@ identities. ## Schema -A `Schema` defines the models allowed in a source, including their keys, +A [`RecordSchema`](./api/records/classes/RecordSchema.md) defines the models allowed in a source, including their keys, attributes, and relationships. Typically, a single schema is shared among all the sources in an application. Schemas are defined with their initial settings as follows: ```javascript -import { Schema } from "@orbit/data"; +import { RecordSchema } from "@orbit/records"; -const schema = new Schema({ +const schema = new RecordSchema({ models: { planet: { attributes: { @@ -154,20 +173,15 @@ object that contains `attributes`, `relationships`, and/or `keys`. Attributes may be defined by their `type`, which determines what type of data they can contain. An attribute's type may also be used to determine how it -should be serialized. Valid attribute types are: +should be serialized and validated. Standard attribute types are: +- `array` - `boolean` - `date` - `datetime` - `number` -- `string` - -The following attribute types are passed through as-is and no special -serialization is performed: - - `object` -- `array` -- `unknown` +- `string` ### Model relationships @@ -186,9 +200,9 @@ Here's an example of a schema definition that includes relationships with inverses: ```javascript -import { Schema } from "@orbit/data"; +import { RecordSchema } from "@orbit/records"; -const schema = new Schema({ +const schema = new RecordSchema({ models: { planet: { relationships: { @@ -204,42 +218,6 @@ const schema = new Schema({ }); ``` -### Model name inflections - -By default, Orbit uses very simple inflections, or pluralization/singularization -of model names - e.g. `user <-> users`. Depending on your API, you may need to -handle this yourself. A common error from same is where `countries` gets converted -to `countrie`, as the `s` is programmatically removed from it when it's singularized. - -You can override the Orbit inflectors via the Schema factory, e.g. - -```javascript -new Schema({ - models, - pluralize, - singularize -}); -``` - -There are several inflection packages available on NPM, or you can keep it super -simple for a small application and do something like the following, where a simple -map containing your model names and their inflections can be kept up to date with -your models. - -```javascript -const inflect = { - country: 'countries', - countries: 'country', - ... -} - -new Schema({ - models, - pluralize: word => inflect[word], - singularize: word => inflect[word] -}) -``` - ### Model keys When working with remote servers that do not support client-generated IDs, it's @@ -250,7 +228,7 @@ Keys currently accept no _standard_ options, so they should be declared with an empty options hash as follows: ```javascript -const schema = new Schema({ +const schema = new RecordSchema({ models: { moon: { keys: { remoteId: {} } @@ -262,60 +240,16 @@ const schema = new Schema({ }); ``` -> Note: Keys can only be of type `"string"`, which is unnecessary to declare. - -> Note: A key such as `remoteId` might be serialized as simply `id` when -> communicating with a server. However, it's important to distinguish it from the -> client-generated `id` used within Orbit, so it requires a unique name. - -### Record initialization +:::info -Schemas support the ability to initialize records via an -`initializeRecord()` method that takes a record (`Record`) argument. -Currently, `initializeRecord` just assigns an `id` to a record if the field -is undefined. It may be extended to allow per-model defaults to be set as well. +Since keys can only be of type `"string"`, it is unnecessary to explicitly +declare this (although `{ type: "string" }` is technically allowed in a key's +declaration). +::: -Here's an example that creates a schema and initializes a record: +:::info -```javascript -import { Schema } from "@orbit/schema"; - -const schema = new Schema({ - models: { - planet: { - attributes: { - name: { type: "string" } - } - } - } -}); - -let earth = { - type: "planet", - attributes: { - name: "Earth" - } -}; - -schema.initializeRecord(earth); - -console.log(earth.id); // "4facf3cc-7270-4b5e-aedd-94d777d31c31" -``` - -The default implementation of `initializeRecord` internally calls the schema's -`generateId()` method to generate an `id`. By default, this invokes -`Orbit.uuid()` to generate a v4 UUID (where `Orbit` is the default export from -`@orbit/core`). - -It's possible to override `generateId` for a given schema to use a different -local ID scheme. Here's a naive example: - -```javascript -let counter = 0; - -const schema = new Schema({ - generateId(type) { - return counter++; - } -}); -``` +A key such as `remoteId` might be serialized as simply `id` when communicating +with a server. However, it's important to distinguish it from the +client-generated `id` used within Orbit, so it requires a unique name. +::: diff --git a/website/docs/packages.md b/website/docs/packages.md deleted file mode 100644 index 44590ed4..00000000 --- a/website/docs/packages.md +++ /dev/null @@ -1,91 +0,0 @@ ---- -title: Packages ---- - -Orbit is distributed on npm through the -[@orbit](https://www.npmjs.com/org/orbit) organization in several packages. - -## Core libraries - -Orbit consists of the following core libraries: - -- [@orbit/core](https://www.npmjs.com/package/@orbit/core) - A core - set of primitives for performing, tracking, and responding to asynchronous - tasks, including: - - - An event system that allows listeners to engage with the fulfillment of - events by returning promises. - - - An asynchronous task processing queue. - - - A log that tracks a history of changes and allows for revision and - interrogation. - - - A bucket interface for persisting state. Used by logs and queues. - -- [@orbit/data](https://www.npmjs.com/package/@orbit/data) - Applies - the core Orbit primitives to data sources. Includes the following elements: - - - A schema for defining models, including attributes and relationships. - - - Operations used to manipulate records (e.g. `addRecord`, `removeRecord`, - `addToHasMany`, etc.). - - - Transforms, which are composed of any number of operations, and must be - performed transactionally. - - - A query language that allows query expressions to be composed in a flexible - AST form. - - - A base `Source` class that can be used to abstract any source of data. - Sources can be decorated as `pullable`, `pushable`, `queryable`, `syncable`, - and/or `updatable` - each decorator provides a unique interface that allows - for transforms and queries to be applied as appropriate. - -- [@orbit/coordinator](https://www.npmjs.com/package/@orbit/coordinator) - - A coordinator and set of coordination strategies for managing data flow and - keeping Orbit Data sources in sync. - -- [@orbit/utils](https://www.npmjs.com/package/@orbit/utils) - A - common set of utility functions used by Orbit libs. - -## Standard data sources - -Orbit provides the following sources for accessing and persisting data: - -- [@orbit/memory](https://www.npmjs.com/package/@orbit/memory) - An - in-memory data source that supports complex querying and updating. Because - memory sources maintain data in immutable data structures, they can be efficiently - forked. Forked memory sources can diverge from the master memory source, and then the changes - can be merged later. - -- [@orbit/jsonapi](https://www.npmjs.com/package/@orbit/jsonapi) - - Provides full CRUD support, including complex querying, for a RESTful API that - conforms to the [JSON:API](http://jsonapi.org/) specification. - -- [@orbit/local-storage](https://www.npmjs.com/package/@orbit/local-storage) - - Persists records to local storage. - -- [@orbit/indexeddb](https://www.npmjs.com/package/@orbit/indexeddb) - - Persists records to IndexedDB. - -These standard sources can provide guidance for building your own custom sources -as well. - -## Standard persistence buckets - -Buckets are used to persist application state, such as queued requests and -change logs. Standard buckets include: - -- [@orbit/local-storage-bucket](https://www.npmjs.com/package/@orbit/local-storage-bucket) - - Persists state to local storage. - -- [@orbit/indexeddb-bucket](https://www.npmjs.com/package/@orbit/indexeddb-bucket) - - Persists state to IndexedDB. - -## Additional libraries - -Additional libraries related to Orbit include: - -- [ember-orbit](https://www.npmjs.com/package/ember-orbit) - An Ember.js data - layer heavily inspired by Ember Data. diff --git a/website/docs/querying-data.md b/website/docs/querying-data.md index 2ac94489..d77460e2 100644 --- a/website/docs/querying-data.md +++ b/website/docs/querying-data.md @@ -2,41 +2,41 @@ title: Querying data --- -The contents of a source can be interrogated using a `Query`. Orbit comes with a -standard set of query expressions for finding records and related records. These -expressions can be paired with refinements (e.g. filters, sort order, etc.). +The contents of a source can be interrogated using a +[`Query`](./api/data/interfaces/Query.md). Orbit comes with a standard set of +query expressions for finding records and related records. These expressions can +be paired with refinements (e.g. filters, sort order, etc.). Custom query expressions can also be developed, as long as all the sources participating can understand them. ## Query expressions -The `QueryExpression` interface requires one member: +The base [`QueryExpression`](./api/data/interfaces/QueryExpression.md) interface +consists of: - `op` - a string identifying the type of query operation +- `options` - (Optional) a [`RequestOptions`](./api/data/interfaces/RequestOptions.md) object -The other members of a `QueryExpression` are specific to the `op`. - -The following standard query expressions are defined in `@orbit/data`: +The other members of a +[`QueryExpression`](./api/data/interfaces/QueryExpression.md) are specific to +the `op`. The following standard record-specific query expressions are defined +in [`@orbit/records`](./api/records/index.md): ```typescript -interface QueryExpression { - op: string; -} - interface FindRecord extends QueryExpression { - op: "findRecord"; + op: 'findRecord'; record: RecordIdentity; } interface FindRelatedRecord extends QueryExpression { - op: "findRelatedRecord"; + op: 'findRelatedRecord'; record: RecordIdentity; relationship: string; } interface FindRelatedRecords extends QueryExpression { - op: "findRelatedRecords"; + op: 'findRelatedRecords'; record: RecordIdentity; relationship: string; sort?: SortSpecifier[]; @@ -45,7 +45,7 @@ interface FindRelatedRecords extends QueryExpression { } interface FindRecords extends QueryExpression { - op: "findRecords"; + op: 'findRecords'; type?: string; sort?: SortSpecifier[]; filter?: FilterSpecifier[]; @@ -56,7 +56,7 @@ interface FindRecords extends QueryExpression { Supporting interfaces include: ```typescript -export type SortOrder = "ascending" | "descending"; +export type SortOrder = 'ascending' | 'descending'; export interface SortSpecifier { kind: string; @@ -64,11 +64,19 @@ export interface SortSpecifier { } export interface AttributeSortSpecifier extends SortSpecifier { - kind: "attribute"; + kind: 'attribute'; attribute: string; } -export type ComparisonOperator = "equal" | "gt" | "lt" | "gte" | "lte" | "some" | "all" | "none"; +export type ComparisonOperator = + | 'equal' + | 'gt' + | 'lt' + | 'gte' + | 'lte' + | 'some' + | 'all' + | 'none'; export interface FilterSpecifier { op: ComparisonOperator; @@ -76,7 +84,7 @@ export interface FilterSpecifier { } export interface AttributeFilterSpecifier extends FilterSpecifier { - kind: "attribute"; + kind: 'attribute'; attribute: string; value: any; } @@ -86,7 +94,7 @@ export interface PageSpecifier { } export interface OffsetLimitPageSpecifier extends PageSpecifier { - kind: "offsetLimit"; + kind: 'offsetLimit'; offset?: number; limit?: number; } @@ -94,10 +102,11 @@ export interface OffsetLimitPageSpecifier extends PageSpecifier { ## Queries -The `Query` interface has the following members: +The [`Query`](./api/data/interfaces/Query.md) interface has the following +members: - `id` - a string that uniquely identifies the query -- `expressions` - an array of `QueryExpression` objects +- `expressions` - an instance or array of [`QueryExpression`](./api/data/interfaces/QueryExpression.md) objects - `options` - an optional object that represents options that can influence how a query is processed @@ -115,17 +124,17 @@ You can use the standard `@orbit/data` query builder as follows: ```javascript // Find a single record by identity -memory.query(q => q.findRecord({ type: "planet", id: "earth" })); +memory.query((q) => q.findRecord({ type: 'planet', id: 'earth' })); // Find all records by type -memory.query(q => q.findRecords("planet")); +memory.query((q) => q.findRecords('planet')); // Find a related record in a to-one relationship -memory.query(q => q.findRelatedRecord({ type: "moon", id: "io" }, "planet")); +memory.query((q) => q.findRelatedRecord({ type: 'moon', id: 'io' }, 'planet')); // Find related records in a to-many relationship -memory.query(q => - q.findRelatedRecords({ type: "planet", id: "earth" }, "moons") +memory.query((q) => + q.findRelatedRecords({ type: 'planet', id: 'earth' }, 'moons') ); ``` @@ -133,78 +142,78 @@ The base `findRecords` query can be enhanced significantly: ```javascript // Sort by name -memory.query(q => q.findRecords('planet') - .sort('name')); +memory.query((q) => q.findRecords('planet') + .sort('name')); // Sort by classification, then name (descending) -memory.query(q => q.findRecords('planet') - .sort('classification', '-name')); +memory.query((q) => q.findRecords('planet') + .sort('classification', '-name')); // Filter by a single attribute -memory.query(q => q.findRecords('planet') - .filter({ attribute: 'classification', value: 'terrestrial' }); +memory.query((q) => q.findRecords('planet') + .filter({ attribute: 'classification', value: 'terrestrial' }); // Filter by multiple attributes -memory.query(q => q.findRecords('planet') - .filter({ attribute: 'classification', value: 'terrestrial' }, - { attribute: 'mass', op: 'gt', value: 987654321 }); +memory.query((q) => q.findRecords('planet') + .filter({ attribute: 'classification', value: 'terrestrial' }, + { attribute: 'mass', op: 'gt', value: 987654321 }); // Filter by related records -memory.query(q => q.findRecords('moons') - .filter({ relation: 'planet', record: { type: 'planet', id: 'earth' }}); +memory.query((q) => q.findRecords('moons') + .filter({ relation: 'planet', record: { type: 'planet', id: 'earth' }}); // Filter by multiple related records -memory.query(q => q.findRecords('moons') - .filter({ relation: 'planet', records: [{ type: 'planet', id: 'earth' }, { type: 'planet', id: 'jupiter'}]}); +memory.query((q) => q.findRecords('moons') + .filter({ relation: 'planet', records: [{ type: 'planet', id: 'earth' }, { type: 'planet', id: 'jupiter'}]}); // Paginate by offset and limit -memory.query(q => q.findRecords('planet') - .page({ offset: 0, limit: 10 })); +memory.query((q) => q.findRecords('planet') + .page({ offset: 0, limit: 10 })); // Combine filtering, sorting, and paginating -memory.query(q => q.findRecords('planet') - .filter({ attribute: 'classification', value: 'terrestrial' }) - .sort('name') - .page({ offset: 0, limit: 10 })); +memory.query((q) => q.findRecords('planet') + .filter({ attribute: 'classification', value: 'terrestrial' }) + .sort('name') + .page({ offset: 0, limit: 10 })); ``` The same parameters can be applied to `findRelatedRecords`: ```javascript // Sort by name -memory.query(q => q.findRelatedRecords({ id: 'solar', type: 'planetarySystem' }, 'planets') - .sort('name')); +memory.query((q) => q.findRelatedRecords({ id: 'solar', type: 'planetarySystem' }, 'planets') + .sort('name')); // Sort by classification, then name (descending) -memory.query(q => q.findRelatedRecords({ id: 'solar', type: 'planetarySystem' }, 'planets') - .sort('classification', '-name')); +memory.query((q) => q.findRelatedRecords({ id: 'solar', type: 'planetarySystem' }, 'planets') + .sort('classification', '-name')); // Filter by a single attribute -memory.query(q => q.findRelatedRecords({ id: 'solar', type: 'planetarySystem' }, 'planets') - .filter({ attribute: 'classification', value: 'terrestrial' }); +memory.query((q) => q.findRelatedRecords({ id: 'solar', type: 'planetarySystem' }, 'planets') + .filter({ attribute: 'classification', value: 'terrestrial' }); // Filter by multiple attributes -memory.query(q => q.findRelatedRecords({ id: 'solar', type: 'planetarySystem' }, 'planets') - .filter({ attribute: 'classification', value: 'terrestrial' }, - { attribute: 'mass', op: 'gt', value: 987654321 }); +memory.query((q) => q.findRelatedRecords({ id: 'solar', type: 'planetarySystem' }, 'planets') + .filter({ attribute: 'classification', value: 'terrestrial' }, + { attribute: 'mass', op: 'gt', value: 987654321 }); // Filter by related records -memory.query(q => q.findRelatedRecords({ id: 'solar', type: 'planetarySystem' }, 'moons') - .filter({ relation: 'planet', record: { type: 'planet', id: 'earth' }}); +memory.query((q) => q.findRelatedRecords({ id: 'solar', type: 'planetarySystem' }, 'moons') + .filter({ relation: 'planet', record: { type: 'planet', id: 'earth' }}); // Filter by multiple related records -memory.query(q => q.findRelatedRecords({ id: 'solar', type: 'planetarySystem' }, 'moons') - .filter({ relation: 'planet', records: [{ type: 'planet', id: 'earth' }, { type: 'planet', id: 'jupiter'}]}); +memory.query((q) => q.findRelatedRecords({ id: 'solar', type: 'planetarySystem' }, 'moons') + .filter({ relation: 'planet', records: [{ type: 'planet', id: 'earth' }, { type: 'planet', id: 'jupiter'}]}); // Paginate by offset and limit -memory.query(q => q.findRelatedRecords({ id: 'solar', type: 'planetarySystem' }, 'planets') - .page({ offset: 0, limit: 10 })); +memory.query((q) => q.findRelatedRecords({ id: 'solar', type: 'planetarySystem' }, 'planets') + .page({ offset: 0, limit: 10 })); // Combine filtering, sorting, and paginating -memory.query(q => q.findRelatedRecords({ id: 'solar', type: 'planetarySystem' }, 'planets') - .filter({ attribute: 'classification', value: 'terrestrial' }) - .sort('name') - .page({ offset: 0, limit: 10 })); +memory.query((q) => q.findRelatedRecords({ id: 'solar', type: 'planetarySystem' }, 'planets') + .filter({ attribute: 'classification', value: 'terrestrial' }) + .sort('name') + .page({ offset: 0, limit: 10 })); ``` ## Filtering @@ -212,7 +221,8 @@ memory.query(q => q.findRelatedRecords({ id: 'solar', type: 'planetarySystem' }, As shown in some of the previous examples, you can filter over the records that are found by a `findRecords` or `findRelatedRecords` query. Filtering is done building a boolean expression and only retrieving the records for which this expression returns `true`. This boolean expression, just like it is with regular javascript, is built out of three parts. Javascript: -``` + +```typescript const denserThanEarth = planets.filter((planet) => { return planet.density > earth.density }) // | 1 | 2 | 3 | @@ -220,21 +230,22 @@ Javascript: ``` Filter expression: -``` + +```typescript const denserThanEarth = orbit.cache.query((q) => { return q.findRecords('planets') - .filter({ attribute: 'radius', op: 'lt', value: earth.density }) -}) // | 1 | 2 | 3 | + .filter({ attribute: 'radius', op: 'lt', value: earth.density }) +}) // | 1 | 2 | 3 | ``` 1. the left hand value: - This is a reference to the property of the records that you want to compare. This can either be a `relationship` or an `attribute`. During evaluation, the reference will be replaced by the actual values of the records. + This is a reference to the property of the records that you want to compare. This can either be a `relationship` or an `attribute`. During evaluation, the reference will be replaced by the actual values of the records. 2. the comparison operation - The operation determines the way the two values will be compared. + The operation determines the way the two values will be compared. 3. the right hand value: - This is a value that will remain constant for the entirety of the filter. This value determines, given the operation, which records will be returned and which will not. + This is a value that will remain constant for the entirety of the filter. This value determines, given the operation, which records will be returned and which will not. ### Comparison operators for filtering @@ -245,7 +256,7 @@ Both have their own comparison operators. Attribute filtering looks like the following: -``` +```typescript const denserThanEarth = orbit.cache.query((q) => { return q.findRecords('planets') .filter({ attribute: 'radius', op: 'lt', value: earth.density }) @@ -265,22 +276,24 @@ For attribute filtering, the following comparison operators are available. Relationship filtering has two types: Filtering on a `hasOne` relationship: -``` + +```typescript const moonsOfJupiter = orbit.cache.query((q) => { return q.findRecords('moon') - .filter({ relationship: 'planet', op: 'equal', record: { type: 'planet', id: 'jupiter' } }) + .filter({ relationship: 'planet', op: 'equal', record: { type: 'planet', id: 'jupiter' } }) }) ``` Filtering on a `hasMany` relationship: -``` + +```typescript const theSolarSystem = orbit.cache.query((q) => { return q.findRecords('planetarySystem') - .filter({ - relationship: 'planets', - op: 'some', - records: [{ type: 'planet', id: 'earth' }] - }) + .filter({ + relationship: 'planets', + op: 'some', + records: [{ type: 'planet', id: 'earth' }] + }) }) ``` @@ -291,6 +304,7 @@ Filtering on a `hasOne` relationship has different comparison operations availab - `equal`: returns a record if the left hand relationship is equal to the right hand relationship. `hasMany` operations: + - `equal`: returns a record if the left hand relationsips are identical to the right hand relationships. - `all`: returns a record if the left hand relationships contain all the right hand relationships. - `some`: returns a record if the left hand relationships contain one or more of the right hand relationships. @@ -300,14 +314,14 @@ Filtering on a `hasOne` relationship has different comparison operations availab If you're using the default settings for JSONAPISource, `findRelatedRecords` and `findRecords.filter(...)` produce very different URLs. -``` +```typescript const relatedRecordId = { type: 'planet', id: 'earth' }; // This fetches from: /planets/earth/moons -memory.query(q => q.findRelatedRecords(relatedRecordId, 'moons')); +memory.query((q) => q.findRelatedRecords(relatedRecordId, 'moons')); // This fetches from: /moons?filter[planet]=earth -memory.query(q => q.findRecords('moon')).filter({ relation: 'planet', record: relatedRecordId }); +memory.query((q) => q.findRecords('moon')).filter({ relation: 'planet', record: relatedRecordId }); ``` ### Query options @@ -319,11 +333,11 @@ For example, the following query is given a `label` and contains instructions for the source named `remote`: ```javascript -memory.query(q => q.findRecords("contact").sort("lastName", "firstName"), { - label: "Find all contacts", +memory.query((q) => q.findRecords('contact').sort('lastName', 'firstName'), { + label: 'Find all contacts', sources: { remote: { - include: ["phone-numbers"] + include: ['phone-numbers'] } } }); @@ -342,9 +356,9 @@ phone numbers. It is possible to pass different options to each expression in the query. ```javascript -memory.query(q => [ - q.findRecords("contact").options({ include: ["phone-numbers"] }), - q.findRecords("meeting").options({ include: ["location"] }) +memory.query((q) => [ + q.findRecords('contact').options({ include: ['phone-numbers'] }), + q.findRecords('meeting').options({ include: ['location'] }) ]); ``` @@ -358,7 +372,7 @@ For example: ```javascript // Results will be returned synchronously by querying the cache -let planets = memory.cache.query(q => q.findRecords("planet").sort("name")); +const planets = memory.cache.query((q) => q.findRecords('planet').sort('name')); ``` > By querying the cache instead of the memory source, you're not allowing other @@ -366,24 +380,30 @@ let planets = memory.cache.query(q => q.findRecords("planet").sort("name")); > coordinate queries across multiple sources, it's critical to make requests > directly on the memory source. -### LiveQuery +### Live queries -On a memory source, you can subscribe to a `LiveQuery`. For that you need to create -a `LiveQuery` instance and then subscribe to changes. By default `LiveQuery` will -run on memory cache `patch` event with a debounce. Subscription callback will be -called on every operation which is relevant to the query. - -> If you use a pull based reactive system (for example Glimmer tracking) you can -> set debounceLiveQueries option to false on memory cache. +On a memory source's cach, you can subscribe to a +[`SyncLiveQuery`](/api/record-cache/classes/SyncLiveQuery.md). For that you need +to request a [`SyncLiveQuery`](/api/record-cache/classes/SyncLiveQuery.md) +instance and then subscribe to changes. By default the +[`SyncLiveQuery`](/api/record-cache/classes/SyncLiveQuery.md) will observe cache +`patch` events with a debounce. The subscription callback will be called on +every operation which is relevant to the query. ```javascript // Create a new LiveQuery instance -let planetsLiveQuery = memory.cache.liveQuery(q => q.findRecords("planet")); +const planetsLiveQuery = memory.cache.liveQuery((q) => q.findRecords('planet')); // Subscribe to LiveQuery changes -let unsubscribe = planetsLiveQuery.subscribe((update) => { +const unsubscribe = planetsLiveQuery.subscribe((update) => { // Query for results when a change occure update.query(); }); // Unsubscribe from the LiveQuery unsubscribe(); ``` + +:::tip + +If you use a pull based reactive system (for example Glimmer tracking) you can +set debounceLiveQueries option to false on memory cache. +::: diff --git a/website/docs/task-processing.md b/website/docs/task-processing.md index 7adc7885..2c013eb0 100644 --- a/website/docs/task-processing.md +++ b/website/docs/task-processing.md @@ -66,8 +66,8 @@ Tasks are normally added to the end of a queue via the `push` method: ```javascript queue.push({ - type: "query", - data: { expression: { op: "findRecords", type: "planet" } } + type: 'query', + data: { expression: { op: 'findRecords', type: 'planet' } } }); ``` diff --git a/website/docs/updating-data.md b/website/docs/updating-data.md index 2129dac7..84fbfa13 100644 --- a/website/docs/updating-data.md +++ b/website/docs/updating-data.md @@ -11,71 +11,69 @@ succeed or fail together. Operations each represent a single change to a record or relationship (e.g. adding a record, updating a field, deleting a relationship, etc.). -The `Operation` interface requires one member: +The base [`Operation`](./api/data/interfaces/Operation.md) interface consists +of: - `op` - a string identifying the type of operation +- `options` - (Optional) a [`RequestOptions`](./api/data/interfaces/RequestOptions.md) object -The other members of an `Operation` are specific to the `op`. - -The following standard operations are defined in `@orbit/data`: +The other members of an [`Operation`](./api/data/interfaces/Operation.md) are +specific to the `op`. The following standard record-specific operations are +defined in [`@orbit/records`](./api/records/index.md): ```typescript -interface Operation { - op: string; -} - interface AddRecordOperation extends Operation { - op: "addRecord"; - record: Record; + op: 'addRecord'; + record: InitializedRecord; } interface UpdateRecordOperation extends Operation { - op: "updateRecord"; - record: Record; + op: 'updateRecord'; + record: InitializedRecord; } interface RemoveRecordOperation extends Operation { - op: "removeRecord"; + op: 'removeRecord'; record: RecordIdentity; } interface ReplaceKeyOperation extends Operation { - op: "replaceKey"; + op: 'replaceKey'; record: RecordIdentity; key: string; value: string; } interface ReplaceAttributeOperation extends Operation { - op: "replaceAttribute"; + op: 'replaceAttribute'; record: RecordIdentity; attribute: string; - value: any; + value: unknown; } interface AddToRelatedRecordsOperation extends Operation { - op: "addToRelatedRecords"; + op: 'addToRelatedRecords'; record: RecordIdentity; relationship: string; relatedRecord: RecordIdentity; } interface RemoveFromRelatedRecordsOperation extends Operation { - op: "removeFromRelatedRecords"; + op: 'removeFromRelatedRecords'; record: RecordIdentity; relationship: string; relatedRecord: RecordIdentity; } interface ReplaceRelatedRecordsOperation extends Operation { - op: "replaceRelatedRecords"; + op: 'replaceRelatedRecords'; record: RecordIdentity; relationship: string; relatedRecords: RecordIdentity[]; } interface ReplaceRelatedRecordOperation extends Operation { - op: "replaceRelatedRecord"; + op: 'replaceRelatedRecord'; record: RecordIdentity; relationship: string; relatedRecord: RecordIdentity | null; @@ -84,10 +82,11 @@ interface ReplaceRelatedRecordOperation extends Operation { ## Transforms -The `Transform` interface has the following members: +The [`Transform`](./api/data/interfaces/Transform.md) interface has the +following members: - `id` - a string that uniquely identifies the transform -- `operations` - an array of `Operation` objects +- `operations` - an instance or array of [`Operation`](./api/data/interfaces/Operation.md) objects - `options` - an optional object that represents options that can influence how a transform is processed @@ -103,21 +102,21 @@ For instance, here's how you might update a memory source with a single record: ```javascript const earth = { - type: "planet", - id: "earth", + type: 'planet', + id: 'earth', attributes: { - name: "Earth" + name: 'Earth' } }; -memory.update(t => t.addRecord(earth)); +memory.update((t) => t.addRecord(earth)); ``` To perform more than one operation in a single transform, just return an array of operations: ```javascript -memory.update(t => [t.addRecord(earth), t.addRecord(jupiter)]); +memory.update((t) => [t.addRecord(earth), t.addRecord(jupiter)]); ``` ### Standard transforms @@ -126,75 +125,75 @@ You can use the standard `@orbit/data` transform builder as follows: ```javascript // Adding a new record -memory.update(t => +memory.update((t) => t.addRecord({ - type: "planet", - id: "earth", + type: 'planet', + id: 'earth', attributes: { - name: "Earth" + name: 'Earth' } }) ); // Updating a record -memory.update(t => +memory.update((t) => t.updateRecord({ - type: "planet", - id: "earth", + type: 'planet', + id: 'earth', attributes: { - name: "Earth", - classification: "terrestrial", + name: 'Earth', + classification: 'terrestrial', atmosphere: true } }) ); // Removing a record -memory.update(t => t.removeRecord({ type: "planet", id: "earth" })); +memory.update((t) => t.removeRecord({ type: 'planet', id: 'earth' })); // Replacing a key -memory.update(t => - t.replaceKey({ type: "planet", id: "earth" }, "remoteId", "abc123") +memory.update((t) => + t.replaceKey({ type: 'planet', id: 'earth' }, 'remoteId', 'abc123') ); // Replacing an attribute -memory.update(t => +memory.update((t) => t.replaceAttribute( - { type: "planet", id: "earth" }, - "classification", - "gaseous" + { type: 'planet', id: 'earth' }, + 'classification', + 'gaseous' ) ); // Adding a member to a to-many relationship -memory.update(t => - t.addToRelatedRecords({ type: "planet", id: "jupiter" }, "moons", { - type: "moon", - id: "io" +memory.update((t) => + t.addToRelatedRecords({ type: 'planet', id: 'jupiter' }, 'moons', { + type: 'moon', + id: 'io' }) ); // Removing a member from a to-many relationship -memory.update(t => - t.removeFromRelatedRecords({ type: "planet", id: "jupiter" }, "moons", { - type: "moon", - id: "io" +memory.update((t) => + t.removeFromRelatedRecords({ type: 'planet', id: 'jupiter' }, 'moons', { + type: 'moon', + id: 'io' }) ); // Replacing every member of a to-many relationship -memory.update(t => - t.replaceRelatedRecords({ type: "planet", id: "jupiter" }, "moons", [ - { type: "moon", id: "io" }, - { type: "moon", id: "europa" } +memory.update((t) => + t.replaceRelatedRecords({ type: 'planet', id: 'jupiter' }, 'moons', [ + { type: 'moon', id: 'io' }, + { type: 'moon', id: 'europa' } ]) ); // Replacing a to-one relationship -memory.update(t => - t.replaceRelatedRecord({ type: "planet", id: "jupiter" }, "solarSystem", { - type: "solarSystem", - id: "ourSolarSystem" +memory.update((t) => + t.replaceRelatedRecord({ type: 'planet', id: 'jupiter' }, 'solarSystem', { + type: 'solarSystem', + id: 'ourSolarSystem' }) ); ``` @@ -209,18 +208,18 @@ instructions for the source named `remote`: ```javascript memory.update( - t => + (t) => t.updateRecord({ - type: "planet", - id: "earth", + type: 'planet', + id: 'earth', attributes: { - name: "Earth", - classification: "terrestrial", + name: 'Earth', + classification: 'terrestrial', atmosphere: true } }), { - label: "Update planet Earth", + label: 'Update planet Earth', sources: { remote: { timeout: 100000 @@ -241,18 +240,22 @@ timeout when performing this particular update. It is possible to pass different options to each operation in the transform. ```javascript -memory.update(t => [ - t.addRecord({ - type: "planet", - attributes: { - name: "Earth" - } - }).options({ timeout: 1000 }), - t.addRecord({ - type: "planet", - attributes: { - name: "Jupiter" - } - }).options({ timeout: 2000 }) +memory.update((t) => [ + t + .addRecord({ + type: 'planet', + attributes: { + name: 'Earth' + } + }) + .options({ timeout: 1000 }), + t + .addRecord({ + type: 'planet', + attributes: { + name: 'Jupiter' + } + }) + .options({ timeout: 2000 }) ]); ``` diff --git a/website/sidebars.js b/website/sidebars.js index 32d37772..718556c0 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -14,7 +14,6 @@ module.exports = { 'intro', 'getting-started', 'whats-new', - 'packages', 'modeling-data', 'data-sources', 'updating-data', diff --git a/website/versioned_docs/version-0.15/coordination.md b/website/versioned_docs/version-0.15/coordination.md index a361d383..a30ccc69 100644 --- a/website/versioned_docs/version-0.15/coordination.md +++ b/website/versioned_docs/version-0.15/coordination.md @@ -104,7 +104,7 @@ strategies. ### Request strategies -Request strategies participate in the [request flow](./data-flows). Every +Request strategies participate in the [request flow](./data-flows.md). Every request strategy should be defined with: * `source` - the name of the observed source @@ -169,7 +169,7 @@ coordinator.addStrategy(new RequestStrategy({ ### Sync strategies -Sync strategies participate in the [sync flow](./data-flows). Every +Sync strategies participate in the [sync flow](./data-flows.md). Every sync strategy should be defined with: * `source` - the name of the observed source diff --git a/website/versioned_docs/version-0.15/data-sources.md b/website/versioned_docs/version-0.15/data-sources.md index 6495b973..a700e2b3 100644 --- a/website/versioned_docs/version-0.15/data-sources.md +++ b/website/versioned_docs/version-0.15/data-sources.md @@ -114,7 +114,7 @@ The following should be logged as a result: "transforms: 1" ``` -> Want to learn more about updating data? [See the guide](./updating-data) +> Want to learn more about updating data? [See the guide](./updating-data.md) ## Standard interfaces @@ -181,7 +181,7 @@ flows back down. The `Syncable` interface participates in the "sync flow", in which data flowing downstream is synchronized with other sources. -> Want to learn more about data flows? [See the guide](./data-flows) +> Want to learn more about data flows? [See the guide](./data-flows.md) ### Developer-facing interfaces @@ -189,6 +189,6 @@ Generally speaking, only the `Updatable` and `Queryable` interfaces are designed to be used directly by developers in most applications. The other interfaces are used to coordinate data requests and synchronization between sources. -> See guides that cover [querying data](./querying-data), - [updating data](./updating-data), and - [configuring coordination strategies](./coordination). +> See guides that cover [querying data](./querying-data.md), + [updating data](./updating-data.md), and + [configuring coordination strategies](./coordination.md). diff --git a/website/versioned_docs/version-0.15/data-stores.md b/website/versioned_docs/version-0.15/data-stores.md index 12275488..bcefae83 100644 --- a/website/versioned_docs/version-0.15/data-stores.md +++ b/website/versioned_docs/version-0.15/data-stores.md @@ -7,8 +7,8 @@ length throughout this guide already. Stores implement the `Updatable`, `Queryable`, and `Syncable` interfaces, and are the primary interface through which developers will interact with an Orbit application. -Instead of re-explaining [querying](./querying-data) and -[updating](./updating-data) stores, this section explores some unique +Instead of re-explaining [querying](./querying-data.md) and +[updating](./updating-data.md) stores, this section explores some unique capabilities of stores and their inner workings. ## Cache @@ -67,7 +67,7 @@ a high fidelity log of changes to a store, observe its cache's `patch` events. ### Querying cache data -As has been [discussed](./querying-data), the contents of a cache can be +As has been [discussed](./querying-data.md), the contents of a cache can be queried directly and synchronously, using the same query expressions that can be applied to other sources. diff --git a/website/versioned_docs/version-0.16/coordination.md b/website/versioned_docs/version-0.16/coordination.md index 34e4ef38..ad039855 100644 --- a/website/versioned_docs/version-0.16/coordination.md +++ b/website/versioned_docs/version-0.16/coordination.md @@ -101,7 +101,7 @@ strategies. ### Request strategies -Request strategies participate in the [request flow](./data-flows). Every +Request strategies participate in the [request flow](./data-flows.md). Every request strategy should be defined with: - `source` - the name of the observed source @@ -174,7 +174,7 @@ coordinator.addStrategy( ### Sync strategies -Sync strategies participate in the [sync flow](./data-flows). Every +Sync strategies participate in the [sync flow](./data-flows.md). Every sync strategy should be defined with: - `source` - the name of the observed source diff --git a/website/versioned_docs/version-0.16/data-sources.md b/website/versioned_docs/version-0.16/data-sources.md index c1c64ae3..8360765e 100644 --- a/website/versioned_docs/version-0.16/data-sources.md +++ b/website/versioned_docs/version-0.16/data-sources.md @@ -114,7 +114,7 @@ The following should be logged as a result: "transforms: 1"; ``` -> Want to learn more about updating data? [See the guide](./updating-data) +> Want to learn more about updating data? [See the guide](./updating-data.md) ## Standard interfaces @@ -181,7 +181,7 @@ flows back down. The `Syncable` interface participates in the "sync flow", in which data flowing downstream is synchronized with other sources. -> Want to learn more about data flows? [See the guide](./data-flows) +> Want to learn more about data flows? [See the guide](./data-flows.md) ### Developer-facing interfaces @@ -189,6 +189,6 @@ Generally speaking, only the `Updatable` and `Queryable` interfaces are designed to be used directly by developers in most applications. The other interfaces are used to coordinate data requests and synchronization between sources. -> See guides that cover [querying data](./querying-data), -> [updating data](./updating-data), and -> [configuring coordination strategies](./coordination). +> See guides that cover [querying data](./querying-data.md), +> [updating data](./updating-data.md), and +> [configuring coordination strategies](./coordination.md). diff --git a/website/versioned_docs/version-0.16/intro.md b/website/versioned_docs/version-0.16/intro.md index 41519668..4938f9e4 100644 --- a/website/versioned_docs/version-0.16/intro.md +++ b/website/versioned_docs/version-0.16/intro.md @@ -17,9 +17,10 @@ runtime.
-Looking a quick code walkthrough? Check out the [Getting Started Guide](./getting-started). +Looking a quick code walkthrough? Check out the [Getting Started +Guide](./getting-started.md). -Upgrading from v0.15? [Learn what's new](./whats-new). +Upgrading from v0.15? [Learn what's new](./whats-new.md).
diff --git a/website/versioned_docs/version-0.16/memory-sources.md b/website/versioned_docs/version-0.16/memory-sources.md index eb80dcc4..f446d19d 100644 --- a/website/versioned_docs/version-0.16/memory-sources.md +++ b/website/versioned_docs/version-0.16/memory-sources.md @@ -7,8 +7,8 @@ length throughout this guide already. Memory sources implement the `Updatable`, `Queryable`, and `Syncable` interfaces, and are the primary interface through which developers will interact with an Orbit application. -Instead of re-explaining [querying](./querying-data) and -[updating](./updating-data) memory sources, this section explores some unique +Instead of re-explaining [querying](./querying-data.md) and +[updating](./updating-data.md) memory sources, this section explores some unique capabilities of memory sources and their inner workings. ## Cache @@ -67,7 +67,7 @@ a high fidelity log of changes to a memory source, observe its cache's `patch` e ### Querying cache data -As has been [discussed](./querying-data), the contents of a cache can be +As has been [discussed](./querying-data.md), the contents of a cache can be queried directly and synchronously, using the same query expressions that can be applied to other sources. diff --git a/website/versioned_docs/version-0.16/whats-new.md b/website/versioned_docs/version-0.16/whats-new.md index 304202ac..ddc3833d 100644 --- a/website/versioned_docs/version-0.16/whats-new.md +++ b/website/versioned_docs/version-0.16/whats-new.md @@ -47,7 +47,8 @@ returned from the server. If the server is using a complex sorting algorithm, it may be impossible to recreate that same logic (and full dataset) on the client in the `MemorySource`. -Read more about using hints in the guide to [coordination strategies](./coordination#Using-hints). +Read more about using hints in the guide to [coordination +strategies](./coordination.md#using-hints). ## Memory sources can now be "rebased"