From d0061f448cb54e01721a9feb65d3944ce1bec4df Mon Sep 17 00:00:00 2001
From: Chris Thoburn <runspired@gmail.com>
Date: Tue, 26 Sep 2023 15:22:23 -0700
Subject: [PATCH] feat: implement postQuery builder

---
 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', {