Skip to content

Commit 9ea4893

Browse files
authored
feat: Improve extensibility (#9069)
* feat: improve configurability of json-api builder and request-manager * fix types * add tests * fix prettier * fix forgottend debugger * fixup lint * fix lint
1 parent ca37a44 commit 9ea4893

File tree

10 files changed

+154
-13
lines changed

10 files changed

+154
-13
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"test": "pnpm turbo test --concurrency=1",
2020
"test:production": "pnpm turbo test:production --concurrency=1",
2121
"test:try-one": "pnpm --filter main-test-app run test:try-one",
22-
"test:docs": "pnpm build:docs && pnpm run -r --workspace-concurrency=-1 --if-present test:blueprints",
22+
"test:docs": "pnpm build:docs && pnpm run -r --workspace-concurrency=-1 --if-present test:docs",
2323
"test:blueprints": "pnpm run -r --workspace-concurrency=-1 --if-present test:blueprints",
2424
"test:fastboot": "pnpm run -r --workspace-concurrency=-1 --if-present test:fastboot",
2525
"test:embroider": "pnpm run -r ---workspace-concurrency=-1 --if-present test:embroider",

packages/-ember-data/addon/store.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,20 @@ import type { Cache } from '@warp-drive/core-types/cache';
2121
import type { CacheCapabilitiesManager } from '@ember-data/store/-types/q/cache-store-wrapper';
2222
import type { ModelSchema } from '@ember-data/store/-types/q/ds-model';
2323

24+
function hasRequestManager(store: BaseStore): boolean {
25+
return 'requestManager' in store;
26+
}
27+
2428
export default class Store extends BaseStore {
2529
declare _fetchManager: FetchManager;
2630

2731
constructor(args?: Record<string, unknown>) {
2832
super(args);
29-
this.requestManager = new RequestManager();
30-
this.requestManager.use([LegacyNetworkHandler, Fetch]);
33+
34+
if (!hasRequestManager(this)) {
35+
this.requestManager = new RequestManager();
36+
this.requestManager.use([LegacyNetworkHandler, Fetch]);
37+
}
3138
this.requestManager.useCache(CacheHandler);
3239
this.registerSchema(buildSchema(this));
3340
}

packages/json-api/src/-private/builders/-utils.ts

+84
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,90 @@
1+
/**
2+
* @module @ember-data/json-api/request
3+
*/
4+
import { BuildURLConfig, setBuildURLConfig as setConfig } from '@ember-data/request-utils';
15
import { type UrlOptions } from '@ember-data/request-utils';
26
import type { CacheOptions, ConstrainedRequestOptions } from '@warp-drive/core-types/request';
37

8+
export interface JSONAPIConfig extends BuildURLConfig {
9+
profiles?: {
10+
pagination?: string;
11+
[key: string]: string | undefined;
12+
};
13+
extensions?: {
14+
atomic?: string;
15+
[key: string]: string | undefined;
16+
};
17+
}
18+
19+
const JsonApiAccept = 'application/vnd.api+json';
20+
const DEFAULT_CONFIG: JSONAPIConfig = { host: '', namespace: '' };
21+
export let CONFIG: JSONAPIConfig = DEFAULT_CONFIG;
22+
export let ACCEPT_HEADER_VALUE = 'application/vnd.api+json';
23+
24+
/**
25+
* Allows setting extensions and profiles to be used in the `Accept` header.
26+
*
27+
* Extensions and profiles are keyed by their namespace with the value being
28+
* their URI.
29+
*
30+
* Example:
31+
*
32+
* ```ts
33+
* setBuildURLConfig({
34+
* extensions: {
35+
* atomic: 'https://jsonapi.org/ext/atomic'
36+
* },
37+
* profiles: {
38+
* pagination: 'https://jsonapi.org/profiles/ethanresnick/cursor-pagination'
39+
* }
40+
* });
41+
*
42+
* This also sets the global configuration for `buildBaseURL`
43+
* for host and namespace values for the application
44+
* in the `@ember-data/request-utils` package.
45+
*
46+
* These values may still be overridden by passing
47+
* them to buildBaseURL directly.
48+
*
49+
* This method may be called as many times as needed
50+
*
51+
* ```ts
52+
* type BuildURLConfig = {
53+
* host: string;
54+
* namespace: string'
55+
* }
56+
* ```
57+
*
58+
* @method setBuildURLConfig
59+
* @static
60+
* @public
61+
* @for @ember-data/json-api/request
62+
* @param {BuildURLConfig} config
63+
* @returns void
64+
*/
65+
export function setBuildURLConfig(config: JSONAPIConfig): void {
66+
CONFIG = Object.assign({}, DEFAULT_CONFIG, config);
67+
68+
if (config.profiles || config.extensions) {
69+
let accept = JsonApiAccept;
70+
if (config.profiles) {
71+
const profiles = Object.values(config.profiles);
72+
if (profiles.length) {
73+
accept += ';profile="' + profiles.join(' ') + '"';
74+
}
75+
}
76+
if (config.extensions) {
77+
const extensions = Object.values(config.extensions);
78+
if (extensions.length) {
79+
accept += ';ext=' + extensions.join(' ');
80+
}
81+
}
82+
ACCEPT_HEADER_VALUE = accept;
83+
}
84+
85+
setConfig(config);
86+
}
87+
488
export function copyForwardUrlOptions(urlOptions: UrlOptions, options: ConstrainedRequestOptions): void {
589
if ('host' in options) {
690
urlOptions.host = options.host;

packages/json-api/src/-private/builders/find-record.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import type {
1010
RemotelyAccessibleIdentifier,
1111
} from '@warp-drive/core-types/request';
1212

13-
import { copyForwardUrlOptions, extractCacheOptions } from './-utils';
13+
import { ACCEPT_HEADER_VALUE, copyForwardUrlOptions, extractCacheOptions } from './-utils';
1414

1515
/**
1616
* Builds request options to fetch a single resource by a known id or identifier
@@ -93,7 +93,7 @@ export function findRecord(
9393

9494
const url = buildBaseURL(urlOptions);
9595
const headers = new Headers();
96-
headers.append('Accept', 'application/vnd.api+json');
96+
headers.append('Accept', ACCEPT_HEADER_VALUE);
9797

9898
return {
9999
url: options.include?.length

packages/json-api/src/-private/builders/query.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {
1212
QueryRequestOptions,
1313
} from '@warp-drive/core-types/request';
1414

15-
import { copyForwardUrlOptions, extractCacheOptions } from './-utils';
15+
import { ACCEPT_HEADER_VALUE, copyForwardUrlOptions, extractCacheOptions } from './-utils';
1616

1717
/**
1818
* Builds request options to query for resources, usually by a primary
@@ -85,7 +85,7 @@ export function query(
8585

8686
const url = buildBaseURL(urlOptions);
8787
const headers = new Headers();
88-
headers.append('Accept', 'application/vnd.api+json');
88+
headers.append('Accept', ACCEPT_HEADER_VALUE);
8989

9090
return {
9191
url: `${url}?${buildQueryParams(query, options.urlParamsSettings)}`,
@@ -160,7 +160,7 @@ export function postQuery(
160160

161161
const url = buildBaseURL(urlOptions);
162162
const headers = new Headers();
163-
headers.append('Accept', 'application/vnd.api+json');
163+
headers.append('Accept', ACCEPT_HEADER_VALUE);
164164

165165
const queryData = structuredClone(query);
166166
cacheOptions.key = cacheOptions.key ?? `${url}?${buildQueryParams(queryData, options.urlParamsSettings)}`;

packages/json-api/src/-private/builders/save-record.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
UpdateRequestOptions,
1818
} from '@warp-drive/core-types/request';
1919

20-
import { copyForwardUrlOptions } from './-utils';
20+
import { ACCEPT_HEADER_VALUE, copyForwardUrlOptions } from './-utils';
2121

2222
function isExisting(identifier: StableRecordIdentifier): identifier is StableExistingRecordIdentifier {
2323
return 'id' in identifier && identifier.id !== null && 'type' in identifier && identifier.type !== null;
@@ -90,7 +90,7 @@ export function deleteRecord(record: unknown, options: ConstrainedRequestOptions
9090

9191
const url = buildBaseURL(urlOptions);
9292
const headers = new Headers();
93-
headers.append('Accept', 'application/vnd.api+json');
93+
headers.append('Accept', ACCEPT_HEADER_VALUE);
9494

9595
return {
9696
url,
@@ -159,7 +159,7 @@ export function createRecord(record: unknown, options: ConstrainedRequestOptions
159159

160160
const url = buildBaseURL(urlOptions);
161161
const headers = new Headers();
162-
headers.append('Accept', 'application/vnd.api+json');
162+
headers.append('Accept', ACCEPT_HEADER_VALUE);
163163

164164
return {
165165
url,
@@ -235,7 +235,7 @@ export function updateRecord(
235235

236236
const url = buildBaseURL(urlOptions);
237237
const headers = new Headers();
238-
headers.append('Accept', 'application/vnd.api+json');
238+
headers.append('Accept', ACCEPT_HEADER_VALUE);
239239

240240
return {
241241
url,

packages/json-api/src/request.ts

+1
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,4 @@ export { findRecord } from './-private/builders/find-record';
6767
export { query, postQuery } from './-private/builders/query';
6868
export { deleteRecord, createRecord, updateRecord } from './-private/builders/save-record';
6969
export { serializeResources, serializePatch } from './-private/serialize';
70+
export { setBuildURLConfig } from './-private/builders/-utils';

packages/request-utils/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ type Store = {
5252
// host and namespace which are provided by the final consuming
5353
// class to the prototype which can result in overwrite errors
5454

55-
interface BuildURLConfig {
55+
export interface BuildURLConfig {
5656
host: string | null;
5757
namespace: string | null;
5858
}

tests/docs/fixtures/expected.js

+1
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ module.exports = {
260260
'(public) @ember-data/json-api/request @ember-data/json-api/request#updateRecord',
261261
'(public) @ember-data/json-api/request @ember-data/json-api/request#serializePatch',
262262
'(public) @ember-data/json-api/request @ember-data/json-api/request#serializeResources',
263+
'(public) @ember-data/json-api/request @ember-data/json-api/request#setBuildURLConfig',
263264
'(public) @ember-data/legacy-compat SnapshotRecordArray#adapterOptions',
264265
'(public) @ember-data/legacy-compat SnapshotRecordArray#include',
265266
'(public) @ember-data/legacy-compat SnapshotRecordArray#length',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { module, test } from 'qunit';
2+
import { setupTest } from 'ember-qunit';
3+
import Store from 'ember-data/store';
4+
import RequestManager from '@ember-data/request';
5+
import { inject as service } from '@ember/service';
6+
7+
module('Integration | Store Extension', function (hooks) {
8+
setupTest(hooks);
9+
10+
test('We can create a store ', function (assert) {
11+
const { owner } = this;
12+
class CustomStore extends Store {}
13+
owner.register('service:store', CustomStore);
14+
const store = owner.lookup('service:store');
15+
16+
assert.true(
17+
store.requestManager instanceof RequestManager,
18+
'We create a request manager for the store automatically'
19+
);
20+
});
21+
22+
test('We can create a store with a custom request manager injected as a service', function (assert) {
23+
const { owner } = this;
24+
class CustomStore extends Store {
25+
@service requestManager!: RequestManager;
26+
}
27+
28+
owner.register('service:store', CustomStore);
29+
owner.register('service:request-manager', RequestManager);
30+
const requestManager = owner.lookup('service:request-manager');
31+
const store = owner.lookup('service:store');
32+
33+
assert.true(store.requestManager === requestManager, 'We can inject a custom request manager into the store');
34+
});
35+
36+
test('We can create a store with a custom request manager initialized as a field', function (assert) {
37+
const { owner } = this;
38+
const requestManager = new RequestManager();
39+
class CustomStore extends Store {
40+
requestManager = requestManager;
41+
}
42+
43+
owner.register('service:store', CustomStore);
44+
const store = owner.lookup('service:store');
45+
46+
assert.true(store.requestManager === requestManager, 'We can inject a custom request manager into the store');
47+
});
48+
});

0 commit comments

Comments
 (0)