Skip to content

Commit

Permalink
fix: use json content-type header (#949)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonkuhrt authored Jun 30, 2024
1 parent 31e586e commit 833d172
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 36 deletions.
16 changes: 15 additions & 1 deletion src/layers/5_client/client.test.ts
Original file line number Diff line number Diff line change
@@ -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`)
Expand All @@ -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)
})
})
})
89 changes: 54 additions & 35 deletions src/layers/5_core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -104,6 +104,9 @@ type RequestInput = {
}

export type HookDefExchange = {
slots: {
fetch: typeof fetch
}
input:
& InterfaceInput
& TransportInput<
Expand Down Expand Up @@ -186,14 +189,16 @@ export const anyware = Anyware.create<HookSequence, HookMap, ExecutionResult>({

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`: {
Expand All @@ -215,12 +220,19 @@ export const anyware = Anyware.create<HookSequence, HookMap, ExecutionResult>({
}
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,
},
Expand All @@ -230,36 +242,43 @@ export const anyware = Anyware.create<HookSequence, HookMap, ExecutionResult>({
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) {
Expand Down
11 changes: 11 additions & 0 deletions tests/_/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@ export const createResponse = (body: object) =>

interface Fixtures {
fetch: Mock
graffle: Client<any, any>
}

import { Graffle } from '../../src/entrypoints/alpha/main.js'
import type { Client } from '../../src/layers/5_client/client.js'

export const test = testBase.extend<Fixtures>({
// @ts-expect-error https://github.com/vitest-dev/vitest/discussions/5710
// eslint-disable-next-line
Expand All @@ -20,4 +24,11 @@ export const test = testBase.extend<Fixtures>({
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)
},
})

0 comments on commit 833d172

Please sign in to comment.