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: first class abort errors #1061

Merged
merged 5 commits into from
Sep 4, 2024
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ coverage
tsconfig.vitest-temp.json
website/.vitepress/dist
website/.vitepress/cache
legacy
1 change: 1 addition & 0 deletions examples/transport-http_abort.output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
'This operation was aborted'
32 changes: 32 additions & 0 deletions examples/transport-http_abort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* It is possible to cancel a request using an `AbortController` signal.
*/

import { gql, Graffle } from '../src/entrypoints/main.js'
import { publicGraphQLSchemaEndpoints, show } from './$helpers.js'

const abortController = new AbortController()

const graffle = Graffle.create({
schema: publicGraphQLSchemaEndpoints.SocialStudies,
})

const resultPromise = graffle
.with({ request: { signal: abortController.signal } })
.raw({
document: gql`
{
countries {
name
}
}
`,
})

abortController.abort()

const result = await resultPromise.catch((error: unknown) => (error as Error).message)

show(result)

// todo .with(...) variant
2 changes: 1 addition & 1 deletion examples/transport-http_fetch.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable */
import { Graffle } from '../src/entrypoints/main.js'
import { show, showJson } from './$helpers.js'
import { showJson } from './$helpers.js'
import { publicGraphQLSchemaEndpoints } from './$helpers.js'

