Skip to content

Commit a04e060

Browse files
authored
feat: improve builder extensibility and add postQuery builder (#9223)
* feat: implement postQuery builder (#8925) * 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 * lint fix * fix types
1 parent 640322d commit a04e060

File tree

12 files changed

+274
-19
lines changed

12 files changed

+274
-19
lines changed

ember-data-types/request.ts

+9
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ export type QueryRequestOptions = {
2424
op: 'query';
2525
};
2626

27+
export type PostQueryRequestOptions = {
28+
url: string;
29+
method: 'POST' | 'QUERY';
30+
headers: Headers;
31+
body: string;
32+
cacheOptions: CacheOptions & { key: string };
33+
op: 'query';
34+
};
35+
2736
export type DeleteRequestOptions = {
2837
url: string;
2938
method: 'DELETE';

package.json

+5-5
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414
"test": "pnpm --filter main-test-app --filter graph-test-app --filter json-api-test-app --filter request-test-app --filter builders-test-app run test",
1515
"test:production": "pnpm --filter main-test-app --filter graph-test-app --filter json-api-test-app run test -e production",
1616
"test:try-one": "pnpm --filter main-test-app run test:try-one",
17-
"test:docs": "pnpm build:docs && pnpm --filter docs-tests test",
18-
"test:encapsulation": "pnpm --filter '*-encapsulation-test-app' run test",
19-
"test:fastboot": "pnpm --filter fastboot-test-app test",
20-
"test:embroider": "pnpm --filter embroider-basic-compat test",
21-
"test:infra": "pnpm --filter @ember-data/unpublished-test-infra test",
17+
"test:docs": "pnpm build:docs && pnpm run -r --workspace-concurrency=-1 --if-present test:docs",
18+
"test:blueprints": "pnpm run -r --workspace-concurrency=-1 --if-present test:blueprints",
19+
"test:fastboot": "pnpm run -r --workspace-concurrency=-1 --if-present test:fastboot",
20+
"test:embroider": "pnpm run -r ---workspace-concurrency=-1 --if-present test:embroider",
21+
"test:infra": "pnpm run -r --workspace-concurrency=-1 --if-present test:infra",
2222
"test-external:ember-m3": "node ./scripts/test-external-partner-project.js ember-m3 https://github.com/hjdivad/ember-m3.git",
2323
"test-external:ember-data-change-tracker": "node ./scripts/test-external-partner-project.js ember-data-change-tracker https://github.com/danielspaniel/ember-data-change-tracker.git",
2424
"test-external:model-fragments": "node ./scripts/test-external-partner-project.js model-fragments https://github.com/lytics/ember-data-model-fragments.git",

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

+9-2
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,18 @@ import type { CacheCapabilitiesManager } from '@ember-data/types/q/cache-store-w
1212
import type { ModelSchema } from '@ember-data/types/q/ds-model';
1313
import type { StableRecordIdentifier } from '@ember-data/types/q/identifier';
1414

15+
function hasRequestManager(store: BaseStore): boolean {
16+
return 'requestManager' in store;
17+
}
18+
1519
export default class Store extends BaseStore {
1620
constructor(args: Record<string, unknown>) {
1721
super(args);
18-
this.requestManager = new RequestManager();
19-
this.requestManager.use([LegacyNetworkHandler, Fetch]);
22+
23+
if (!hasRequestManager(this)) {
24+
this.requestManager = new RequestManager();
25+
this.requestManager.use([LegacyNetworkHandler, Fetch]);
26+
}
2027
this.requestManager.useCache(CacheHandler);
2128
this.registerSchema(buildSchema(this));
2229
}

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 '@ember-data/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 '@ember-data/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

+80-3
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@
44
import { pluralize } from 'ember-inflector';
55

66
import { buildBaseURL, buildQueryParams, QueryParamsSource, type QueryUrlOptions } from '@ember-data/request-utils';
7-
import type { ConstrainedRequestOptions, QueryRequestOptions } from '@ember-data/types/request';
7+
import type {
8+
CacheOptions,
9+
ConstrainedRequestOptions,
10+
PostQueryRequestOptions,
11+
QueryRequestOptions,
12+
} from '@ember-data/types/request';
813

9-
import { copyForwardUrlOptions, extractCacheOptions } from './-utils';
14+
import { ACCEPT_HEADER_VALUE, copyForwardUrlOptions, extractCacheOptions } from './-utils';
1015

1116
/**
1217
* Builds request options to query for resources, usually by a primary
@@ -75,7 +80,7 @@ export function query(
7580

7681
const url = buildBaseURL(urlOptions);
7782
const headers = new Headers();
78-
headers.append('Accept', 'application/vnd.api+json');
83+
headers.append('Accept', ACCEPT_HEADER_VALUE);
7984

8085
return {
8186
url: `${url}?${buildQueryParams(query, options.urlParamsSettings)}`,
@@ -85,3 +90,75 @@ export function query(
8590
op: 'query',
8691
};
8792
}
93+
94+
/**
95+
* Builds request options to query for resources, usually by a primary
96+
* type, configured for the url and header expectations of most JSON:API APIs.
97+
*
98+
* ```ts
99+
* import { postQuery } from '@ember-data/json-api/request';
100+
*
101+
* const options = query('person', { include: ['pets', 'friends'] });
102+
* const data = await store.request(options);
103+
* ```
104+
*
105+
* **Supplying Options to Modify the Request Behavior**
106+
*
107+
* The following options are supported:
108+
*
109+
* - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`.
110+
* - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`.
111+
* - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type
112+
* - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this
113+
* option will delegate to the store's lifetimes service, defaulting to `false` if none is configured.
114+
* - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the
115+
* promise with the cached value, not supplying this option will delegate to the store's lifetimes service,
116+
* defaulting to `false` if none is configured.
117+
* - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`)
118+
*
119+
* ```ts
120+
* import { query } from '@ember-data/json-api/request';
121+
*
122+
* const options = query('person', { include: ['pets', 'friends'] }, { reload: true });
123+
* const data = await store.request(options);
124+
* ```
125+
*
126+
* @method postQuery
127+
* @public
128+
* @static
129+
* @for @ember-data/json-api/request
130+
* @param identifier
131+
* @param query
132+
* @param options
133+
*/
134+
export function postQuery(
135+
type: string,
136+
// eslint-disable-next-line @typescript-eslint/no-shadow
137+
query: QueryParamsSource = {},
138+
options: ConstrainedRequestOptions = {}
139+
): PostQueryRequestOptions {
140+
const cacheOptions = extractCacheOptions(options);
141+
const urlOptions: QueryUrlOptions = {
142+
identifier: { type },
143+
op: 'query',
144+
resourcePath: options.resourcePath ?? pluralize(type),
145+
};
146+
147+
copyForwardUrlOptions(urlOptions, options);
148+
149+
const url = buildBaseURL(urlOptions);
150+
const headers = new Headers();
151+
headers.append('Accept', ACCEPT_HEADER_VALUE);
152+
153+
const queryData = structuredClone(query);
154+
cacheOptions.key = cacheOptions.key ?? `${url}?${buildQueryParams(queryData, options.urlParamsSettings)}`;
155+
156+
return {
157+
url,
158+
method: 'POST',
159+
body: JSON.stringify(query),
160+
headers,
161+
cacheOptions: cacheOptions as CacheOptions & { key: string },
162+
op: 'query',
163+
};
164+
}

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 '@ember-data/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

+2-1
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ URLs follow the most common JSON:API format (dasherized pluralized resource type
6464
* @main @ember-data/json-api/request
6565
*/
6666
export { findRecord } from './-private/builders/find-record';
67-
export { query } from './-private/builders/query';
67+
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
@@ -47,7 +47,7 @@ import { StableDocumentIdentifier } from '@ember-data/types/cache/identifier';
4747
// host and namespace which are provided by the final consuming
4848
// class to the prototype which can result in overwrite errors
4949

50-
interface BuildURLConfig {
50+
export interface BuildURLConfig {
5151
host: string | null;
5252
namespace: string | null;
5353
}

tests/builders/tests/unit/json-api-builder-test.ts

+26-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { module, test } from 'qunit';
22

33
import { setupTest } from 'ember-qunit';
44

5-
import { createRecord, deleteRecord, findRecord, query, updateRecord } from '@ember-data/json-api/request';
5+
import { createRecord, deleteRecord, findRecord, postQuery, query, updateRecord } from '@ember-data/json-api/request';
66
import { setBuildURLConfig } from '@ember-data/request-utils';
77
import Store, { recordIdentifierFor } from '@ember-data/store';
88

@@ -116,6 +116,31 @@ module('JSON:API | Request Builders', function (hooks) {
116116
assert.deepEqual(headersToObject(result.headers), JSON_API_HEADERS);
117117
});
118118

119+
test('postQuery', function (assert) {
120+
const result = postQuery(
121+
'user-setting',
122+
{ include: 'user,friends', sort: 'name:asc', search: ['zeta', 'beta'] },
123+
{ reload: true, backgroundReload: false }
124+
);
125+
assert.deepEqual(
126+
result,
127+
{
128+
url: 'https://api.example.com/api/v1/user-settings',
129+
method: 'POST',
130+
body: JSON.stringify({ include: 'user,friends', sort: 'name:asc', search: ['zeta', 'beta'] }),
131+
headers: new Headers(JSON_API_HEADERS),
132+
cacheOptions: {
133+
reload: true,
134+
backgroundReload: false,
135+
key: 'https://api.example.com/api/v1/user-settings?include=friends%2Cuser&search=beta%2Czeta&sort=name%3Aasc',
136+
},
137+
op: 'query',
138+
},
139+
`query works with type and options`
140+
);
141+
assert.deepEqual(headersToObject(result.headers), JSON_API_HEADERS);
142+
});
143+
119144
test('createRecord passing store record', function (assert) {
120145
const store = this.owner.lookup('service:store') as Store;
121146
const userSetting = store.createRecord('user-setting', {

tests/docs/fixtures/expected.js

+1
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ module.exports = {
261261
'(public) @ember-data/json-api/request @ember-data/json-api/request#updateRecord',
262262
'(public) @ember-data/json-api/request @ember-data/json-api/request#serializePatch',
263263
'(public) @ember-data/json-api/request @ember-data/json-api/request#serializeResources',
264+
'(public) @ember-data/json-api/request @ember-data/json-api/request#setBuildURLConfig',
264265
'(public) @ember-data/legacy-compat SnapshotRecordArray#adapterOptions',
265266
'(public) @ember-data/legacy-compat SnapshotRecordArray#include',
266267
'(public) @ember-data/legacy-compat SnapshotRecordArray#length',

0 commit comments

Comments
 (0)