Skip to content

Commit

Permalink
feat: Improve extensibility (#9069)
Browse files Browse the repository at this point in the history
* feat: improve configurability of json-api builder and request-manager

* fix types

* add tests

* fix prettier

* fix forgottend debugger

* fixup lint

* fix lint
  • Loading branch information
runspired committed Feb 12, 2024
1 parent 553ec74 commit 3b4ebdb
Show file tree
Hide file tree
Showing 10 changed files with 158 additions and 17 deletions.
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
"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",
"test:production": "pnpm --filter main-test-app --filter graph-test-app --filter json-api-test-app run test -e production",
"test:try-one": "pnpm --filter main-test-app run test:try-one",
"test:docs": "pnpm build:docs && pnpm --filter docs-tests test",
"test:encapsulation": "pnpm --filter '*-encapsulation-test-app' run test",
"test:fastboot": "pnpm --filter fastboot-test-app test",
"test:embroider": "pnpm --filter embroider-basic-compat test",
"test:infra": "pnpm --filter @ember-data/unpublished-test-infra test",
"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",
"test:infra": "pnpm run -r --workspace-concurrency=-1 --if-present test:infra",
"test-external:ember-m3": "node ./scripts/test-external-partner-project.js ember-m3 https://github.com/hjdivad/ember-m3.git",
"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",
"test-external:model-fragments": "node ./scripts/test-external-partner-project.js model-fragments https://github.com/lytics/ember-data-model-fragments.git",
Expand Down
11 changes: 9 additions & 2 deletions packages/-ember-data/addon/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,18 @@ import type { CacheCapabilitiesManager } from '@ember-data/types/q/cache-store-w
import type { ModelSchema } from '@ember-data/types/q/ds-model';
import type { StableRecordIdentifier } from '@ember-data/types/q/identifier';

function hasRequestManager(store: BaseStore): boolean {
return 'requestManager' in store;
}

export default class Store extends BaseStore {
constructor(args: Record<string, unknown>) {
super(args);
this.requestManager = new RequestManager();
this.requestManager.use([LegacyNetworkHandler, Fetch]);

if (!hasRequestManager(this)) {
this.requestManager = new RequestManager();
this.requestManager.use([LegacyNetworkHandler, Fetch]);
}
this.requestManager.useCache(CacheHandler);
this.registerSchema(buildSchema(this));
}
Expand Down
84 changes: 84 additions & 0 deletions packages/json-api/src/-private/builders/-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,90 @@
/**
* @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 '@ember-data/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';
const DEFAULT_CONFIG: JSONAPIConfig = { host: '', namespace: '' };
export let CONFIG: JSONAPIConfig = DEFAULT_CONFIG;
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 {
CONFIG = Object.assign({}, DEFAULT_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(' ');
}
}
ACCEPT_HEADER_VALUE = accept;
}

setConfig(config);
}

export function copyForwardUrlOptions(urlOptions: UrlOptions, options: ConstrainedRequestOptions): void {
if ('host' in options) {
urlOptions.host = options.host;
Expand Down
4 changes: 2 additions & 2 deletions packages/json-api/src/-private/builders/find-record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
RemotelyAccessibleIdentifier,
} from '@ember-data/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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions packages/json-api/src/-private/builders/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import type {
QueryRequestOptions,
} from '@ember-data/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
Expand Down Expand Up @@ -80,7 +80,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)}`,
Expand Down Expand Up @@ -148,7 +148,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)}`;
Expand Down
8 changes: 4 additions & 4 deletions packages/json-api/src/-private/builders/save-record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
UpdateRequestOptions,
} from '@ember-data/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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/json-api/src/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
2 changes: 1 addition & 1 deletion packages/request-utils/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import { StableDocumentIdentifier } from '@ember-data/types/cache/identifier';
// 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;
}
Expand Down
1 change: 1 addition & 0 deletions tests/docs/fixtures/expected.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,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',
Expand Down
48 changes: 48 additions & 0 deletions tests/main/tests/integration/store-extension-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { module, test } from 'qunit';

Check failure on line 1 in tests/main/tests/integration/store-extension-test.ts

View workflow job for this annotation

GitHub Actions / lint

Run autofix to sort these imports!
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');

Check failure on line 33 in tests/main/tests/integration/store-extension-test.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected equality comparison in assert.true call. Use assert.strictEqual(store.requestManager, requestManager) instead
});

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');

Check failure on line 46 in tests/main/tests/integration/store-extension-test.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected equality comparison in assert.true call. Use assert.strictEqual(store.requestManager, requestManager) instead
});
});

0 comments on commit 3b4ebdb

Please sign in to comment.