diff --git a/README.md b/README.md index cf28a1bd8af..a8ece991f25 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,12 @@ WarpDrive provides features that make it easy to build scalable, fast, feature rich application — letting you ship better experiences more quickly without re-architecting your app or API. WarpDrive is: +- 🌌 Seamless Reactivity in any Framework - ⚡️ Committed to Best-In-Class Performance - 💚 Typed - ⚛️ Works with any API - 🌲 Focused on being as tiny as possible - 🚀 SSR Ready -- 🔜 Seamless reactivity in any framework - 🐹 Built with ♥️ by [Ember](https://emberjs.com)
diff --git a/docs-viewer/docs.warp-drive.io/.vitepress/config.mts b/docs-viewer/docs.warp-drive.io/.vitepress/config.mts index 0325f148988..b0013994c70 100644 --- a/docs-viewer/docs.warp-drive.io/.vitepress/config.mts +++ b/docs-viewer/docs.warp-drive.io/.vitepress/config.mts @@ -37,14 +37,13 @@ export default defineConfig({ // https://vitepress.dev/reference/default-theme-config nav: [ - { text: 'Home', link: '/' }, - { text: 'Guides', link: '/guides' }, + { text: 'Guides', link: '/guides/1-manual/1-introduction' }, { text: 'API', link: '/api' }, ], sidebar: { // This sidebar gets displayed when a user - // is on `guide` directory. + // is on `api-docs` directory. '/api-docs/': [ { text: 'API Documentation', @@ -57,7 +56,7 @@ export default defineConfig({ ], // This sidebar gets displayed when a user - // is on `guide` directory. + // is on `guides` directory. '/guides/': GuidesStructure, }, @@ -66,5 +65,9 @@ export default defineConfig({ { icon: 'discord', link: 'https://discord.gg/zT3asNS' }, { icon: 'bluesky', link: 'https://bsky.app/profile/warp-drive.io' }, ], + + search: { + provider: 'local', + }, }, }); diff --git a/docs-viewer/docs.warp-drive.io/index.md b/docs-viewer/docs.warp-drive.io/index.md index 089c45e8d08..c8972ef1a57 100644 --- a/docs-viewer/docs.warp-drive.io/index.md +++ b/docs-viewer/docs.warp-drive.io/index.md @@ -6,10 +6,13 @@ hero: name: "The Manual" text: Boldly go where no App has gone before tagline: "WarpDrive is the lightweight data framework for ambitious web applications — universal, typed, reactive, and ready to scale." + image: + src: /images/handlers-all-labeled.gif + alt: WarpDrive actions: - theme: brand text: Guides - link: /guides + link: /guides/1-manual/1-introduction - theme: alt text: API Docs link: /api diff --git a/docs-viewer/docs.warp-drive.io/public/images/handlers-all-labeled.gif b/docs-viewer/docs.warp-drive.io/public/images/handlers-all-labeled.gif new file mode 100644 index 00000000000..fa9dd9d284d Binary files /dev/null and b/docs-viewer/docs.warp-drive.io/public/images/handlers-all-labeled.gif differ diff --git a/docs-viewer/src/prepare-website.ts b/docs-viewer/src/prepare-website.ts index 5ba9e8efca7..4dd32b5d34c 100644 --- a/docs-viewer/src/prepare-website.ts +++ b/docs-viewer/src/prepare-website.ts @@ -2,35 +2,28 @@ Symlinks the guides folder to docs.warp-drive.io/guides */ import { join } from 'path'; -import { symlinkSync, existsSync } from 'fs'; +import { existsSync, rmSync } from 'fs'; import { spawnSync } from 'child_process'; -async function main() { +export async function main() { const guidesPath = join(__dirname, '../../guides'); - const symlinkPath = join(__dirname, '../docs.warp-drive.io/guides'); + const copiedPath = join(__dirname, '../docs.warp-drive.io/guides'); // use Bun to create the symlink if it doesn't exist - if (existsSync(symlinkPath)) { - return; + if (existsSync(copiedPath)) { + // remove the symlink if it exists + rmSync(copiedPath, { recursive: true, force: true }); } try { - if (process.env.CI) { - // in CI we do a copy instead of a symlink - // because the symlink will not work in the CI environment - // and we don't want to fail the build - spawnSync('cp', ['-r', guidesPath, symlinkPath], { - stdio: 'inherit', - cwd: __dirname, - }); - console.log(`Copied: ${guidesPath} -> ${symlinkPath}`); - } else { - symlinkSync(guidesPath, symlinkPath); - console.log(`Symlink created: ${guidesPath} -> ${symlinkPath}`); - } + spawnSync('cp', ['-r', guidesPath, copiedPath], { + stdio: 'inherit', + cwd: __dirname, + }); + console.log(`Copied: ${guidesPath} -> ${copiedPath}`); } catch (error) { - console.error('Error creating symlink:', error); + console.error('Error copying directory:', error); } } diff --git a/docs-viewer/src/site-utils.ts b/docs-viewer/src/site-utils.ts index 850315598c4..b2212208b92 100644 --- a/docs-viewer/src/site-utils.ts +++ b/docs-viewer/src/site-utils.ts @@ -1,5 +1,4 @@ import path from 'path'; -// @ts-expect-error missing in node types import { globSync } from 'node:fs'; function segmentToTitle(segment: string) { @@ -12,22 +11,37 @@ function segmentToTitle(segment: string) { return result === 'Index' ? 'Introduction' : result; } +function segmentToIndex(segment: string, index: number) { + const value = segment.split('-').map((s) => s.charAt(0).toUpperCase() + s.slice(1)); + if (!isNaN(Number(value[0]))) { + return Number(value[0]); + } + return index; +} + export async function getGuidesStructure() { const GuidesDirectoryPath = path.join(__dirname, '../../guides'); - const glob = globSync('**/*.md', { cwd: GuidesDirectoryPath }) as string[]; - + const glob = globSync('**/*.md', { cwd: GuidesDirectoryPath }); const groups: Record = {}; for (const filepath of glob) { const segments = filepath.split(path.sep); const lastSegment = segments.pop()!; + + if (lastSegment.startsWith('0-')) { + // skip hidden files + continue; + } + let group = groups; for (let i = 0; i < segments.length; i++) { const segment = segments[i]; if (!group[segment]) { + const existing = Object.keys(group); group[segment] = { text: segmentToTitle(segment), + index: segmentToIndex(segment, existing.length), collapsed: true, items: {}, }; @@ -35,17 +49,18 @@ export async function getGuidesStructure() { group = group[segment].items; } + // add the last segment to the group + const existing = Object.keys(group); group[lastSegment] = { text: segmentToTitle(lastSegment), + index: segmentToIndex(lastSegment, existing.length), link: `/guides/${filepath.replace(/\.md$/, '')}`, }; } // deep iterate converting items objects to arrays const result = deepConvert(groups); - if (process.env.CI) { - console.log(JSON.stringify(result, null, 2)); - } + console.log(JSON.stringify(result, null, 2)); return result; } @@ -57,5 +72,7 @@ function deepConvert(obj: Record) { group.items = deepConvert(group.items); } } - return groups; + return groups.sort((a, b) => { + return a.index < b.index ? -1 : a.index > b.index ? 1 : 0; + }); } diff --git a/docs-viewer/src/start-guides-sync.ts b/docs-viewer/src/start-guides-sync.ts new file mode 100644 index 00000000000..8748eb4b96e --- /dev/null +++ b/docs-viewer/src/start-guides-sync.ts @@ -0,0 +1,26 @@ +import { watch } from 'fs'; +import { main } from './prepare-website'; +import { join } from 'path'; + +const guidesPath = join(__dirname, '../../guides'); + +let debounce: ReturnType | null = null; + +watch( + guidesPath, + { + recursive: true, + }, + (eventName: 'rename' | 'change', fileName: string) => { + console.log('triggered', eventName, fileName); + if (debounce) { + console.log('debounced'); + clearTimeout(debounce); + } + debounce = setTimeout(() => { + console.log('rebuilding'); + main(); + debounce = null; + }, 10); + } +); diff --git a/guides/index.md b/guides/0-index.md similarity index 100% rename from guides/index.md rename to guides/0-index.md diff --git a/guides/1-manual/0-index.md b/guides/1-manual/0-index.md new file mode 100644 index 00000000000..913e234af89 --- /dev/null +++ b/guides/1-manual/0-index.md @@ -0,0 +1,18 @@ +# The Manual + +## Table Of Contents + +1) [Overview](./1-overview.md) +2) [Making Requests](./2-requests.md) + + diff --git a/guides/1-manual/1-introduction.md b/guides/1-manual/1-introduction.md new file mode 100644 index 00000000000..bcff7602200 --- /dev/null +++ b/guides/1-manual/1-introduction.md @@ -0,0 +1,226 @@ + + + + + + + +
+ +[Making Requests →](./2-requests.md) + +
+ +# Introduction + +***Warp*Drive** is the data framework for building ambitious applications. + +By ambitious, we mean that ***Warp*Drive** is ideal for both small and large applications that strive to be best-in-class. ***Warp*Drive** seamlessly handles and simplifies the hardest parts of state management when building an app, helping you focus on creating the features and user experiences that drive value. + +### Reactivity that Just Works + +```hbs +Hello {{@user.name}}! +``` + +Our innovative approach to [fine grained reactivity](https://dev.to/ryansolid/a-hands-on-introduction-to-fine-grained-reactivity-3ndf) enables rapidly developing robust, performant web applications using any [Signals](https://github.com/tc39/proposal-signals#readme) compatible framework such as [Ember](https://guides.emberjs.com/release/in-depth-topics/autotracking-in-depth/), [Svelte](https://svelte.dev/docs/svelte/what-are-runes), [Angular](https://angular.dev/guide/signals), [Vue.js](https://vuejs.org/guide/extras/reactivity-in-depth.html), [SolidJS](https://www.solidjs.com/tutorial/introduction_signals), +[Preact](https://preactjs.com/guide/v10/signals/) or [Lit](https://lit.dev/docs/data/signals/). + +### Requests without the Fuss + +```ts +const { content } = await store.request({ + url: '/api/users' +}); +``` + +By building around the same interface as the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), ***Warp*Drive** makes powerful request management features like caching, deduping, errors and data normalization feel simple to use. + +

+ 💚 Fully Typed! +

+ +```ts +const { content } = await store.request({ url: '/api/users/1' }); +``` + +`request` takes a generic that can be used to set the return type of the content of the associated request. Builders – functions that return a RequestInit object – can supply the return type via a special [brand](https://egghead.io/blog/using-branded-types-in-typescript). This brand will be automatically inferred when using the `RequestInfo` return type. + +```ts +import type { RequestInfo } from '@warp-drive/core-types/request'; +import type { User } from './types/data'; + +export function getUser(id: string): RequestInfo { + return { + method: 'GET', + url: `/api/users/${id}`, + }; +} + +// ... + +const { content } = await store.request(getUser('1')); +``` + + +We pair this with [reactive control flow](./concepts/reactive-control-flow.md) to give apps the ability to declaratively derive states with safety. + +### Build Quickly and Robustly with Reactive Control Flow + +```glimmer-ts +import { Request } from '@warp-drive/ember'; +import { findRecord } from '@ember-data/json-api/request'; +import { Spinner } from './spinner'; + +export default +``` + +### ORM Powers without ORM Problems + +```ts +const { content } = await store.request({ + url: '/api/user/1?include=organizations' +}); + +content.data.organizations.map(organization => { + +}); +``` + +**Web clients are like high-latency, remotely distributed, often-stale partial replicas of server state**. ***Warp*Drive** provides an [advanced relational cache](./5-caching.md) that simplifies these problems--solving them when it can and providing intelligent escape valves for when it can't. No matter what, you can quickly **get the data you need in the right state**. + +### Schema Driven Reactivity + +***Warp*Drive**'s reactive objects transform raw cached data into rich, reactive data. The resulting objects are immutable, always displaying the latest state in the cache while preventing accidental or unsafe mutation in your app. The output and [transformation](./concepts/transformation.md) is controlled by a simple JSON [ResourceSchema](./concepts/schemas.md). + +```ts +import { withDefaults } from '@warp-drive/schema-record'; + +store.schema.registerResource( + withDefaults({ + type: 'user', + fields: [ + { kind: 'field', name: 'firstName' }, + { kind: 'field', name: 'lastName' }, + { kind: 'field', name: 'birthday', type: 'luxon-date' }, + { + kind: 'derived', + name: 'age', + type: 'duration', + options: { field: 'birthday', format: 'y' } + }, + { + kind: 'derived', + name: 'fullName', + type: 'concat', + options: { fields: ['firstName', 'lastName'], separator: ' ' } + }, + ] + }) +) +``` + +### Immutability Without The Performative Hassle + + +### Mutation Management + +[Mutation](./concepts/mutations.md) is handled within controlled contexts. The data to edit is "checked out" for editing, giving access to a mutable version. Local edits are seamlessly preserved if the user navigates away and returns without saving, and the changes are buffered from appearing elsewhere in your app until they are also committed to the server. + +```ts +import { Checkout } from '@warp-drive/schema-record'; + +// ... + +const editable = await user[Checkout](); +editable.firstName = 'Chris'; +``` + +***Warp*Drive** is only semi-opinionated about your API. Almost every API is compatible just by authoring a [request handler](./concepts/handlers.md) to ensure that the responses are normalized into the cache format. + +```ts +const NormalizeKeysHandler = { + request(context, next) { + return next(context.request).then((result) => { + return convertUnderscoredKeysToCamelCase(result.content); + }); + } +} +``` + +***Warp*Drive** offers both a JS and a Component based way of making requests and working with the result. Above we saw +how to generate a request in component form. Here's how we can generate the same request using plain JavaScript. + +```ts + +// setup a store for the app +import Store from '@ember-data/store'; +import RequestManager from '@ember-data/request'; +import Fetch from '@ember-data/request/fetch'; + +class AppStore extends Store { + requestManager = new RequestManager().use([Fetch]) +} + +// --------------- + +// make use of the store +import { findRecord } from '@ember-data/json-api/request'; + +const request = store.request( + findRecord('user', this.args.id) +); + +const result = await request; +``` + +You may be thinking "what is store and where did that come from"? The [store]() helps us to manage our data and cache responses. The store is something that you will configure for your application. Our component usage above is also using our application's store, a detail we will explore further in later sections. + +If using an app-specific cache format would work better for the demands of your API, the [cache](https://github.com/emberjs/data/blob/main/packages/core-types/src/cache.ts) the store should use is customizable: + +```ts +import Store from '@ember-data/store'; +import { CustomCache } from './my-custom-cache'; + +class AppStore extends Store { + createCache(capabilities) { + return new CustomCache(capabilities); + } +} +``` + +Realtime subscriptions are supported through an extensive list of [operations](./concepts/operations.md) for surgically updating cache state, as well as by a comprehensive [notifications service]() which alerts us to when data has been added, updated or removed from the cache allowing subscriptions to dynamically adjust as needed. + +```ts +store.cache.patch({ + op: 'add', + record: User1Key, + field: 'friends', + value: User2Key +}); +``` + +***Warp*Drive** has been designed as a series of interfaces following the single-responsibility principle with well defined boundaries and configuration points. Because of this, nearly every aspect of the library is configurable, extensible, composable, replaceable or all of the above: meaning that if something doesn't meet your needs out-of-the-box, you can configure it to. + +The list of features doesn't end here. This guide will teach you the basics of everything you need to know, but if you find yourself needing more help or with a question you can't find the answer to, ask on [GitHub](https://github.com/emberjs/data/issues), in our [forum](https://discuss.emberjs.com/) or on [discord](https://discord.gg/zT3asNS). + + +
+ diff --git a/guides/manual/1-overview.md b/guides/1-manual/11-about.md similarity index 75% rename from guides/manual/1-overview.md rename to guides/1-manual/11-about.md index c43afbe3a31..e8b3da7eab3 100644 --- a/guides/manual/1-overview.md +++ b/guides/1-manual/11-about.md @@ -1,25 +1,63 @@ -| | | -| -- | -- | -| [← Table of Contents](./0-index.md#table-of-contents)                        |                       [Making Requests →](./2-requests.md) | + + + + + -WarpDrive is a suite of features built around orchestrated data-fetching. + + + +
+ +[❖ Table of Contents](./0-index.md) -## Introduction + + +[Overview →](./2-overview.md) + +
+ +## Why WarpDrive? + +WarpDrive is the data framework for building ambitious applications. + +What do we mean by ambitious? WarpDrive is ideal for applications looking to +be best-in-class: whether that's a small todo app, e-commerce, a +social app, or an enterprise b2b software solution. + +That's because WarpDrive is designed to seamlessly handle the hardest parts +of state management when building an app, helping you focus on creating the +features and user experiences that drive value. At its most basic, it is "managed fetch". At its most advanced it is a powerful local-first or offline-first solution that dedupes and reactively updates requests across tabs. +Our value goes beyond our feature set. WarpDrive embraces the platform, making it +quick to pickup the basics. Our patterns are portable and scalable, meaning that as +your app, team and data needs evolve we'll be right there with you. + Usage across various rendering frameworks will be similar. In fact, this is an explicit goal: WarpDrive enables developers to quickly port and re-use data patterns. +Because we are universal and also not tied to any API Format or backend architecture, +there's no lock-in. The data patterns you learn and the code you write is portable +between frontend frameworks and backend APIs and can help smooth the evolution of both. + +We're also not specific to a given frontend architecture. When serving on the same +domain, you can dedupe and cache requests across multiple apps and tabs at once! +This means we are as good for embedded content and MPAs as we are for SPAs. + +A single WarpDrive configuration can power multiple web-apps using differing +frameworks all sharing a single domain: bridging the gap between MPA and SPA. + We see this as one of the keys to scalability. Providing a stable framework for how data is requested, cached, mutated, and mocked allows developers to focus more time on the product requirements that matter. -A single WarpDrive configuration can power multiple web-apps using differing -frameworks all sharing a single domain: bridging the gap between MPA and SPA. +Our core philosophy is to deliver value that lasts decades and evolves with your app, +helping you ship, iterate and deliver to your customers.
@@ -37,35 +75,19 @@ decades.
-## Why WarpDrive? - -WarpDrive is the data framework for building ambitious applications. +--- -What do we mean by ambitious? WarpDrive is ideal for applications looking to -be best-in-class: whether that's a small todo app, e-commerce, a -social app, or an enterprise b2b software solution. - -That's because WarpDrive is designed to seamlessly handle the hardest parts -of state management when building an app, helping you focus on creating the -features and user experiences that drive value. - -Our value goes beyond our feature set. WarpDrive embraces the platform, making it -quick to pickup the basics. Our patterns are portable and scalable, meaning that as -your app, team and data needs evolve we'll be right there with you. - -Because we are universal and also not tied to any API Format or backend architecture, -there's no lock-in. The data patterns you learn and the code you write is portable -between frontend frameworks and backend APIs and can help smooth the evolution of both. - -We're also not specific to a given frontend architecture. When serving on the same -domain, you can dedupe and cache requests across multiple apps and tabs at once! -This means we are as good for embedded content and MPAs as we are for SPAs. +
-Our core philosophy is to deliver value that lasts decades and evolves with your app, -helping you ship, iterate and deliver to your customers. + + + + + + + +
-
+[Overview →](./2-overview.md) -| | | -| -- | -- | -| [← Table of Contents](./0-index.md#table-of-contents)                        |                       [Making Requests →](./2-requests.md) | +
diff --git a/guides/1-manual/2-requests.md b/guides/1-manual/2-requests.md new file mode 100644 index 00000000000..ff8965bbe39 --- /dev/null +++ b/guides/1-manual/2-requests.md @@ -0,0 +1,291 @@ + + + + + + + +
+ +[← Introduction](./1-introduction.md) + + + +[Key Data Structures →](./4-data.md) + +
+ +# Making Requests + +Requests are how your application fetches or updates data stored remotely. + +```ts +const { content } = await store.request({ + url: '/api/users' +}); +``` + +### What does remote mean? + +Most commonly remote data refers to data that is stored on your server and accessed and updated via your backend API. + +But it doesn't have to be! Remote really boils down to [persistence](https://en.wikipedia.org/wiki/Persistence_(computer_science)) - the ability for data to be reliably stored someplace so that it can be found again at a later time. + +Common examples of persistent or remote data sources that aren't accessed via connecting to a server are the [File System](https://developer.mozilla.org/en-US/docs/Web/API/File_System_API/Origin_private_file_system), browser managed storage mediums such as [IndexedDB](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) and [localStorage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage), or [WebAssembly](https://webassembly.org/) builds of [sqlite3](https://sqlite.org/wasm/doc/trunk/index.md). + +### Request Options + +*Warp***Drive** uses the native [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) interfaces for [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) as the foundation upon which requests are made. This ensures that if the platform supports it, WarpDrive exposes it: platform APIs are never hidden away. + +```ts +const { content } = await store.request({ + url: '/api/users', + method: 'POST', + headers: new Headers({ + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }), + body: JSON.stringify({ + filter: { + $in: { teams: ['1', '9', '42'] } + }, + search: { name: 'chris' } + }) +}); +``` + +Of course, writing requests could get repetitive. This is where [builders]() help out. Builders are simple functions that produce the json request object. + +```ts +function queryUsers(query) { + return { + url: '/api/users', + method: 'POST', + headers: new Headers({ + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }), + body: JSON.stringify(query) + } +} + +const { content } = await store.request( + queryUsers({ + filter: { + $in: { teams: ['1', '9', '42'] } + }, + search: { name: 'chris' } + }) +) +``` + +Builders make it easy to quickly write shareable, reusable requests with [typed responses]() that mirror your application's capabilities. + +### Do requests have to use fetch? + +Requests do not need to use fetch! The native [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) interface provides a convenient, feature-rich way to describe the data you want to retrieve or update – but ultimately request handlers get to decide how that occurs. + +Request handlers can be used to connect to any data source via any mechanism. Besides fetch, this might be localStorage, [XMLHttpRequest](https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest), [WebSockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket), [ServerEvents](https://developer.mozilla.org/en-US/docs/Web/API/EventSource), [MessageChannel](https://developer.mozilla.org/en-US/docs/Web/API/MessageChannel), or something else entirely! + +

+ a flow diagram showing data resolving from server via a chain of request handlers +

+ + + +WarpDrive offers both a typed JS approach to making requests and a declarative component approach. + + + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +```ts +const result = await store.request({ + url: '/api/users?search=Chris' +}) +``` + +```glimmer-ts +const UsersSearch = { + url: '/api/users?search=Chris' +}); + +export default +``` + +Every request three distinct parts + +- *the request* - the object we construct to describe what it is we want to do +- *fulfillment* - how we go about making the request do the thing we need it to do +- *the response* - the processed result once the request is complete + +### Fetch Example + +> [!TIP] +> When we want to show integration with a framework, this tutorial +> uses [EmberJS](https://emberjs.com), a powerful modern web framework with an established multi-decade legacy. + +
+ +*Run This Example* → [Request | Fetch Example](https://warpdrive.nullvoxpopuli.com/manual/requests/fetch) + +
+ +Data fetching is managed by a `RequestManager`, which executes handlers you provide. + +In order to make requests, first we create a `RequestManager` for our +application, and we tell it to fulfill requests using a `Fetch` handler. + +```ts +import RequestManager from '@ember-data/request'; +import { Fetch } from '@ember-data/request/fetch'; + +const manager = new RequestManager().use([Fetch]); +``` + +Now we can issue a request for a list of our users: + +```ts +const { content } = await manager.request({ url: '/users' }); + +for (const user of content.data) { + greet(`${user.firstName} ${user.lastName}`); +} +``` + +If we wanted to type the above request, we could supply a type for the +content returned by the request: + +```ts +type User = { + id: string; + firstName: string; + lastName: string; + age: number; +}; + +type UsersQuery = { + data: User[]; +} +``` + +And use it like this: + +```ts +const { content } = await manager.request({ url: '/users' }); +``` + +> [!TIP] +> Manually supplying the generic is NOT the preferred way +> to type a request, look for the section on [builders](./7-builders.md) +> later. + +
+ +### The Chain of Responsibility + +When we created the request manager for our application above, you may have noticed that when we told it to fulfill requests using the `Fetch` handler we did so by passing in an array: + +```ts +new RequestManager().use([Fetch]); +``` + +The request manager follows the [chain-of-responsibility pattern](https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern): each handler in our array may choose to fulfill the request, modify it, or pass it along unchanged to the next handler in the array, in array order. + + +```mermaid +--- +config: + theme: neutral +--- +flowchart LR + C(Handler 1) + C <==> D(Handler 2) + D <==> E(Handler 3) + C <--> F@{ shape: lin-cyl, label: "Source A" } + D <--> G@{ shape: lin-cyl, label: "Source B" } + E <--> H@{ shape: lin-cyl, label: "Source C" } +``` + + +A handler receives the request `context` as well as a `next` function with which to pass along +a request if it so chooses. + +```ts +type NextFn = (req: RequestInfo) => Future; + +type Handler = { + request(context: RequestContext, next: NextFn): Promise | Future; +} +``` + +`next` returns a Future, which is a promise with a few additional capabilities. Futures resolve +with the response from the next handler in the chain. This allows a handler to read or modify +the response if it wants. + +> [!Important] +> requests are immutable, to modify one the handler must create a new request, copying over or +> cloning the parts it wants to leave unchanged. + +```ts +type NextFn = (req: RequestInfo) => Future; + +type Handler = { + request(context: RequestContext, next: NextFn): Promise | Future; +} +``` + +A handler may be any object with a `request` method. This allows both stateful and non-stateful handlers to be utilized. + +> [!TIP] +> Handlers should take care of the most generalizable concerns. In general great handlers +> - apply to many-if-not-all requests +> - have a clear heuristic by which they activate (a header, op-code, option, or url scheme) +> - don't block response streaming (we'll talk about this later) + +
diff --git a/guides/manual/3-data.md b/guides/1-manual/4-data.md similarity index 100% rename from guides/manual/3-data.md rename to guides/1-manual/4-data.md diff --git a/guides/manual/4-caching.md b/guides/1-manual/5-caching.md similarity index 100% rename from guides/manual/4-caching.md rename to guides/1-manual/5-caching.md diff --git a/guides/manual/5-presentation.md b/guides/1-manual/6-presentation.md similarity index 100% rename from guides/manual/5-presentation.md rename to guides/1-manual/6-presentation.md diff --git a/guides/manual/7-builders.md b/guides/1-manual/7-builders.md similarity index 100% rename from guides/manual/7-builders.md rename to guides/1-manual/7-builders.md diff --git a/guides/manual/6-schemas.md b/guides/1-manual/8-schemas.md similarity index 100% rename from guides/manual/6-schemas.md rename to guides/1-manual/8-schemas.md diff --git a/guides/1-manual/concepts/builders.md b/guides/1-manual/concepts/builders.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/guides/1-manual/concepts/handlers.md b/guides/1-manual/concepts/handlers.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/guides/1-manual/concepts/mutations.md b/guides/1-manual/concepts/mutations.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/guides/1-manual/concepts/normalization.md b/guides/1-manual/concepts/normalization.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/guides/1-manual/concepts/operations.md b/guides/1-manual/concepts/operations.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/guides/1-manual/concepts/reactive-control-flow.md b/guides/1-manual/concepts/reactive-control-flow.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/guides/1-manual/concepts/requests.md b/guides/1-manual/concepts/requests.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/guides/1-manual/concepts/schemas.md b/guides/1-manual/concepts/schemas.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/guides/1-manual/concepts/serialization.md b/guides/1-manual/concepts/serialization.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/guides/1-manual/concepts/transformation.md b/guides/1-manual/concepts/transformation.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/guides/1-manual/images/handlers-all-labeled.gif b/guides/1-manual/images/handlers-all-labeled.gif new file mode 100644 index 00000000000..fa9dd9d284d Binary files /dev/null and b/guides/1-manual/images/handlers-all-labeled.gif differ diff --git a/guides/1-manual/images/handlers-cloud-resolution.gif b/guides/1-manual/images/handlers-cloud-resolution.gif new file mode 100644 index 00000000000..74cd4b31689 Binary files /dev/null and b/guides/1-manual/images/handlers-cloud-resolution.gif differ diff --git a/guides/1-manual/images/handlers-early-resolution.gif b/guides/1-manual/images/handlers-early-resolution.gif new file mode 100644 index 00000000000..ce4dae20022 Binary files /dev/null and b/guides/1-manual/images/handlers-early-resolution.gif differ diff --git a/guides/1-manual/images/handlers-local-resolution.gif b/guides/1-manual/images/handlers-local-resolution.gif new file mode 100644 index 00000000000..4aad06b2973 Binary files /dev/null and b/guides/1-manual/images/handlers-local-resolution.gif differ diff --git a/guides/typescript/0-installation.md b/guides/2-typescript/0-installation.md similarity index 100% rename from guides/typescript/0-installation.md rename to guides/2-typescript/0-installation.md diff --git a/guides/typescript/1-configuration.md b/guides/2-typescript/1-configuration.md similarity index 100% rename from guides/typescript/1-configuration.md rename to guides/2-typescript/1-configuration.md diff --git a/guides/typescript/2-why-brands.md b/guides/2-typescript/2-why-brands.md similarity index 100% rename from guides/typescript/2-why-brands.md rename to guides/2-typescript/2-why-brands.md diff --git a/guides/typescript/3-typing-models.md b/guides/2-typescript/3-typing-models.md similarity index 100% rename from guides/typescript/3-typing-models.md rename to guides/2-typescript/3-typing-models.md diff --git a/guides/typescript/4-typing-requests.md b/guides/2-typescript/4-typing-requests.md similarity index 94% rename from guides/typescript/4-typing-requests.md rename to guides/2-typescript/4-typing-requests.md index e96cb184f16..8c71dafb96e 100644 --- a/guides/typescript/4-typing-requests.md +++ b/guides/2-typescript/4-typing-requests.md @@ -26,8 +26,7 @@ The signature for `request` will infer the generic for the content type from a s ```ts import type { RequestSignature } from '@warp-drive/core-types/symbols'; -import type { CollectionRecordArray } from '@ember-data/store/types'; -import type User from '../models/user'; +import type { User } from '../types/data/user'; type MyRequest { // ... diff --git a/guides/typescript/5-typing-includes.md b/guides/2-typescript/5-typing-includes.md similarity index 100% rename from guides/typescript/5-typing-includes.md rename to guides/2-typescript/5-typing-includes.md diff --git a/guides/typescript/index.md b/guides/2-typescript/index.md similarity index 100% rename from guides/typescript/index.md rename to guides/2-typescript/index.md diff --git a/guides/requests/examples/0-basic-usage.md b/guides/3-requests/examples/0-basic-usage.md similarity index 100% rename from guides/requests/examples/0-basic-usage.md rename to guides/3-requests/examples/0-basic-usage.md diff --git a/guides/requests/examples/1-auth.md b/guides/3-requests/examples/1-auth.md similarity index 89% rename from guides/requests/examples/1-auth.md rename to guides/3-requests/examples/1-auth.md index d19d7bece53..d7a0602e998 100644 --- a/guides/requests/examples/1-auth.md +++ b/guides/3-requests/examples/1-auth.md @@ -49,20 +49,15 @@ const AuthHandler: Handler = { } ``` -This handler would need to be added to request manager service configuration: +This handler would need to be added to the request manager configuration: ```ts import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; import AuthHandler from './auth-handler.js'; -export default class extends RequestManager { - constructor(args?: Record) { - super(args); - - this.use([AuthHandler, Fetch]); - } -} +const manager = new RequestManager() + .use([AuthHandler, Fetch]); ``` This way every request that was made using this request manager will have `Authorization` header added to it. @@ -104,17 +99,16 @@ To use this handler we need to register it in our request manager service, but a ```ts import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; -import { getOwner, setOwner } from '@ember/application'; +import { setOwner } from '@ember/owner'; import AuthHandler from './auth-handler'; -export default class extends RequestManager { - constructor(args?: Record) { - super(args); - +export default { + create(owner) { const authHandler = new AuthHandler(); - setOwner(authHandler, getOwner(this)); + setOwner(authHandler, owner); - this.use([authHandler, Fetch]); + return new RequestManager() + .use([authHandler, Fetch]); } } ``` @@ -168,20 +162,15 @@ const AuthHandler: Handler = { } ``` -This handler would need to be added to request manager service configuration: +This handler would need to be added to request manager configuration: ```ts import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; import AuthHandler from './auth-handler'; -export default class extends RequestManager { - constructor(args?: Record) { - super(args); - - this.use([AuthHandler, Fetch]); - } -} +const manager = new RequestManager() + .use([AuthHandler, Fetch]); ``` This way every request that was made using this request manager will have `X-CSRF-Token` header added to it when needed. diff --git a/guides/requests/index.md b/guides/3-requests/index.md similarity index 96% rename from guides/requests/index.md rename to guides/3-requests/index.md index 16bfeef7687..01235c67a6a 100644 --- a/guides/requests/index.md +++ b/guides/3-requests/index.md @@ -293,39 +293,42 @@ In the case of the `Future` being returned, `Stream` proxying is automatic and i --- -### Using as a Service +#### Using with `@ember-data/store` -Most applications will desire to have a single `RequestManager` instance, which can be achieved using module-state patterns for singletons, or for [Ember](https://emberjs.com) applications by exporting the manager as a [service](https://guides.emberjs.com/release/services/). +To have a request service unique to a Store: -*services/request.ts* ```ts +import Store, { CacheHandler } from '@ember-data/store'; import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; -import Auth from 'app/services/ember-data-handler'; -export default class extends RequestManager { - constructor(args?: Record) { - super(args); - this.use([Auth, Fetch]); - } +class extends Store { + requestManager = new RequestManager() + .use([Fetch]) + .useCache(CacheHandler); } ``` --- -#### Using with `@ember-data/store` -To have a request service unique to a Store: +### Using as a Service + +Some applications will desire to have direct service-level access to the `RequestManager`, which can be achieved using module-state patterns for singletons, or for [Ember](https://emberjs.com) applications by exporting the manager as a [service](https://guides.emberjs.com/release/services/). +*services/request.ts* ```ts -import Store, { CacheHandler } from '@ember-data/store'; +import { CacheHandler } from '@ember-data/store'; import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; +import Auth from 'app/services/ember-data-handler'; -class extends Store { - requestManager = new RequestManager() - .use([Fetch]) - .useCache(CacheHandler); +export default { + create() { + return new RequestManager() + .use([Auth, Fetch]) + .useCache(CacheHandler); + } } ``` diff --git a/guides/requests/overview/0-intro.md b/guides/3-requests/overview/0-intro.md similarity index 100% rename from guides/requests/overview/0-intro.md rename to guides/3-requests/overview/0-intro.md diff --git a/guides/requests/overview/1-request-management.md b/guides/3-requests/overview/1-request-management.md similarity index 100% rename from guides/requests/overview/1-request-management.md rename to guides/3-requests/overview/1-request-management.md diff --git a/guides/reactive-data/index.md b/guides/4-reactivity/index.md similarity index 100% rename from guides/reactive-data/index.md rename to guides/4-reactivity/index.md diff --git a/guides/reactive-data/legacy/overview.md b/guides/4-reactivity/legacy/overview.md similarity index 100% rename from guides/reactive-data/legacy/overview.md rename to guides/4-reactivity/legacy/overview.md diff --git a/guides/reactive-data/polaris/overview.md b/guides/4-reactivity/polaris/overview.md similarity index 100% rename from guides/reactive-data/polaris/overview.md rename to guides/4-reactivity/polaris/overview.md diff --git a/guides/relationships/configuration/0-one-to-none.md b/guides/5-relational-data/configuration/0-one-to-none.md similarity index 100% rename from guides/relationships/configuration/0-one-to-none.md rename to guides/5-relational-data/configuration/0-one-to-none.md diff --git a/guides/relationships/configuration/1-one-to-one.md b/guides/5-relational-data/configuration/1-one-to-one.md similarity index 100% rename from guides/relationships/configuration/1-one-to-one.md rename to guides/5-relational-data/configuration/1-one-to-one.md diff --git a/guides/relationships/configuration/2-one-to-many.md b/guides/5-relational-data/configuration/2-one-to-many.md similarity index 100% rename from guides/relationships/configuration/2-one-to-many.md rename to guides/5-relational-data/configuration/2-one-to-many.md diff --git a/guides/relationships/configuration/3-many-to-none.md b/guides/5-relational-data/configuration/3-many-to-none.md similarity index 100% rename from guides/relationships/configuration/3-many-to-none.md rename to guides/5-relational-data/configuration/3-many-to-none.md diff --git a/guides/relationships/configuration/4-many-to-one.md b/guides/5-relational-data/configuration/4-many-to-one.md similarity index 100% rename from guides/relationships/configuration/4-many-to-one.md rename to guides/5-relational-data/configuration/4-many-to-one.md diff --git a/guides/relationships/configuration/5-many-to-many.md b/guides/5-relational-data/configuration/5-many-to-many.md similarity index 100% rename from guides/relationships/configuration/5-many-to-many.md rename to guides/5-relational-data/configuration/5-many-to-many.md diff --git a/guides/relationships/features/inverses.md b/guides/5-relational-data/features/inverses.md similarity index 100% rename from guides/relationships/features/inverses.md rename to guides/5-relational-data/features/inverses.md diff --git a/guides/relationships/features/links-mode.md b/guides/5-relational-data/features/links-mode.md similarity index 100% rename from guides/relationships/features/links-mode.md rename to guides/5-relational-data/features/links-mode.md diff --git a/guides/relationships/features/polymorphism.md b/guides/5-relational-data/features/polymorphism.md similarity index 100% rename from guides/relationships/features/polymorphism.md rename to guides/5-relational-data/features/polymorphism.md diff --git a/guides/relationships/index.md b/guides/5-relational-data/index.md similarity index 100% rename from guides/relationships/index.md rename to guides/5-relational-data/index.md diff --git a/guides/migrating/two-store-migration.md b/guides/6-migrating/two-store-migration.md similarity index 100% rename from guides/migrating/two-store-migration.md rename to guides/6-migrating/two-store-migration.md diff --git a/guides/cookbook/incremental-adoption-guide.md b/guides/7-cookbook/incremental-adoption-guide.md similarity index 87% rename from guides/cookbook/incremental-adoption-guide.md rename to guides/7-cookbook/incremental-adoption-guide.md index 94c97fe0a86..ccd027f1509 100644 --- a/guides/cookbook/incremental-adoption-guide.md +++ b/guides/7-cookbook/incremental-adoption-guide.md @@ -16,11 +16,8 @@ Here is how you do it: ```js // eslint-disable-next-line ember/use-ember-data-rfc-395-imports import Store from 'ember-data/store'; -import { service } from '@ember/service'; -export default class MyStore extends Store { - @service requestManager; -} +export default class AppStore extends Store {} ``` @@ -30,15 +27,19 @@ Notice we still want to import the `Store` class from `ember-data/store` package > Note: Because we are extending `ember-data/store`, it is still v1 addon, so things might not work for you if you are using typescript. We recommend to have `store.js` file for now. -## Step 3: Add `RequestManager` service to your application +## Step 3: Add `RequestManager` to your application -Now let's create our very own `RequestManager` service. It is a service that is responsible for sending requests to the server. It is a composable class, which means you can add your own request handlers to it. +Now let's configure a `RequestManager` for our store. The RequestManager is responsible for sending requests to the server. It fulfills requests using a chain-of-responsibility pipeline, which means you can add your own request handlers to it. -First you need to install [`@ember-data/request`](https://github.com/emberjs/data/tree/main/packages/request) and [`@ember-data/legacy-compat`](https://github.com/emberjs/data/tree/main/packages/legacy-compat) packages. First contains the `RequestManager` service and a few request handlers, second has `LegacyNetworkHandler` that gonna handle all old-style `this.store.*` calls. +First you need to install [`@ember-data/request`](https://github.com/emberjs/data/tree/main/packages/request) and [`@ember-data/legacy-compat`](https://github.com/emberjs/data/tree/main/packages/legacy-compat) packages. The first contains the `RequestManager` service and a few request handlers, while the second has `LegacyNetworkHandler` that will handle all old-style `this.store.*` calls. Here is how your own `RequestManager` service may look like: ```ts +// eslint-disable-next-line ember/use-ember-data-rfc-395-imports +import Store from 'ember-data/store'; + +import { CacheHandler } from '@ember-data/store'; import { LegacyNetworkHandler } from '@ember-data/legacy-compat'; import type { Handler, NextFn, RequestContext } from '@ember-data/request'; import RequestManager from '@ember-data/request'; @@ -54,12 +55,12 @@ const TestHandler: Handler = { }, }; -export default class Requests extends RequestManager { - constructor(args?: Record) { - super(args); - this.use([LegacyNetworkHandler, TestHandler, Fetch]); - } +export default class AppStore extends Store { + requestManager = new RequestManager() + .use([LegacyNetworkHandler, TestHandler, Fetch]) + .useCache(CacheHandler); } + ``` Let's go over the code above: @@ -70,7 +71,7 @@ Let's go over the code above: 3. Lastly `Fetch`. It is a handler that sends requests to the server using the `fetch` API. It expects responses to be JSON and when in use it should be the last handler you put in the chain. After finishing each request it will convert the response into json and pass it back to the handlers chain in reverse order as the request context's response. So `TestHandler` will receive `response` property first, and so on if we would have any. -> NOTE: Your `RequestManager` service should be exactly `app/services/request-manager.[js|ts]` file. It is a convention that Ember uses to find the service. +The CacheHandler is a special handler that enables requests to fulfill from and update the cache associated to this store. You can read more about request manager in the [request manager guide](../requests/index.md). diff --git a/guides/cookbook/index.md b/guides/7-cookbook/index.md similarity index 100% rename from guides/cookbook/index.md rename to guides/7-cookbook/index.md diff --git a/guides/cookbook/naming-conventions.md b/guides/7-cookbook/naming-conventions.md similarity index 100% rename from guides/cookbook/naming-conventions.md rename to guides/7-cookbook/naming-conventions.md diff --git a/guides/community-resources.md b/guides/8-community-resources.md similarity index 100% rename from guides/community-resources.md rename to guides/8-community-resources.md diff --git a/guides/terminology.md b/guides/9-terminology.md similarity index 100% rename from guides/terminology.md rename to guides/9-terminology.md diff --git a/guides/manual/0-index.md b/guides/manual/0-index.md deleted file mode 100644 index ca398a9593a..00000000000 --- a/guides/manual/0-index.md +++ /dev/null @@ -1,12 +0,0 @@ -# The Manual - -## Table Of Contents - -1) [Intro](./1-overview.md) - - [Why WarpDrive?](./1-overview.md#why-warpdrive) -2) [Making Requests](./2-requests.md) -3) [Key Data Structures](./3-data.md) -4) [Caching](./4-caching.md) -5) [Working with Data in your UI](./5-presentation.md) -6) [Field Schemas](./6-schemas.md) -7) [Request Builders](./7-builders.md) diff --git a/guides/manual/2-requests.md b/guides/manual/2-requests.md deleted file mode 100644 index 5350fe1de50..00000000000 --- a/guides/manual/2-requests.md +++ /dev/null @@ -1,145 +0,0 @@ -| | | -| -- | -- | -| [← Introduction](./1-overview.md)                        |                       [Key Data Structures →](./3-data.md) | - -## Requests - -Requests are how your application fetches data from a source or asks the source to update data to a new state. - -*Warp***Drive** uses the native interfaces for [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) and [Response](https://developer.mozilla.org/en-US/docs/Web/API/Response) as the foundation upon which it layers features for fulfilling requests. - -Sources can be anything that has the ability for you to store and retrieve data: for example your API, the file system, or IndexedDB. - -Though the actual source and connection type do not matter, in a typical app requests are fulfilled by making [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) calls against server endpoints making up an API. - -
- -### Fetch Example - -> [!TIP] -> When we want to show integration with a framework, this tutorial -> uses [EmberJS](https://emberjs.com), a powerful modern web framework with an established multi-decade legacy. - -
- -*Run This Example* → [Request | Fetch Example](https://warpdrive.nullvoxpopuli.com/manual/requests/fetch) - -
- -Data fetching is managed by a `RequestManager`, which executes handlers you provide. - -In order to make requests, first we create a `RequestManager` for our -application, and we tell it to fulfill requests using a `Fetch` handler. - -```ts -import RequestManager from '@ember-data/request'; -import { Fetch } from '@ember-data/request/fetch'; - -const manager = new RequestManager().use([Fetch]); -``` - -Now we can issue a request for a list of our users: - -```ts -const { content } = await manager.request({ url: '/users' }); - -for (const user of content.data) { - greet(`${user.firstName} ${user.lastName}`); -} -``` - -If we wanted to type the above request, we could supply a type for the -content returned by the request: - -```ts -type User = { - id: string; - firstName: string; - lastName: string; - age: number; -}; - -type UsersQuery = { - data: User[]; -} -``` - -And use it like this: - -```ts -const { content } = await manager.request({ url: '/users' }); -``` - -> [!TIP] -> Manually supplying the generic is NOT the preferred way -> to type a request, look for the section on [builders](./7-builders.md) -> later. - -
- -### The Chain of Responsibility - -When we created the request manager for our application above, you may have noticed that when we told it to fulfill requests using the `Fetch` handler we did so by passing in an array: - -```ts -new RequestManager().use([Fetch]); -``` - -The request manager follows the [chain-of-responsibility pattern](https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern): each handler in our array may choose to fulfill the request, modify it, or pass it along unchanged to the next handler in the array, in array order. - - -```mermaid ---- -config: - theme: neutral ---- -flowchart LR - C(Handler 1) - C <==> D(Handler 2) - D <==> E(Handler 3) - C <--> F@{ shape: lin-cyl, label: "Source A" } - D <--> G@{ shape: lin-cyl, label: "Source B" } - E <--> H@{ shape: lin-cyl, label: "Source C" } -``` - - -A handler receives the request `context` as well as a `next` function with which to pass along -a request if it so chooses. - -```ts -type NextFn = (req: RequestInfo) => Future; - -type Handler = { - request(context: RequestContext, next: NextFn): Promise | Future; -} -``` - -`next` returns a Future, which is a promise with a few additional capabilities. Futures resolve -with the response from the next handler in the chain. This allows a handler to read or modify -the response if it wants. - -> [!Important] -> requests are immutable, to modify one the handler must create a new request, copying over or -> cloning the parts it wants to leave unchanged. - -```ts -type NextFn = (req: RequestInfo) => Future; - -type Handler = { - request(context: RequestContext, next: NextFn): Promise | Future; -} -``` - -A handler may be any object with a `request` method. This allows both stateful and non-stateful handlers to be utilized. - -> [!TIP] -> Handlers should take care of the most generalizable concerns. In general great handlers -> - apply to many-if-not-all requests -> - have a clear heuristic by which they activate (a header, op-code, option, or url scheme) -> - don't block response streaming (we'll talk about this later) - -
- -| | | -| -- | -- | -| [← Introduction](./1-overview.md)                        |                       [Key Data Structures →](./3-data.md) | diff --git a/packages/core-types/src/request.type-test.ts b/packages/core-types/src/request.type-test.ts new file mode 100644 index 00000000000..fce5601efb0 --- /dev/null +++ b/packages/core-types/src/request.type-test.ts @@ -0,0 +1,23 @@ +import type { RequestInfo } from './request'; +import type { RequestSignature } from './symbols'; + +type User = { + id: string; + name: string; +}; + +function myBuilder(type: string, id: string): RequestInfo { + return { + method: 'GET', + url: `/${type}/${id}`, + headers: new Headers(), + body: null, + }; +} + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const _result = myBuilder('user', '1'); + +type A = typeof _result; +// eslint-disable-next-line @typescript-eslint/no-unused-vars +type _B = A[typeof RequestSignature]; diff --git a/packages/request/README.md b/packages/request/README.md index 3170a8823ef..6d140a48812 100644 --- a/packages/request/README.md +++ b/packages/request/README.md @@ -411,39 +411,41 @@ In the case of the `Future` being returned, `Stream` proxying is automatic and i --- -### Using as a Service +#### Using with `@ember-data/store` -Most applications will desire to have a single `RequestManager` instance, which can be achieved using module-state patterns for singletons, or for [Ember](https://emberjs.com) applications by exporting the manager as a [service](https://guides.emberjs.com/release/services/). +To have a request service unique to a Store: -*services/request.ts* ```ts +import Store, { CacheHandler } from '@ember-data/store'; import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; -import Auth from 'app/services/auth-handler'; -export default class extends RequestManager { - constructor(createArgs) { - super(createArgs); - this.use([Auth, Fetch]); - } +class extends Store { + requestManager = new RequestManager() + .use([Fetch]) + .useCache(CacheHandler); } ``` --- -#### Using with `@ember-data/store` +### Using as a Service -To have a request service unique to a Store: +Some applications will desire to have direct service-level access to the `RequestManager`, which can be achieved using module-state patterns for singletons, or for [Ember](https://emberjs.com) applications by exporting the manager as a [service](https://guides.emberjs.com/release/services/). +*services/request.ts* ```ts -import Store, { CacheHandler } from '@ember-data/store'; +import { CacheHandler } from '@ember-data/store'; import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; +import Auth from 'app/services/ember-data-handler'; -class extends Store { - requestManager = new RequestManager() - .use([Fetch]) - .useCache(CacheHandler); +export default { + create() { + return new RequestManager() + .use([Auth, Fetch]) + .useCache(CacheHandler); + } } ``` diff --git a/packages/request/src/-private/manager.ts b/packages/request/src/-private/manager.ts index aa05ed5e807..d79bfdfcd06 100644 --- a/packages/request/src/-private/manager.ts +++ b/packages/request/src/-private/manager.ts @@ -353,39 +353,41 @@ In the case of the `Future` being returned, `Stream` proxying is automatic and i --- -### Using as a Service +#### Using with `@ember-data/store` -Most applications will desire to have a single `RequestManager` instance, which can be achieved using module-state patterns for singletons, or for [Ember](https://emberjs.com) applications by exporting the manager as a [service](https://guides.emberjs.com/release/services/). +To have a request service unique to a Store: -*services/request.ts* ```ts +import Store, { CacheHandler } from '@ember-data/store'; import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; -import Auth from 'ember-simple-auth/ember-data-handler'; -export default class extends RequestManager { - constructor(createArgs) { - super(createArgs); - this.use([Auth, Fetch]); - } +class extends Store { + requestManager = new RequestManager() + .use([Fetch]) + .useCache(CacheHandler); } ``` --- -#### Using with `@ember-data/store` +### Using as a Service -To have a request service unique to a Store: +Some applications will desire to have a single `RequestManager` instance, which can be achieved using module-state patterns for singletons, or for [Ember](https://emberjs.com) applications by exporting the manager as a [service](https://guides.emberjs.com/release/services/). +*services/request.ts* ```ts -import Store, { CacheHandler } from '@ember-data/store'; +import { CacheHandler } from '@ember-data/store'; import RequestManager from '@ember-data/request'; import Fetch from '@ember-data/request/fetch'; +import Auth from 'ember-simple-auth/ember-data-handler'; -class extends Store { - requestManager = new RequestManager() - .use([Fetch]) - .useCache(CacheHandler); +export default { + create() { + return new RequestManager() + .use([Auth, Fetch]) + .use(CacheHandler); + } } ``` diff --git a/packages/store/README.md b/packages/store/README.md index 1dfdfa48e33..ad79b1a798a 100644 --- a/packages/store/README.md +++ b/packages/store/README.md @@ -124,11 +124,11 @@ import RequestManager from '@ember-data/request'; import { CacheHandler } from '@ember-data/store'; import Fetch from '@ember-data/request/fetch'; -export default class extends RequestManager { - constructor(createArgs) { - super(createArgs); - this.use([Fetch]); - this.useCache(CacheHandler); +export default { + create() { + return new RequestManager() + .use([Fetch]) + .useCache(CacheHandler); } } ``` diff --git a/packages/store/src/index.ts b/packages/store/src/index.ts index 8aecc675aa1..adba5487925 100644 --- a/packages/store/src/index.ts +++ b/packages/store/src/index.ts @@ -96,10 +96,11 @@ * import RequestManager from '@ember-data/request'; * import Fetch from '@ember-data/request/fetch'; * - * export default class extends RequestManager { - * constructor(createArgs) { - * super(createArgs); - * this.use([Fetch]); + * export default { + * create() { + * return new RequestManager() + * .use([Fetch]) + * .useCache(CacheHandler); * } * } * ``` diff --git a/tests/ember-data__request/tests/integration/service-test.ts b/tests/ember-data__request/tests/integration/service-test.ts index 7c51d62295c..adfa7286d95 100644 --- a/tests/ember-data__request/tests/integration/service-test.ts +++ b/tests/ember-data__request/tests/integration/service-test.ts @@ -1,4 +1,5 @@ -import { getOwner } from '@ember/application'; +import { getOwner, setOwner } from '@ember/application'; +import type Owner from '@ember/owner'; import * as s from '@ember/service'; import type { TestContext } from '@ember/test-helpers'; @@ -33,4 +34,27 @@ module('RequestManager | Ember Service Setup', function (hooks) { assert.ok(manager.cache instanceof Cache, 'We can utilize injections'); assert.equal(getOwner(manager), this.owner, 'The manager correctly sets owner'); }); + + test('We can use injections when registering the RequestManager as a service (create)', function (this: TestContext, assert) { + class CustomManager extends RequestManager { + @service cache; + } + + const ManagerService = { + create(owner: Owner) { + const manager = new CustomManager(); + setOwner(manager, owner); + + return manager; + }, + }; + this.owner.register('service:request', ManagerService); + class Cache extends Service {} + this.owner.register('service:cache', Cache); + const manager = this.owner.lookup('service:request') as unknown as CustomManager; + assert.ok(manager instanceof RequestManager, 'We instantiated'); + assert.ok(manager instanceof CustomManager, 'We instantiated'); + assert.ok(manager.cache instanceof Cache, 'We can utilize injections'); + assert.equal(getOwner(manager), this.owner, 'The manager correctly sets owner'); + }); }); diff --git a/tests/ember-data__request/tests/integration/stateful-handler-test.ts b/tests/ember-data__request/tests/integration/stateful-handler-test.ts index a76d27f198d..3aca7d560e0 100644 --- a/tests/ember-data__request/tests/integration/stateful-handler-test.ts +++ b/tests/ember-data__request/tests/integration/stateful-handler-test.ts @@ -1,4 +1,4 @@ -import { setOwner } from '@ember/owner'; +import { setOwner } from '@ember/application'; import { service } from '@ember/service'; import type { TestContext } from '@ember/test-helpers'; diff --git a/tests/example-json-api/app/services/request-manager.ts b/tests/example-json-api/app/services/request-manager.ts index 522664f295e..9f4603167c6 100644 --- a/tests/example-json-api/app/services/request-manager.ts +++ b/tests/example-json-api/app/services/request-manager.ts @@ -13,9 +13,8 @@ const TestHandler: Handler = { }, }; -export default class Requests extends RequestManager { - constructor(args?: Record) { - super(args); - this.use([LegacyNetworkHandler, TestHandler, Fetch]); - } -} +export default { + create() { + return new RequestManager().use([LegacyNetworkHandler, TestHandler, Fetch]); + }, +}; diff --git a/tests/main/tests/integration/cache-handler/store-package-setup-test.ts b/tests/main/tests/integration/cache-handler/store-package-setup-test.ts index 43e57a7aac4..3a4ae2e2b5a 100644 --- a/tests/main/tests/integration/cache-handler/store-package-setup-test.ts +++ b/tests/main/tests/integration/cache-handler/store-package-setup-test.ts @@ -41,13 +41,11 @@ type UserRecord = { [Type]: 'user'; }; -class RequestManagerService extends RequestManager { - constructor() { - super(...arguments); - this.use([LegacyNetworkHandler, Fetch]); - this.useCache(CacheHandler); - } -} +const RequestManagerService = { + create() { + return new RequestManager().use([LegacyNetworkHandler, Fetch]).useCache(CacheHandler); + }, +}; class TestStore extends Store { @service('request') declare requestManager: RequestManager; diff --git a/tests/main/tests/onboarding/basic-setup-test.ts b/tests/main/tests/onboarding/basic-setup-test.ts new file mode 100644 index 00000000000..0f569ca7c80 --- /dev/null +++ b/tests/main/tests/onboarding/basic-setup-test.ts @@ -0,0 +1,48 @@ +import { module, test } from 'qunit'; + +import { setupTest } from 'ember-qunit'; + +import RequestManager from '@ember-data/request'; +import Store from '@ember-data/store'; + +module('Onboarding | Basic Setup', function (hooks) { + setupTest(hooks); + + test('We can use the store without a cache and get the raw result', async function (assert) { + class AppStore extends Store { + requestManager = new RequestManager().use([ + { + request({ request }) { + assert.step(`request ${request.url}`); + return Promise.resolve({ + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris Thoburn', + }, + }, + }) as Promise; + }, + }, + ]); + } + + const store = new AppStore(); + const result = await store.request({ + url: '/users/1', + }); + + assert.deepEqual(result?.content, { + data: { + type: 'user', + id: '1', + attributes: { + name: 'Chris Thoburn', + }, + }, + }); + + assert.verifySteps(['request /users/1']); + }); +});