From 833d172a0cc22dbca2b3c954d767d2eb0d49d45d Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Sun, 30 Jun 2024 00:51:44 -0400 Subject: [PATCH] fix: use json content-type header (#949) --- src/layers/5_client/client.test.ts | 16 +++- src/layers/5_core/core.ts | 89 +++++++++++-------- .../Upload/{Upload.spec.ts => Upload.test.ts} | 0 tests/_/helpers.ts | 11 +++ 4 files changed, 80 insertions(+), 36 deletions(-) rename src/layers/6_extensions/Upload/{Upload.spec.ts => Upload.test.ts} (100%) diff --git a/src/layers/5_client/client.test.ts b/src/layers/5_client/client.test.ts index c5754fc2a..80168c27e 100644 --- a/src/layers/5_client/client.test.ts +++ b/src/layers/5_client/client.test.ts @@ -1,5 +1,7 @@ -import { describe, expect, test } from 'vitest' +import { describe, expect } from 'vitest' +import { createResponse, test } from '../../../tests/_/helpers.js' import { Graffle } from '../../entrypoints/alpha/main.js' +import { CONTENT_TYPE_GQL, CONTENT_TYPE_JSON } from '../../lib/http.js' describe(`without schemaIndex only raw is available`, () => { const schema = new URL(`https://foo.io/api/graphql`) @@ -21,3 +23,15 @@ describe(`without schemaIndex only raw is available`, () => { expect(graffle.rawOrThrow).toBeTypeOf(`function`) }) }) + +describe(`interface`, () => { + describe(`http`, () => { + test(`sends well formed request`, async ({ fetch, graffle }) => { + fetch.mockImplementationOnce(() => createResponse({ data: { greetings: `Hello World` } })) + await graffle.raw({ document: `query { greetings }` }) + const request = fetch.mock.calls[0][0] as Request // eslint-disable-line + expect(request.headers.get(`content-type`)).toEqual(CONTENT_TYPE_JSON) + expect(request.headers.get(`accept`)).toEqual(CONTENT_TYPE_GQL) + }) + }) +}) diff --git a/src/layers/5_core/core.ts b/src/layers/5_core/core.ts index 2465705fb..92de45bd1 100644 --- a/src/layers/5_core/core.ts +++ b/src/layers/5_core/core.ts @@ -3,7 +3,7 @@ import { print } from 'graphql' import { Anyware } from '../../lib/anyware/__.js' import { type StandardScalarVariables } from '../../lib/graphql.js' import { parseExecutionResult } from '../../lib/graphqlHTTP.js' -import { CONTENT_TYPE_GQL } from '../../lib/http.js' +import { CONTENT_TYPE_GQL, CONTENT_TYPE_JSON } from '../../lib/http.js' import { casesExhausted } from '../../lib/prelude.js' import { execute } from '../0_functions/execute.js' import type { Schema } from '../1_Schema/__.js' @@ -104,6 +104,9 @@ type RequestInput = { } export type HookDefExchange = { + slots: { + fetch: typeof fetch + } input: & InterfaceInput & TransportInput< @@ -186,14 +189,16 @@ export const anyware = Anyware.create({ switch (input.transport) { case `http`: { + const body = slots.body({ + query: document, + variables, + operationName: `todo`, + }) + return { ...input, url: input.schema, - body: slots.body({ - query: document, - variables, - operationName: `todo`, - }), + body, } } case `memory`: { @@ -215,12 +220,19 @@ export const anyware = Anyware.create({ } case `http`: { const headers = new Headers(input.headers) - headers.append(`accept`, CONTENT_TYPE_GQL) + // @see https://graphql.github.io/graphql-over-http/draft/#sec-Accept + headers.set(`accept`, CONTENT_TYPE_GQL) + // @see https://graphql.github.io/graphql-over-http/draft/#sec-POST + // todo if body is something else, say upload extension turns it into a FormData, then fetch will automatically set the content-type header. + // ... however we should not rely on that behavior, and instead error here if there is no content type header and we cannot infer it here? + if (typeof input.body === `string`) { + headers.set(`content-type`, CONTENT_TYPE_JSON) + } return { ...input, request: { url: input.url, - body: input.body, // JSON.stringify({ query, variables, operationName }), + body: input.body, method: `POST`, headers, }, @@ -230,36 +242,43 @@ export const anyware = Anyware.create({ throw casesExhausted(input) } }, - exchange: async ({ input }) => { - switch (input.transport) { - case `http`: { - const response = await fetch( - new Request(input.request.url, { - method: input.request.method, - headers: input.request.headers, - body: input.request.body, - }), - ) - return { - ...input, - response, + exchange: { + slots: { + fetch: (request) => { + return fetch(request) + }, + }, + run: async ({ input, slots }) => { + switch (input.transport) { + case `http`: { + const response = await slots.fetch( + new Request(input.request.url, { + method: input.request.method, + headers: input.request.headers, + body: input.request.body, + }), + ) + return { + ...input, + response, + } } - } - case `memory`: { - const result = await execute({ - schema: input.schema, - document: input.query, - variables: input.variables, - operationName: input.operationName, - }) - return { - ...input, - result, + case `memory`: { + const result = await execute({ + schema: input.schema, + document: input.query, + variables: input.variables, + operationName: input.operationName, + }) + return { + ...input, + result, + } } + default: + throw casesExhausted(input) } - default: - throw casesExhausted(input) - } + }, }, unpack: async ({ input }) => { switch (input.transport) { diff --git a/src/layers/6_extensions/Upload/Upload.spec.ts b/src/layers/6_extensions/Upload/Upload.test.ts similarity index 100% rename from src/layers/6_extensions/Upload/Upload.spec.ts rename to src/layers/6_extensions/Upload/Upload.test.ts diff --git a/tests/_/helpers.ts b/tests/_/helpers.ts index e4df0b67e..e667577d3 100644 --- a/tests/_/helpers.ts +++ b/tests/_/helpers.ts @@ -7,8 +7,12 @@ export const createResponse = (body: object) => interface Fixtures { fetch: Mock + graffle: Client } +import { Graffle } from '../../src/entrypoints/alpha/main.js' +import type { Client } from '../../src/layers/5_client/client.js' + export const test = testBase.extend({ // @ts-expect-error https://github.com/vitest-dev/vitest/discussions/5710 // eslint-disable-next-line @@ -20,4 +24,11 @@ export const test = testBase.extend({ await use(fetchMock) globalThis.fetch = fetch }, + graffle: async ({ fetch }, use) => { + const graffle = Graffle.create({ schema: new URL(`https://foo.io/api/graphql`) }) + .use(async ({ exchange }) => { + return exchange({ using: { fetch } }) + }) + await use(graffle) + }, })