From 553ec7435124abe05aa17d7bb1c2536e4a7b1160 Mon Sep 17 00:00:00 2001 From: Chris Thoburn Date: Tue, 26 Sep 2023 15:26:42 -0700 Subject: [PATCH] feat: implement postQuery builder (#8925) --- ember-data-types/request.ts | 9 +++ .../json-api/src/-private/builders/query.ts | 79 ++++++++++++++++++- packages/json-api/src/request.ts | 2 +- .../tests/unit/json-api-builder-test.ts | 27 ++++++- 4 files changed, 114 insertions(+), 3 deletions(-) diff --git a/ember-data-types/request.ts b/ember-data-types/request.ts index 75a4e62cf13..dda3dc139e7 100644 --- a/ember-data-types/request.ts +++ b/ember-data-types/request.ts @@ -24,6 +24,15 @@ export type QueryRequestOptions = { op: 'query'; }; +export type PostQueryRequestOptions = { + url: string; + method: 'POST' | 'QUERY'; + headers: Headers; + body: string; + cacheOptions: CacheOptions & { key: string }; + op: 'query'; +}; + export type DeleteRequestOptions = { url: string; method: 'DELETE'; diff --git a/packages/json-api/src/-private/builders/query.ts b/packages/json-api/src/-private/builders/query.ts index afb57949ae9..420cf88407a 100644 --- a/packages/json-api/src/-private/builders/query.ts +++ b/packages/json-api/src/-private/builders/query.ts @@ -4,7 +4,12 @@ import { pluralize } from 'ember-inflector'; import { buildBaseURL, buildQueryParams, QueryParamsSource, type QueryUrlOptions } from '@ember-data/request-utils'; -import type { ConstrainedRequestOptions, QueryRequestOptions } from '@ember-data/types/request'; +import type { + CacheOptions, + ConstrainedRequestOptions, + PostQueryRequestOptions, + QueryRequestOptions, +} from '@ember-data/types/request'; import { copyForwardUrlOptions, extractCacheOptions } from './-utils'; @@ -85,3 +90,75 @@ export function query( op: 'query', }; } + +/** + * Builds request options to query for resources, usually by a primary + * type, configured for the url and header expectations of most JSON:API APIs. + * + * ```ts + * import { postQuery } from '@ember-data/json-api/request'; + * + * const options = query('person', { include: ['pets', 'friends'] }); + * const data = await store.request(options); + * ``` + * + * **Supplying Options to Modify the Request Behavior** + * + * The following options are supported: + * + * - `host` - The host to use for the request, defaults to the `host` configured with `setBuildURLConfig`. + * - `namespace` - The namespace to use for the request, defaults to the `namespace` configured with `setBuildURLConfig`. + * - `resourcePath` - The resource path to use for the request, defaults to pluralizing the supplied type + * - `reload` - Whether to forcibly reload the request if it is already in the store, not supplying this + * option will delegate to the store's lifetimes service, defaulting to `false` if none is configured. + * - `backgroundReload` - Whether to reload the request if it is already in the store, but to also resolve the + * promise with the cached value, not supplying this option will delegate to the store's lifetimes service, + * defaulting to `false` if none is configured. + * - `urlParamsSetting` - an object containing options for how to serialize the query params (see `buildQueryParams`) + * + * ```ts + * import { query } from '@ember-data/json-api/request'; + * + * const options = query('person', { include: ['pets', 'friends'] }, { reload: true }); + * const data = await store.request(options); + * ``` + * + * @method postQuery + * @public + * @static + * @for @ember-data/json-api/request + * @param identifier + * @param query + * @param options + */ +export function postQuery( + type: string, + // eslint-disable-next-line @typescript-eslint/no-shadow + query: QueryParamsSource = {}, + options: ConstrainedRequestOptions = {} +): PostQueryRequestOptions { + const cacheOptions = extractCacheOptions(options); + const urlOptions: QueryUrlOptions = { + identifier: { type }, + op: 'query', + resourcePath: options.resourcePath ?? pluralize(type), + }; + + copyForwardUrlOptions(urlOptions, options); + + const url = buildBaseURL(urlOptions); + const headers = new Headers(); + headers.append('Accept', 'application/vnd.api+json'); + + const queryData = structuredClone(query); + cacheOptions.key = cacheOptions.key ?? `${url}?${buildQueryParams(queryData, options.urlParamsSettings)}`; + + return { + url, + method: 'POST', + body: JSON.stringify(query), + headers, + cacheOptions: cacheOptions as CacheOptions & { key: string }, + op: 'query', + }; +} diff --git a/packages/json-api/src/request.ts b/packages/json-api/src/request.ts index b601fed9072..b664706957e 100644 --- a/packages/json-api/src/request.ts +++ b/packages/json-api/src/request.ts @@ -64,6 +64,6 @@ URLs follow the most common JSON:API format (dasherized pluralized resource type * @main @ember-data/json-api/request */ export { findRecord } from './-private/builders/find-record'; -export { query } from './-private/builders/query'; +export { query, postQuery } from './-private/builders/query'; export { deleteRecord, createRecord, updateRecord } from './-private/builders/save-record'; export { serializeResources, serializePatch } from './-private/serialize'; diff --git a/tests/builders/tests/unit/json-api-builder-test.ts b/tests/builders/tests/unit/json-api-builder-test.ts index 44a46ac994a..79992d1ae38 100644 --- a/tests/builders/tests/unit/json-api-builder-test.ts +++ b/tests/builders/tests/unit/json-api-builder-test.ts @@ -2,7 +2,7 @@ import { module, test } from 'qunit'; import { setupTest } from 'ember-qunit'; -import { createRecord, deleteRecord, findRecord, query, updateRecord } from '@ember-data/json-api/request'; +import { createRecord, deleteRecord, findRecord, postQuery, query, updateRecord } from '@ember-data/json-api/request'; import { setBuildURLConfig } from '@ember-data/request-utils'; import Store, { recordIdentifierFor } from '@ember-data/store'; @@ -116,6 +116,31 @@ module('JSON:API | Request Builders', function (hooks) { assert.deepEqual(headersToObject(result.headers), JSON_API_HEADERS); }); + test('postQuery', function (assert) { + const result = postQuery( + 'user-setting', + { include: 'user,friends', sort: 'name:asc', search: ['zeta', 'beta'] }, + { reload: true, backgroundReload: false } + ); + assert.deepEqual( + result, + { + url: 'https://api.example.com/api/v1/user-settings', + method: 'POST', + body: JSON.stringify({ include: 'user,friends', sort: 'name:asc', search: ['zeta', 'beta'] }), + headers: new Headers(JSON_API_HEADERS), + cacheOptions: { + reload: true, + backgroundReload: false, + key: 'https://api.example.com/api/v1/user-settings?include=friends%2Cuser&search=beta%2Czeta&sort=name%3Aasc', + }, + op: 'query', + }, + `query works with type and options` + ); + assert.deepEqual(headersToObject(result.headers), JSON_API_HEADERS); + }); + test('createRecord passing store record', function (assert) { const store = this.owner.lookup('service:store') as Store; const userSetting = store.createRecord('user-setting', {