Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Improve extensibility #9069

Merged
merged 7 commits into from
Oct 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
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 @@ -21,13 +21,20 @@ 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<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 '@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';
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 '@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
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 @@ -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
Expand Down Expand Up @@ -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)}`,
Expand Down Expand Up @@ -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)}`;
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 '@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;
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 @@ -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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@runspired I have a question, do you think it would be useful to allow users to overwrite function that does resourcePath conversion right here as part of BuildURLConfig?

Something like

BuildURLConfig({
  host: "https://myhost.com",
  namespace: "api/v1",
  resourcePath: (resourceName) => {
    return camelize(pluralize(resourceName));
  }
});

or is it too crazy and I should go to sleep ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

go to sleep :P

This is mostly already configurable in this way though maybe we could make it even more configurable

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I know you can configure it using builder options, but would be nice to have it in single place like it used to be in adapter. It would cover 90% cases, and you can still change it on builder level

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 @@ -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',
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';
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');
});
});