const graffle = Graffle
Expand Down
1 change: 0 additions & 1 deletion src/layers/5_core/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,6 @@ export const anyware = Anyware.create<HookSequence, HookMap, ExecutionResult>({
// 1. Generate a map of possible custom scalar paths (tree structure)
// 2. When traversing the result, skip keys that are not in the map
const dataDecoded = Result.decode(getRootIndexOrThrow(input.context, input.rootTypeName), input.result.data)
// console.log(8, Object.keys({ ...input.result, data: dataDecoded }))
switch (input.transport) {
case `memory`: {
return { ...input.result, data: dataDecoded }
Expand Down
4 changes: 3 additions & 1 deletion src/layers/6_client/RootTypeMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,9 @@ export type RootTypeMethods<$Config extends Config, $Index extends Schema.Index,
// dprint-ignore
type RootMethod<$Config extends Config, $Index extends Schema.Index, $RootTypeName extends Schema.RootTypeName> =
<$SelectionSet extends object>(selectionSet: Exact<$SelectionSet, SelectionSet.Root<$Index, $RootTypeName>>) =>
Promise<ResolveOutputReturnRootType<$Config, $Index, ResultSet.Root<AugmentRootTypeSelectionWithTypename<$Config,$Index,$RootTypeName,$SelectionSet>, $Index, $RootTypeName>>>
Promise<
ResolveOutputReturnRootType<$Config, $Index, ResultSet.Root<AugmentRootTypeSelectionWithTypename<$Config,$Index,$RootTypeName,$SelectionSet>, $Index, $RootTypeName>>
>

// dprint-ignore
type RootTypeFieldMethod<$Context extends RootTypeFieldContext> =
Expand Down
41 changes: 23 additions & 18 deletions src/layers/6_client/Settings/Config.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import type { GraphQLError } from 'graphql'
import type { Simplify } from 'type-fest'
import type { GraphQLExecutionResultError } from '../../../lib/graphql.js'
import type { ConfigManager, StringKeyof, Values } from '../../../lib/prelude.js'
import type { ConfigManager, SimplifyExceptError, StringKeyof, Values } from '../../../lib/prelude.js'
import type { Schema } from '../../1_Schema/__.js'
import type { GlobalRegistry } from '../../2_generator/globalRegistry.js'
import type { SelectionSet } from '../../3_SelectionSet/__.js'
import type { Transport } from '../../5_core/types.js'
import type { ErrorsOther } from '../client.js'
import type { InputStatic } from './Input.js'
import type { RequestInputOptions } from './inputIncrementable/request.js'

Expand Down Expand Up @@ -116,25 +117,29 @@ export type Config = {

// dprint-ignore
export type ResolveOutputReturnRootType<$Config extends Config, $Index extends Schema.Index, $Data> =
| Simplify<IfConfiguredGetOutputErrorReturns<$Config>>
| (
$Config['output']['envelope']['enabled'] extends true
? Envelope<$Config, IfConfiguredStripSchemaErrorsFromDataRootType<$Config, $Index, $Data>>
: Simplify<IfConfiguredStripSchemaErrorsFromDataRootType<$Config, $Index, $Data>>
)
SimplifyExceptError<
| IfConfiguredGetOutputErrorReturns<$Config>
| (
$Config['output']['envelope']['enabled'] extends true
? Envelope<$Config, IfConfiguredStripSchemaErrorsFromDataRootType<$Config, $Index, $Data>>
: Simplify<IfConfiguredStripSchemaErrorsFromDataRootType<$Config, $Index, $Data>>
)
>

// dprint-ignore
export type ResolveOutputReturnRootField<$Config extends Config, $Index extends Schema.Index, $Data, $DataRaw = undefined> =
| IfConfiguredGetOutputErrorReturns<$Config>
| (
$Config['output']['envelope']['enabled'] extends true
// todo: a typed execution result that allows for additional error types.
// currently it is always graphql execution error however envelope configuration can put more errors into that.
? Envelope<$Config, $DataRaw extends undefined
? Simplify<IfConfiguredStripSchemaErrorsFromDataRootField<$Config, $Index, $Data>>
: Simplify<IfConfiguredStripSchemaErrorsFromDataRootType<$Config, $Index, $DataRaw>>>
: Simplify<IfConfiguredStripSchemaErrorsFromDataRootField<$Config, $Index, $Data>>
)
SimplifyExceptError<
| IfConfiguredGetOutputErrorReturns<$Config>
| (
$Config['output']['envelope']['enabled'] extends true
// todo: a typed execution result that allows for additional error types.
// currently it is always graphql execution error however envelope configuration can put more errors into that.
? Envelope<$Config, $DataRaw extends undefined
? Simplify<IfConfiguredStripSchemaErrorsFromDataRootField<$Config, $Index, $Data>>
: Simplify<IfConfiguredStripSchemaErrorsFromDataRootType<$Config, $Index, $DataRaw>>>
: Simplify<IfConfiguredStripSchemaErrorsFromDataRootField<$Config, $Index, $Data>>
)
>

type ObjMap<T = unknown> = {
[key: string]: T
Expand Down Expand Up @@ -193,7 +198,7 @@ type ConfigGetOutputError<$Config extends Config, $ErrorCategory extends ErrorCa
// dprint-ignore
type IfConfiguredGetOutputErrorReturns<$Config extends Config> =
| (ConfigGetOutputError<$Config, 'execution'> extends 'return' ? GraphQLExecutionResultError : never)
| (ConfigGetOutputError<$Config, 'other'> extends 'return' ? Error : never)
| (ConfigGetOutputError<$Config, 'other'> extends 'return' ? ErrorsOther : never)
| (ConfigGetOutputError<$Config, 'schema'> extends 'return' ? Error : never)

// dprint-ignore
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
/* eslint-disable */
import { type ExecutionResult } from 'graphql'
import type { ObjMap } from 'graphql/jsutils/ObjMap.js'
import { describe } from 'node:test'
import type { Simplify } from 'type-fest'
import type { ConditionalSimplify } from 'type-fest/source/conditional-simplify.js'
import { expectTypeOf, test } from 'vitest'
import { Graffle } from '../../../../tests/_/schema/generated/__.js'
import { schema } from '../../../../tests/_/schema/schema.js'
import { type GraphQLExecutionResultError } from '../../../lib/graphql.js'
import { type Envelope, type OutputConfigDefault } from './Config.js'
import type { ErrorsOther } from '../client.js'
import { type Envelope } from './Config.js'

const G = Graffle.create

Expand Down Expand Up @@ -104,15 +102,15 @@ describe('.envelope', () => {
describe('.errors', () => {
test('defaults to execution errors in envelope', () => {
const g = G({ schema, output: { defaults: { errorChannel: 'return' }, envelope: true } })
expectTypeOf(g.query.__typename()).resolves.toEqualTypeOf<ExecutionResult<{ __typename: 'Query' }> | Error>()
expectTypeOf(g.query.__typename()).resolves.toEqualTypeOf<ExecutionResult<{ __typename: 'Query' }> | ErrorsOther>()
})
test('.execution:false restores errors to return', async () => {
const g = G({
schema,
output: { defaults: { errorChannel: 'return' }, envelope: { errors: { execution: false } } },
})
expectTypeOf(g.query.__typename()).resolves.toEqualTypeOf<
Omit<ExecutionResult<{ __typename: 'Query' }>, 'errors'> | Error | GraphQLExecutionResultError
Omit<ExecutionResult<{ __typename: 'Query' }>, 'errors'> | ErrorsOther | GraphQLExecutionResultError
>()
})
test('.other:true raises them to envelope', () => {
Expand Down Expand Up @@ -140,7 +138,7 @@ describe('defaults.errorChannel: "return"', () => {
describe('puts errors into return type', () => {
const g = G({ schema, output: { defaults: { errorChannel: 'return' } } })
test('query.<fieldMethod>', () => {
expectTypeOf(g.query.__typename()).resolves.toEqualTypeOf<'Query' | Error | GraphQLExecutionResultError>()
expectTypeOf(g.query.__typename()).resolves.toEqualTypeOf<'Query' | ErrorsOther | GraphQLExecutionResultError>()
})
})
describe('with .errors', () => {
Expand All @@ -149,7 +147,7 @@ describe('defaults.errorChannel: "return"', () => {
schema,
output: { defaults: { errorChannel: 'return' }, errors: { execution: 'throw' } },
})
expectTypeOf(await g.query.__typename()).toEqualTypeOf<'Query' | Error>()
expectTypeOf(await g.query.__typename()).toEqualTypeOf<'Query' | ErrorsOther>()
})
test('.other: throw', async () => {
const g = G({
Expand Down
32 changes: 30 additions & 2 deletions src/layers/6_client/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,42 @@ describe(`transport`, () => {
const request = fetch.mock.calls[0]?.[0]
expect(request?.headers.get(`x-foo`)).toEqual(`bar`)
})

test(`sends well formed request`, async ({ fetch, graffle }) => {
test(`sends spec compliant request`, async ({ fetch, graffle }) => {
fetch.mockImplementationOnce(() => Promise.resolve(createResponse({ data: { greetings: `Hello World` } })))
await graffle.rawString({ document: `query { greetings }` })
const request = fetch.mock.calls[0]?.[0]
expect(request?.headers.get(`content-type`)).toEqual(CONTENT_TYPE_JSON)
expect(request?.headers.get(`accept`)).toEqual(CONTENT_TYPE_GQL)
})
describe(`signal`, () => {
// JSDom and Node result in different errors. JSDom is a plain Error type. Presumably an artifact of JSDom and now in actual browsers.
const abortErrorMessagePattern = /This operation was aborted|AbortError: The operation was aborted/
test(`AbortController at instance level works`, async () => {
const abortController = new AbortController()
const graffle = Graffle.create({
schema: endpoint,
request: { signal: abortController.signal },
})
const resultPromise = graffle.rawString({ document: `query { id }` })
abortController.abort()
const { caughtError } = await resultPromise.catch((caughtError: unknown) => ({ caughtError })) as any as {
caughtError: Error
}
expect(caughtError.message).toMatch(abortErrorMessagePattern)
})
test(`AbortController at method level works`, async () => {
const abortController = new AbortController()
const graffle = Graffle.create({
schema: endpoint,
}).with({ request: { signal: abortController.signal } })
const resultPromise = graffle.rawString({ document: `query { id }` })
abortController.abort()
const { caughtError } = await resultPromise.catch((caughtError: unknown) => ({ caughtError })) as any as {
caughtError: Error
}
expect(caughtError.message).toMatch(abortErrorMessagePattern)
})
})
})
describe(`memory`, () => {
test(`anyware hooks are typed to memory transport`, () => {
Expand Down
Loading