From 2dc1b1cdd190189b0469e73438b6cd3fcc79ac7c Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sun, 29 Oct 2023 15:59:48 -0700 Subject: [PATCH 1/7] feat: improve configurability of json-api builder and request-manager --- package.json | 2 +- packages/-ember-data/addon/store.ts | 7 +- .../json-api/src/-private/builders/-utils.ts | 82 +++++++++++++++++++ .../src/-private/builders/find-record.ts | 4 +- .../json-api/src/-private/builders/query.ts | 6 +- .../src/-private/builders/save-record.ts | 8 +- packages/json-api/src/request.ts | 1 + packages/request-utils/src/index.ts | 2 +- tests/docs/fixtures/expected.js | 1 + 9 files changed, 100 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index bbe30abc9b3..59afcbc2640 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "test": "pnpm turbo test --concurrency=1", "test:production": "pnpm turbo test:production --concurrency=1", "test:try-one": "pnpm --filter main-test-app run test:try-one", - "test:docs": "pnpm build:docs && pnpm run -r --workspace-concurrency=-1 --if-present test:blueprints", + "test:docs": "pnpm build:docs && pnpm run -r --workspace-concurrency=-1 --if-present test:docs", "test:blueprints": "pnpm run -r --workspace-concurrency=-1 --if-present test:blueprints", "test:fastboot": "pnpm run -r --workspace-concurrency=-1 --if-present test:fastboot", "test:embroider": "pnpm run -r ---workspace-concurrency=-1 --if-present test:embroider", diff --git a/packages/-ember-data/addon/store.ts b/packages/-ember-data/addon/store.ts index 4236139c53a..6c203291b27 100644 --- a/packages/-ember-data/addon/store.ts +++ b/packages/-ember-data/addon/store.ts @@ -26,8 +26,11 @@ export default class Store extends BaseStore { constructor(args?: Record) { super(args); - this.requestManager = new RequestManager(); - this.requestManager.use([LegacyNetworkHandler, Fetch]); + + if (!('requestManager' in this)) { + this.requestManager = new RequestManager(); + this.requestManager.use([LegacyNetworkHandler, Fetch]); + } this.requestManager.useCache(CacheHandler); this.registerSchema(buildSchema(this)); } diff --git a/packages/json-api/src/-private/builders/-utils.ts b/packages/json-api/src/-private/builders/-utils.ts index 9cae4d3439b..15a9bd8218e 100644 --- a/packages/json-api/src/-private/builders/-utils.ts +++ b/packages/json-api/src/-private/builders/-utils.ts @@ -1,6 +1,88 @@ +/** + * @module @ember-data/json-api/request + */ +import { BuildURLConfig, setBuildURLConfig as setConfig } from '@ember-data/request-utils'; import { type UrlOptions } from '@ember-data/request-utils'; import type { CacheOptions, ConstrainedRequestOptions } from '@warp-drive/core-types/request'; +export interface JSONAPIConfig extends BuildURLConfig { + profiles?: { + pagination?: string; + [key: string]: string | undefined; + }, + extensions?: { + atomic?: string; + [key: string]: string | undefined; + } +} + +const JsonApiAccept = 'application/vnd.api+json'; +export let CONFIG: JSONAPIConfig = { host: '', namespace: '' }; +export let ACCEPT_HEADER_VALUE = 'application/vnd.api+json'; + +/** + * Allows setting extensions and profiles to be used in the `Accept` header. + * + * Extensions and profiles are keyed by their namespace with the value being + * their URI. + * + * Example: + * + * ```ts + * setBuildURLConfig({ + * extensions: { + * atomic: 'https://jsonapi.org/ext/atomic' + * }, + * profiles: { + * pagination: 'https://jsonapi.org/profiles/ethanresnick/cursor-pagination' + * } + * }); + * + * This also sets the global configuration for `buildBaseURL` + * for host and namespace values for the application + * in the `@ember-data/request-utils` package. + * + * These values may still be overridden by passing + * them to buildBaseURL directly. + * + * This method may be called as many times as needed + * + * ```ts + * type BuildURLConfig = { + * host: string; + * namespace: string' + * } + * ``` + * + * @method setBuildURLConfig + * @static + * @public + * @for @ember-data/json-api/request + * @param {BuildURLConfig} config + * @returns void + */ +export function setBuildURLConfig(config: JSONAPIConfig): void { + Object.assign(CONFIG, config); + + if (config.profiles || config.extensions) { + let accept = JsonApiAccept; + if (config.profiles) { + const profiles = Object.values(config.profiles); + if (profiles.length) { + accept += ';profile="' + profiles.join(' ') + '"'; + } + } + if (config.extensions) { + const extensions = Object.values(config.extensions); + if (extensions.length) { + accept += ';ext=' + extensions.join(' '); + } + } + } + + setConfig(config); +} + export function copyForwardUrlOptions(urlOptions: UrlOptions, options: ConstrainedRequestOptions): void { if ('host' in options) { urlOptions.host = options.host; diff --git a/packages/json-api/src/-private/builders/find-record.ts b/packages/json-api/src/-private/builders/find-record.ts index 02b87a878db..6f8c075c242 100644 --- a/packages/json-api/src/-private/builders/find-record.ts +++ b/packages/json-api/src/-private/builders/find-record.ts @@ -10,7 +10,7 @@ import type { RemotelyAccessibleIdentifier, } from '@warp-drive/core-types/request'; -import { copyForwardUrlOptions, extractCacheOptions } from './-utils'; +import { ACCEPT_HEADER_VALUE, copyForwardUrlOptions, extractCacheOptions } from './-utils'; /** * Builds request options to fetch a single resource by a known id or identifier @@ -93,7 +93,7 @@ export function findRecord( const url = buildBaseURL(urlOptions); const headers = new Headers(); - headers.append('Accept', 'application/vnd.api+json'); + headers.append('Accept', ACCEPT_HEADER_VALUE); return { url: options.include?.length diff --git a/packages/json-api/src/-private/builders/query.ts b/packages/json-api/src/-private/builders/query.ts index 234fe64cdbe..04221d2eccf 100644 --- a/packages/json-api/src/-private/builders/query.ts +++ b/packages/json-api/src/-private/builders/query.ts @@ -12,7 +12,7 @@ import type { QueryRequestOptions, } from '@warp-drive/core-types/request'; -import { copyForwardUrlOptions, extractCacheOptions } from './-utils'; +import { ACCEPT_HEADER_VALUE, copyForwardUrlOptions, extractCacheOptions } from './-utils'; /** * Builds request options to query for resources, usually by a primary @@ -85,7 +85,7 @@ export function query( const url = buildBaseURL(urlOptions); const headers = new Headers(); - headers.append('Accept', 'application/vnd.api+json'); + headers.append('Accept', ACCEPT_HEADER_VALUE); return { url: `${url}?${buildQueryParams(query, options.urlParamsSettings)}`, @@ -160,7 +160,7 @@ export function postQuery( const url = buildBaseURL(urlOptions); const headers = new Headers(); - headers.append('Accept', 'application/vnd.api+json'); + headers.append('Accept', ACCEPT_HEADER_VALUE); const queryData = structuredClone(query); cacheOptions.key = cacheOptions.key ?? `${url}?${buildQueryParams(queryData, options.urlParamsSettings)}`; diff --git a/packages/json-api/src/-private/builders/save-record.ts b/packages/json-api/src/-private/builders/save-record.ts index 1f91b816f7e..5b4073e9be7 100644 --- a/packages/json-api/src/-private/builders/save-record.ts +++ b/packages/json-api/src/-private/builders/save-record.ts @@ -17,7 +17,7 @@ import { UpdateRequestOptions, } from '@warp-drive/core-types/request'; -import { copyForwardUrlOptions } from './-utils'; +import { ACCEPT_HEADER_VALUE, copyForwardUrlOptions } from './-utils'; function isExisting(identifier: StableRecordIdentifier): identifier is StableExistingRecordIdentifier { return 'id' in identifier && identifier.id !== null && 'type' in identifier && identifier.type !== null; @@ -90,7 +90,7 @@ export function deleteRecord(record: unknown, options: ConstrainedRequestOptions const url = buildBaseURL(urlOptions); const headers = new Headers(); - headers.append('Accept', 'application/vnd.api+json'); + headers.append('Accept', ACCEPT_HEADER_VALUE); return { url, @@ -159,7 +159,7 @@ export function createRecord(record: unknown, options: ConstrainedRequestOptions const url = buildBaseURL(urlOptions); const headers = new Headers(); - headers.append('Accept', 'application/vnd.api+json'); + headers.append('Accept', ACCEPT_HEADER_VALUE); return { url, @@ -235,7 +235,7 @@ export function updateRecord( const url = buildBaseURL(urlOptions); const headers = new Headers(); - headers.append('Accept', 'application/vnd.api+json'); + headers.append('Accept', ACCEPT_HEADER_VALUE); return { url, diff --git a/packages/json-api/src/request.ts b/packages/json-api/src/request.ts index b664706957e..29bc57b79a8 100644 --- a/packages/json-api/src/request.ts +++ b/packages/json-api/src/request.ts @@ -67,3 +67,4 @@ export { findRecord } from './-private/builders/find-record'; export { query, postQuery } from './-private/builders/query'; export { deleteRecord, createRecord, updateRecord } from './-private/builders/save-record'; export { serializeResources, serializePatch } from './-private/serialize'; +export { setBuildURLConfig } from './-private/builders/-utils'; diff --git a/packages/request-utils/src/index.ts b/packages/request-utils/src/index.ts index 0a18cb127f8..609db561e51 100644 --- a/packages/request-utils/src/index.ts +++ b/packages/request-utils/src/index.ts @@ -52,7 +52,7 @@ type Store = { // host and namespace which are provided by the final consuming // class to the prototype which can result in overwrite errors -interface BuildURLConfig { +export interface BuildURLConfig { host: string | null; namespace: string | null; } diff --git a/tests/docs/fixtures/expected.js b/tests/docs/fixtures/expected.js index 86dd5dd55a7..29b28cf9f60 100644 --- a/tests/docs/fixtures/expected.js +++ b/tests/docs/fixtures/expected.js @@ -260,6 +260,7 @@ module.exports = { '(public) @ember-data/json-api/request @ember-data/json-api/request#updateRecord', '(public) @ember-data/json-api/request @ember-data/json-api/request#serializePatch', '(public) @ember-data/json-api/request @ember-data/json-api/request#serializeResources', + '(public) @ember-data/json-api/request @ember-data/json-api/request#setBuildURLConfig', '(public) @ember-data/legacy-compat SnapshotRecordArray#adapterOptions', '(public) @ember-data/legacy-compat SnapshotRecordArray#include', '(public) @ember-data/legacy-compat SnapshotRecordArray#length', From 387b9891766d35e95ecdf3fbc5bf49c484275d4e Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sun, 29 Oct 2023 16:01:30 -0700 Subject: [PATCH 2/7] fix types --- packages/-ember-data/addon/store.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/-ember-data/addon/store.ts b/packages/-ember-data/addon/store.ts index 6c203291b27..320780c8a6c 100644 --- a/packages/-ember-data/addon/store.ts +++ b/packages/-ember-data/addon/store.ts @@ -21,13 +21,17 @@ import type { Cache } from '@warp-drive/core-types/cache'; import type { CacheCapabilitiesManager } from '@ember-data/store/-types/q/cache-store-wrapper'; import type { ModelSchema } from '@ember-data/store/-types/q/ds-model'; +function hasRequestManager(store: BaseStore): boolean { + return 'requestManager' in store; +} + export default class Store extends BaseStore { declare _fetchManager: FetchManager; constructor(args?: Record) { super(args); - if (!('requestManager' in this)) { + if (!hasRequestManager(this)) { this.requestManager = new RequestManager(); this.requestManager.use([LegacyNetworkHandler, Fetch]); } From edb4d0c9fe8f0f7f53021367a5a614f0ee2a4602 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sun, 29 Oct 2023 16:24:58 -0700 Subject: [PATCH 3/7] add tests --- packages/-ember-data/addon/store.ts | 2 + .../tests/integration/store-extension-test.ts | 45 +++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 tests/main/tests/integration/store-extension-test.ts diff --git a/packages/-ember-data/addon/store.ts b/packages/-ember-data/addon/store.ts index 320780c8a6c..4ecb07047fd 100644 --- a/packages/-ember-data/addon/store.ts +++ b/packages/-ember-data/addon/store.ts @@ -31,7 +31,9 @@ export default class Store extends BaseStore { constructor(args?: Record) { super(args); + debugger; if (!hasRequestManager(this)) { + debugger; this.requestManager = new RequestManager(); this.requestManager.use([LegacyNetworkHandler, Fetch]); } diff --git a/tests/main/tests/integration/store-extension-test.ts b/tests/main/tests/integration/store-extension-test.ts new file mode 100644 index 00000000000..b8908560b94 --- /dev/null +++ b/tests/main/tests/integration/store-extension-test.ts @@ -0,0 +1,45 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import Store from 'ember-data/store'; +import RequestManager from '@ember-data/request'; +import { inject as service } from '@ember/service'; + +module('Integration | Store Extension', function (hooks) { + setupTest(hooks); + + test('We can create a store ', function (assert) { + const { owner } = this; + class CustomStore extends Store {} + owner.register('service:store', CustomStore); + const store = owner.lookup('service:store'); + + assert.true(store.requestManager instanceof RequestManager, 'We create a request manager for the store automatically'); + }); + + test('We can create a store with a custom request manager injected as a service', function (assert) { + const { owner } = this; + class CustomStore extends Store { + @service requestManager!: RequestManager; + } + + owner.register('service:store', CustomStore); + owner.register('service:request-manager', RequestManager); + const requestManager = owner.lookup('service:request-manager'); + const store = owner.lookup('service:store'); + + assert.true(store.requestManager === requestManager, 'We can inject a custom request manager into the store'); + }); + + test('We can create a store with a custom request manager initialized as a field', function (assert) { + const { owner } = this; + const requestManager = new RequestManager(); + class CustomStore extends Store { + requestManager = requestManager; + } + + owner.register('service:store', CustomStore); + const store = owner.lookup('service:store'); + + assert.true(store.requestManager === requestManager, 'We can inject a custom request manager into the store'); + }); +}); From 84763965186fa99077fced1bf3547e6009416a7a Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sun, 29 Oct 2023 16:29:51 -0700 Subject: [PATCH 4/7] fix prettier --- packages/json-api/src/-private/builders/-utils.ts | 4 ++-- tests/main/tests/integration/store-extension-test.ts | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/json-api/src/-private/builders/-utils.ts b/packages/json-api/src/-private/builders/-utils.ts index 15a9bd8218e..e7e3d2c7ac4 100644 --- a/packages/json-api/src/-private/builders/-utils.ts +++ b/packages/json-api/src/-private/builders/-utils.ts @@ -9,11 +9,11 @@ export interface JSONAPIConfig extends BuildURLConfig { profiles?: { pagination?: string; [key: string]: string | undefined; - }, + }; extensions?: { atomic?: string; [key: string]: string | undefined; - } + }; } const JsonApiAccept = 'application/vnd.api+json'; diff --git a/tests/main/tests/integration/store-extension-test.ts b/tests/main/tests/integration/store-extension-test.ts index b8908560b94..75610eb0735 100644 --- a/tests/main/tests/integration/store-extension-test.ts +++ b/tests/main/tests/integration/store-extension-test.ts @@ -13,7 +13,10 @@ module('Integration | Store Extension', function (hooks) { owner.register('service:store', CustomStore); const store = owner.lookup('service:store'); - assert.true(store.requestManager instanceof RequestManager, 'We create a request manager for the store automatically'); + assert.true( + store.requestManager instanceof RequestManager, + 'We create a request manager for the store automatically' + ); }); test('We can create a store with a custom request manager injected as a service', function (assert) { From b2fc65b3853c387ebe1764ab46476d2dbb2506b5 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sun, 29 Oct 2023 16:33:27 -0700 Subject: [PATCH 5/7] fix forgottend debugger --- packages/-ember-data/addon/store.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/-ember-data/addon/store.ts b/packages/-ember-data/addon/store.ts index 4ecb07047fd..320780c8a6c 100644 --- a/packages/-ember-data/addon/store.ts +++ b/packages/-ember-data/addon/store.ts @@ -31,9 +31,7 @@ export default class Store extends BaseStore { constructor(args?: Record) { super(args); - debugger; if (!hasRequestManager(this)) { - debugger; this.requestManager = new RequestManager(); this.requestManager.use([LegacyNetworkHandler, Fetch]); } From b28803996b49db9810cf03f725ff984e0a21ba9a Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sun, 29 Oct 2023 16:44:22 -0700 Subject: [PATCH 6/7] fixup lint --- packages/json-api/src/-private/builders/-utils.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/json-api/src/-private/builders/-utils.ts b/packages/json-api/src/-private/builders/-utils.ts index e7e3d2c7ac4..d1db7de4352 100644 --- a/packages/json-api/src/-private/builders/-utils.ts +++ b/packages/json-api/src/-private/builders/-utils.ts @@ -17,7 +17,8 @@ export interface JSONAPIConfig extends BuildURLConfig { } const JsonApiAccept = 'application/vnd.api+json'; -export let CONFIG: JSONAPIConfig = { host: '', namespace: '' }; +const DEFAULT_CONFIG: JSONAPIConfig = { host: '', namespace: '' }; +export let CONFIG: JSONAPIConfig = DEFAULT_CONFIG; export let ACCEPT_HEADER_VALUE = 'application/vnd.api+json'; /** @@ -62,7 +63,7 @@ export let ACCEPT_HEADER_VALUE = 'application/vnd.api+json'; * @returns void */ export function setBuildURLConfig(config: JSONAPIConfig): void { - Object.assign(CONFIG, config); + CONFIG = Object.assign({}, DEFAULT_CONFIG, config); if (config.profiles || config.extensions) { let accept = JsonApiAccept; @@ -78,6 +79,7 @@ export function setBuildURLConfig(config: JSONAPIConfig): void { accept += ';ext=' + extensions.join(' '); } } + ACCEPT_HEADER_VALUE = accept; } setConfig(config); From ca9e31c0f4d1ac3d08b8e62a486995672d9514e8 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Sun, 29 Oct 2023 16:45:59 -0700 Subject: [PATCH 7/7] fix lint --- packages/json-api/src/-private/builders/-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/json-api/src/-private/builders/-utils.ts b/packages/json-api/src/-private/builders/-utils.ts index d1db7de4352..7e1488bbd99 100644 --- a/packages/json-api/src/-private/builders/-utils.ts +++ b/packages/json-api/src/-private/builders/-utils.ts @@ -63,7 +63,7 @@ export let ACCEPT_HEADER_VALUE = 'application/vnd.api+json'; * @returns void */ export function setBuildURLConfig(config: JSONAPIConfig): void { - CONFIG = Object.assign({}, DEFAULT_CONFIG, config); + CONFIG = Object.assign({}, DEFAULT_CONFIG, config); if (config.profiles || config.extensions) { let accept = JsonApiAccept;