diff --git a/src/client/Config.ts b/src/client/Config.ts
index 759055761..9a97a3114 100644
--- a/src/client/Config.ts
+++ b/src/client/Config.ts
@@ -1,7 +1,22 @@
import type { ExecutionResult } from 'graphql'
+import type { GraphQLExecutionResultError } from '../lib/graphql.js'
+import type { SetProperty } from '../lib/prelude.js'
-// todo: dataAndErrors | dataAndSchemaErrors
-export type ReturnModeType = 'graphql' | 'data'
+export type ReturnModeType =
+ | ReturnModeTypeGraphQL
+ | ReturnModeTypeData
+ | ReturnModeTypeDataAndSchemaErrors
+ | ReturnModeTypeDataAllErrors
+
+export type ReturnModeTypeBase = ReturnModeTypeGraphQL | ReturnModeTypeData | ReturnModeTypeDataAllErrors
+
+export type ReturnModeTypeGraphQL = 'graphql'
+
+export type ReturnModeTypeData = 'data'
+
+export type ReturnModeTypeDataAllErrors = 'dataAndAllErrors'
+
+export type ReturnModeTypeDataAndSchemaErrors = 'dataAndSchemaErrors'
export type OptionsInput = {
returnMode: ReturnModeType | undefined
@@ -16,9 +31,15 @@ export type Config = {
}
export type ApplyInputDefaults = {
- [Key in keyof OptionsInputDefaults]: undefined extends Input[Key] ? OptionsInputDefaults[Key] : Input[Key]
+ [Key in keyof OptionsInputDefaults]: undefined extends Input[Key] ? OptionsInputDefaults[Key]
+ : Exclude
}
// dprint-ignore
-export type ReturnMode<$Config extends Config, $Data> =
- $Config['returnMode'] extends 'graphql' ? ExecutionResult<$Data> : $Data
+export type ReturnMode<$Config extends Config, $Data, $DataRaw = undefined> =
+ $Config['returnMode'] extends 'graphql' ? ExecutionResult<$DataRaw extends undefined ? $Data : $DataRaw> :
+ $Config['returnMode'] extends 'data' ? $Data :
+ $Data | GraphQLExecutionResultError
+
+export type OrThrowifyConfig<$Config extends Config> = $Config['returnMode'] extends 'graphql' ? $Config
+ : SetProperty<$Config, 'returnMode', 'data'>
diff --git a/src/client/RootTypeMethods.ts b/src/client/RootTypeMethods.ts
index 946f0f49f..1033c2457 100644
--- a/src/client/RootTypeMethods.ts
+++ b/src/client/RootTypeMethods.ts
@@ -1,25 +1,32 @@
-import type { ExecutionResult } from 'graphql'
+import type { OperationName } from '../lib/graphql.js'
import type { Exact } from '../lib/prelude.js'
import type { TSError } from '../lib/TSError.js'
import type { InputFieldsAllNullable, Schema } from '../Schema/__.js'
-import type { Config, OptionsInputDefaults, ReturnMode } from './Config.js'
+import type { Config, OrThrowifyConfig, ReturnMode } from './Config.js'
import type { ResultSet } from './ResultSet/__.js'
import type { SelectionSet } from './SelectionSet/__.js'
-type OperationName = 'query' | 'mutation'
+type RootTypeFieldContext = {
+ Config: Config
+ Index: Schema.Index
+ RootTypeName: Schema.RootTypeName
+ RootTypeFieldName: string
+ Field: Schema.SomeField
+}
// dprint-ignore
-export type GetRootTypeMethods<$Config extends OptionsInputDefaults, $Index extends Schema.Index> = {
+export type GetRootTypeMethods<$Config extends Config, $Index extends Schema.Index> = {
[$OperationName in OperationName as $Index['Root'][Capitalize<$OperationName>] extends null ? never : $OperationName]:
RootTypeMethods<$Config, $Index, Capitalize<$OperationName>>
}
// dprint-ignore
-export type RootTypeMethods<$Config extends OptionsInputDefaults, $Index extends Schema.Index, $RootTypeName extends Schema.RootTypeName> =
+export type RootTypeMethods<$Config extends Config, $Index extends Schema.Index, $RootTypeName extends Schema.RootTypeName> =
$Index['Root'][$RootTypeName] extends Schema.Object$2 ?
(
& {
$batch: RootMethod<$Config, $Index, $RootTypeName>
+ $batchOrThrow: RootMethod, $Index, $RootTypeName>
}
& {
[$RootTypeFieldName in keyof $Index['Root'][$RootTypeName]['fields'] & string]:
@@ -31,6 +38,16 @@ export type RootTypeMethods<$Config extends OptionsInputDefaults, $Index extends
Field: $Index['Root'][$RootTypeName]['fields'][$RootTypeFieldName]
}>
}
+ & {
+ [$RootTypeFieldName in keyof $Index['Root'][$RootTypeName]['fields'] & string as `${$RootTypeFieldName}OrThrow`]:
+ RootTypeFieldMethod<{
+ Config: OrThrowifyConfig<$Config>,
+ Index: $Index,
+ RootTypeName: $RootTypeName,
+ RootTypeFieldName: $RootTypeFieldName
+ Field: $Index['Root'][$RootTypeName]['fields'][$RootTypeFieldName]
+ }>
+ }
)
: TSError<'RootTypeMethods', `Your schema does not have the root type "${$RootTypeName}".`>
@@ -65,14 +82,4 @@ type ScalarFieldMethod<$Context extends RootTypeFieldContext> =
(() => Promise>>)
// dprint-ignore
type ReturnModeForFieldMethod<$Context extends RootTypeFieldContext, $Data> =
- $Context['Config']['returnMode'] extends 'data'
- ? $Data
- : ExecutionResult<{ [k in $Context['RootTypeFieldName']] : $Data }>
-
-type RootTypeFieldContext = {
- Config: Config
- Index: Schema.Index
- RootTypeName: Schema.RootTypeName
- RootTypeFieldName: string
- Field: Schema.SomeField
-}
+ ReturnMode<$Context['Config'], $Data, { [k in $Context['RootTypeFieldName']] : $Data }>
diff --git a/src/client/client.document.test.ts b/src/client/client.document.test.ts
index 857b7a22f..40abde398 100644
--- a/src/client/client.document.test.ts
+++ b/src/client/client.document.test.ts
@@ -52,6 +52,11 @@ test(`document with one mutation`, async () => {
await expect(run(undefined)).resolves.toEqual({ id: db.id1 })
})
+test(`error`, async () => {
+ const { run } = client.document({ foo: { query: { error: true } } })
+ await expect(run()).rejects.toMatchObject({ errors: [{ message: `Something went wrong.` }] })
+})
+
test(`document with one mutation and one query`, async () => {
const { run } = client.document({
foo: {
diff --git a/src/client/client.input.test-d.ts b/src/client/client.input.test-d.ts
new file mode 100644
index 000000000..8adbd0295
--- /dev/null
+++ b/src/client/client.input.test-d.ts
@@ -0,0 +1,22 @@
+import { test } from 'vitest'
+import { $Index } from '../../tests/_/schema/generated/SchemaRuntime.js'
+import { schema } from '../../tests/_/schema/schema.js'
+import { create } from './client.js'
+
+test(`works`, () => {
+ create({ schemaIndex: $Index, schema, name: `QueryOnly`, returnMode: `graphql` })
+ create({ schemaIndex: $Index, schema, name: `QueryOnly`, returnMode: `data` })
+ create({ schemaIndex: $Index, schema, name: `QueryOnly`, returnMode: `dataAndAllErrors` })
+ // @ts-expect-error bad returnMode
+ create({ schemaIndex: $Index, schema, name: `QueryOnly`, returnMode: `dataAndSchemaErrors` })
+
+ create({ schemaIndex: $Index, schema, name: `default`, returnMode: `graphql` })
+ create({ schemaIndex: $Index, schema, name: `default`, returnMode: `data` })
+ create({ schemaIndex: $Index, schema, name: `default`, returnMode: `dataAndAllErrors` })
+ create({ schemaIndex: $Index, schema, name: `default`, returnMode: `dataAndSchemaErrors` })
+
+ create({ schemaIndex: $Index, schema, returnMode: `graphql` })
+ create({ schemaIndex: $Index, schema, returnMode: `data` })
+ create({ schemaIndex: $Index, schema, returnMode: `dataAndAllErrors` })
+ create({ schemaIndex: $Index, schema, returnMode: `dataAndSchemaErrors` })
+})
diff --git a/src/client/client.returnMode.test-d.ts b/src/client/client.returnMode.test-d.ts
index 37aa8b0cb..8c97e1340 100644
--- a/src/client/client.returnMode.test-d.ts
+++ b/src/client/client.returnMode.test-d.ts
@@ -5,23 +5,24 @@ import { describe } from 'node:test'
import { expectTypeOf, test } from 'vitest'
import { $Index as schemaIndex } from '../../tests/_/schema/generated/SchemaRuntime.js'
import { schema } from '../../tests/_/schema/schema.js'
+import { GraphQLExecutionResultError } from '../lib/graphql.js'
import { create } from './client.js'
// dprint-ignore
-describe('default', () => {
+describe('default is data', () => {
const client = create({ schema, schemaIndex })
test(`document`, async () => {
expectTypeOf(client.document({ main: { query: { id: true } } }).run()).resolves.toEqualTypeOf<{ id: string | null }>()
})
- test(`raw`, async () => {
- expectTypeOf(client.raw('query main {\nid\n}', {}, 'main')).resolves.toEqualTypeOf()
- })
test('query field method', async () => {
await expectTypeOf(client.query.__typename()).resolves.toEqualTypeOf<'Query'>()
})
test('query $batch', async () => {
await expectTypeOf(client.query.$batch({ __typename: true, id: true })).resolves.toEqualTypeOf<{ __typename: 'Query', id: string|null }>()
})
+ test(`raw`, async () => {
+ expectTypeOf(client.raw('query main {\nid\n}', {}, 'main')).resolves.toEqualTypeOf()
+ })
})
// dprint-ignore
@@ -30,14 +31,42 @@ describe('data', () => {
test(`document`, async () => {
expectTypeOf(client.document({ main: { query: { id: true } } }).run()).resolves.toEqualTypeOf<{ id: string | null }>()
})
+ test('query field method', async () => {
+ await expectTypeOf(client.query.__typename()).resolves.toEqualTypeOf<'Query'>()
+ })
+ test('query $batch', async () => {
+ await expectTypeOf(client.query.$batch({ __typename: true, id: true })).resolves.toEqualTypeOf<{ __typename: 'Query', id: string|null }>()
+ })
+ test('result',async () => {
+ const x = await client.query.result({$: { case: 'Object1' }, onObject1:{id:true},onErrorOne:{infoId:true},onErrorTwo:{infoInt:true}})
+ await expectTypeOf(client.query.result({$: { case: 'Object1' }, onObject1:{id:true},onErrorOne:{infoId:true},onErrorTwo:{infoInt:true}})).resolves.toEqualTypeOf()
+ })
test(`raw`, async () => {
expectTypeOf(client.raw('query main {\nid\n}', {}, 'main')).resolves.toEqualTypeOf()
})
+})
+
+// dprint-ignore
+describe('dataAndAllErrors', () => {
+ const client = create({ schema, schemaIndex, returnMode: 'dataAndAllErrors' })
+ test(`document`, async () => {
+ expectTypeOf(client.document({ main: { query: { id: true } } }).run()).resolves.toEqualTypeOf<{ id: string | null } | GraphQLExecutionResultError>()
+ })
+ test(`document runOrThrow`, async () => {
+ expectTypeOf(client.document({ main: { query: { id: true } } }).runOrThrow()).resolves.toEqualTypeOf<{ id: string | null }>()
+ })
test('query field method', async () => {
- await expectTypeOf(client.query.__typename()).resolves.toEqualTypeOf<'Query'>()
+ await expectTypeOf(client.query.__typename()).resolves.toEqualTypeOf<'Query' | GraphQLExecutionResultError>()
})
test('query $batch', async () => {
- await expectTypeOf(client.query.$batch({ __typename: true, id: true })).resolves.toEqualTypeOf<{ __typename: 'Query', id: string|null }>()
+ await expectTypeOf(client.query.$batch({ __typename: true, id: true })).resolves.toEqualTypeOf<{ __typename: 'Query', id: string|null } | GraphQLExecutionResultError>()
+ })
+ test('result',async () => {
+ const x = await client.query.result({$: { case: 'Object1' }, onObject1:{id:true},onErrorOne:{infoId:true},onErrorTwo:{infoInt:true}})
+ await expectTypeOf(client.query.result({$: { case: 'Object1' }, onObject1:{id:true},onErrorOne:{infoId:true},onErrorTwo:{infoInt:true}})).resolves.toEqualTypeOf()
+ })
+ test(`raw`, async () => {
+ expectTypeOf(client.raw('query main {\nid\n}', {}, 'main')).resolves.toEqualTypeOf()
})
})
@@ -47,13 +76,22 @@ describe('graphql', () => {
test(`document`, async () => {
expectTypeOf(client.document({ main: { query: { id: true } } }).run()).resolves.toEqualTypeOf>>()
})
- test(`raw`, async () => {
- expectTypeOf(client.raw('query main {\nid\n}', {}, 'main')).resolves.toEqualTypeOf()
+ test(`document runOrThrow`, async () => {
+ expectTypeOf(client.document({ main: { query: { id: true } } }).runOrThrow()).resolves.toEqualTypeOf>>()
})
test('query field method', async () => {
await expectTypeOf(client.query.__typename()).resolves.toEqualTypeOf>()
})
+ test('query field methodOrThrow', async () => {
+ await expectTypeOf(client.query.__typenameOrThrow()).resolves.toEqualTypeOf>()
+ })
test('query $batch', async () => {
await expectTypeOf(client.query.$batch({ __typename: true, id: true })).resolves.toEqualTypeOf>()
})
+ test('query $batchOrThrow', async () => {
+ await expectTypeOf(client.query.$batchOrThrow({ __typename: true, id: true })).resolves.toEqualTypeOf>()
+ })
+ test(`raw`, async () => {
+ expectTypeOf(client.raw('query main {\nid\n}', {}, 'main')).resolves.toEqualTypeOf()
+ })
})
diff --git a/src/client/client.returnMode.test.ts b/src/client/client.returnMode.test.ts
index f7a45b6e2..34bc0d146 100644
--- a/src/client/client.returnMode.test.ts
+++ b/src/client/client.returnMode.test.ts
@@ -1,26 +1,43 @@
/* eslint-disable */
+import { GraphQLError } from 'graphql'
import { describe, expect, test } from 'vitest'
import { db } from '../../tests/_/db.js'
import { $Index as schemaIndex } from '../../tests/_/schema/generated/SchemaRuntime.js'
import { schema } from '../../tests/_/schema/schema.js'
+import { Errors } from '../lib/errors/__.js'
import { __typename } from '../Schema/_.js'
import { create } from './client.js'
// dprint-ignore
-describe('default', () => {
+describe('default (data)', () => {
const client = create({ schema, schemaIndex })
test(`document`, async () => {
await expect(client.document({ main: { query: { id: true } } }).run()).resolves.toEqual({ id: db.id })
})
+ test(`document runOrThrow`, async () => {
+ await expect(client.document({ main: { query: { id: true } } }).runOrThrow()).resolves.toEqual({ id: db.id })
+ })
+ test(`document runOrThrow error`, async () => {
+ await expect(client.document({ main: { query: { error: true } } }).runOrThrow()).rejects.toEqual(db.error)
+ })
test('raw', async () => {
await expect(client.raw('query main {\nid\n}', {}, 'main')).resolves.toEqual({ data: { id: db.id } })
})
test('query field method', async () => {
await expect(client.query.__typename()).resolves.toEqual('Query')
})
+ test('query field method error', async () => {
+ await expect(client.query.error()).rejects.toMatchObject(db.error)
+ })
+ test('query field method error orThrow', async () => {
+ await expect(client.query.errorOrThrow()).rejects.toMatchObject(db.error)
+ })
test('query $batch', async () => {
await expect(client.query.$batch({ __typename: true, id: true })).resolves.toEqual({ __typename: 'Query', id: db.id })
})
+ test('query $batchOrThrow error', async () => {
+ await expect(client.query.$batchOrThrow({ error: true })).rejects.toMatchObject(db.error)
+ })
test('mutation field method', async () => {
await expect(client.mutation.__typename()).resolves.toEqual('Mutation')
})
@@ -30,20 +47,35 @@ describe('default', () => {
})
// dprint-ignore
-describe('data', () => {
- const client = create({ schema, schemaIndex, returnMode: 'data' })
+describe('dataAndAllErrors', () => {
+ const client = create({ schema, schemaIndex, returnMode: 'dataAndAllErrors' })
test(`document`, async () => {
await expect(client.document({ main: { query: { id: true } } }).run()).resolves.toEqual({ id: db.id })
})
+ test(`document runOrThrow`, async () => {
+ await expect(client.document({ main: { query: { id: true } } }).runOrThrow()).resolves.toEqual({ id: db.id })
+ })
+ test(`document runOrThrow error`, async () => {
+ await expect(client.document({ main: { query: { error: true } } }).runOrThrow()).rejects.toEqual(db.error)
+ })
test('raw', async () => {
await expect(client.raw('query main {\nid\n}', {}, 'main')).resolves.toEqual({ data: { id: db.id } })
})
test('query field method', async () => {
await expect(client.query.__typename()).resolves.toEqual('Query')
})
+ test('query field method error', async () => {
+ await expect(client.query.error()).resolves.toMatchObject(db.error)
+ })
+ test('query field method error orThrow', async () => {
+ await expect(client.query.errorOrThrow()).rejects.toMatchObject(db.error)
+ })
test('query $batch', async () => {
await expect(client.query.$batch({ __typename: true, id: true })).resolves.toEqual({ __typename: 'Query', id: db.id })
})
+ test('query $batchOrThrow error', async () => {
+ await expect(client.query.$batchOrThrow({ error: true })).rejects.toMatchObject(db.error)
+ })
test('mutation field method', async () => {
await expect(client.mutation.__typename()).resolves.toEqual('Mutation')
})
@@ -58,15 +90,30 @@ describe('graphql', () => {
test(`document`, async () => {
await expect(client.document({ main: { query: { id: true } } }).run()).resolves.toEqual({ data: { id: db.id } }) // dprint-ignore
})
+ test(`document runOrThrow`, async () => {
+ await expect(client.document({ main: { query: { id: true } } }).runOrThrow()).resolves.toEqual({data:{ id: db.id }})
+ })
+ test(`document runOrThrow error`, async () => {
+ await expect(client.document({ main: { query: { error: true } } }).runOrThrow()).rejects.toEqual(db.error)
+ })
test('raw', async () => {
await expect(client.raw('query main {\nid\n}', {}, 'main')).resolves.toEqual({ data: { id: db.id } })
})
test('query field method', async () => {
await expect(client.query.__typename()).resolves.toEqual({ data: { __typename: 'Query' } })
})
+ test('query field method error', async () => {
+ await expect(client.query.error()).resolves.toMatchObject({ errors:db.error['errors'] })
+ })
+ test('query field method error orThrow', async () => {
+ await expect(client.query.errorOrThrow()).rejects.toMatchObject(db.error)
+ })
test('query $batch', async () => {
await expect(client.query.$batch({ __typename: true, id: true })).resolves.toEqual({ data: { __typename: 'Query', id: db.id } })
})
+ test('query $batchOrThrow error', async () => {
+ await expect(client.query.$batchOrThrow({ error: true })).rejects.toMatchObject(db.error)
+ })
test('mutation field method', async () => {
await expect(client.mutation.__typename()).resolves.toEqual({ data: { __typename: 'Mutation' } })
})
diff --git a/src/client/client.rootTypeMethods.test-d.ts b/src/client/client.rootTypeMethods.test-d.ts
index 5de71d9ee..417088f93 100644
--- a/src/client/client.rootTypeMethods.test-d.ts
+++ b/src/client/client.rootTypeMethods.test-d.ts
@@ -1,6 +1,7 @@
/* eslint-disable */
import { expectTypeOf, test } from 'vitest'
import * as Schema from '../../tests/_/schema/schema.js'
+import { GraphQLExecutionResultError } from '../lib/graphql.js'
import { create } from './client.js'
const client = create({ schema: Schema.schema, schemaIndex: Schema.$Index })
diff --git a/src/client/client.rootTypeMethods.test.ts b/src/client/client.rootTypeMethods.test.ts
index 5436dd3ec..5c8945fdb 100644
--- a/src/client/client.rootTypeMethods.test.ts
+++ b/src/client/client.rootTypeMethods.test.ts
@@ -5,6 +5,7 @@ import { create } from './client.js'
const client = create({ schema: Schema.schema, schemaIndex: Schema.$Index })
+// dprint-ignore
describe(`query`, () => {
test(`scalar`, async () => {
await expect(client.query.id()).resolves.toEqual(db.id1)
@@ -30,11 +31,33 @@ describe(`query`, () => {
await expect(client.query.interface({ id: true })).resolves.toEqual({ id: db.id })
})
test(`interface instance found`, async () => {
- await expect(client.query.interface({ onObject1ImplementingInterface: { int: true } })).resolves.toEqual({
- int: db.int,
- })
+ await expect(client.query.interface({ onObject1ImplementingInterface: { int: true } })).resolves.toEqual({ int: db.int })
})
test(`interface instance not found`, async () => {
await expect(client.query.interface({ onObject2ImplementingInterface: { boolean: true } })).resolves.toEqual({})
})
+ describe(`orThrow`, () => {
+ test(`without error`, async () => {
+ await expect(client.query.objectWithArgsOrThrow({ $: { id: `x` }, id: true })).resolves.toEqual({ id: `x` })
+ })
+ test(`with error`, async () => {
+ await expect(client.query.errorOrThrow()).rejects.toMatchObject(db.error)
+ })
+ })
+ describe(`$batch`, () => {
+ test(`success`, async () => {
+ await expect(client.query.$batch({ id: true })).resolves.toMatchObject({ id:db.id })
+ })
+ test(`error`, async () => {
+ await expect(client.query.$batch({ error: true })).rejects.toMatchObject(db.error)
+ })
+ describe(`orThrow`, () => {
+ test(`success`, async () => {
+ await expect(client.query.$batchOrThrow({ id: true })).resolves.toMatchObject({ id:db.id })
+ })
+ test(`error`, async () => {
+ await expect(client.query.$batchOrThrow({ error: true })).rejects.toMatchObject(db.error)
+ })
+ })
+ })
})
diff --git a/src/client/client.test.ts b/src/client/client.test.ts
deleted file mode 100644
index aec3922a7..000000000
--- a/src/client/client.test.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { expect, test } from 'vitest'
-import { $Index as schemaIndex } from '../../tests/_/schema/generated/SchemaRuntime.js'
-import { setupMockServer } from '../../tests/raw/__helpers.js'
-import { create } from './client.js'
-
-const ctx = setupMockServer()
-const data = { unionFooBar: { int: 1 } }
-
-const client = () => create({ schema: ctx.url, schemaIndex })
-
-test.todo(`query`, async () => {
- const mockRes = ctx.res({ body: { data } }).spec.body!
- expect(await client().query.$batch({ unionFooBar: { onBar: { int: true } } })).toEqual(mockRes.data)
-})
diff --git a/src/client/client.ts b/src/client/client.ts
index 75b1eeaa1..c02f2e85a 100644
--- a/src/client/client.ts
+++ b/src/client/client.ts
@@ -2,11 +2,13 @@ import type { ExecutionResult } from 'graphql'
import { type DocumentNode, execute, graphql, type GraphQLSchema } from 'graphql'
import type { ExcludeUndefined } from 'type-fest/source/required-deep.js'
import request from '../entrypoints/main.js'
+import type { GlobalRegistry } from '../globalRegistry.js'
+import { Errors } from '../lib/errors/__.js'
import type { RootTypeName, Variables } from '../lib/graphql.js'
import type { Object$2 } from '../Schema/__.js'
import { Schema } from '../Schema/__.js'
import { readMaybeThunk } from '../Schema/core/helpers.js'
-import type { ApplyInputDefaults, OptionsInputDefaults, ReturnModeType } from './Config.js'
+import type { ApplyInputDefaults, Config, ReturnModeTypeBase, ReturnModeTypeDataAndSchemaErrors } from './Config.js'
import * as CustomScalars from './customScalars.js'
import type { DocumentFn } from './document.js'
import { toDocumentExpression } from './document.js'
@@ -15,7 +17,7 @@ import { SelectionSet } from './SelectionSet/__.js'
import type { DocumentObject, GraphQLObjectSelection } from './SelectionSet/toGraphQLDocumentString.js'
// dprint-ignore
-export type Client<$Index extends Schema.Index, $Config extends OptionsInputDefaults> =
+export type Client<$Index extends Schema.Index, $Config extends Config> =
& {
// todo test raw
raw: (document: string | DocumentNode, variables?:Variables, operationName?:string) => Promise
@@ -28,37 +30,41 @@ interface HookInputDocumentEncode {
documentObject: GraphQLObjectSelection
}
-interface Input {
- /**
- * @defaultValue 'default'
- */
- name?: keyof NamedSchemas
- schema: URL | string | GraphQLSchema
- headers?: HeadersInit
- /**
- * Used internally for several functions.
- *
- * When custom scalars are being used, this runtime schema is used to
- * encode/decode them before/after your application sends/receives them.
- *
- * When using root type field methods, this runtime schema is used to assist how arguments on scalars versus objects
- * are constructed into the sent GraphQL document.
- */
- schemaIndex: Schema.Index
- returnMode?: ReturnModeType
- hooks?: {
- documentEncode: (
- input: HookInputDocumentEncode,
- fn: (input: HookInputDocumentEncode) => GraphQLObjectSelection,
- ) => GraphQLObjectSelection
+type InputForSchema<$Name extends GlobalRegistry.SchemaNames> = $Name extends any ? {
+ /**
+ * @defaultValue 'default'
+ */
+ name?: $Name
+ schema: URL | string | GraphQLSchema
+ headers?: HeadersInit
+ /**
+ * Used internally for several functions.
+ *
+ * When custom scalars are being used, this runtime schema is used to
+ * encode/decode them before/after your application sends/receives them.
+ *
+ * When using root type field methods, this runtime schema is used to assist how arguments on scalars versus objects
+ * are constructed into the sent GraphQL document.
+ */
+ schemaIndex: Schema.Index
+ returnMode?:
+ | ReturnModeTypeBase
+ | (GlobalRegistry.HasSchemaErrors<$Name> extends true ? ReturnModeTypeDataAndSchemaErrors : never)
+ hooks?: {
+ documentEncode: (
+ input: HookInputDocumentEncode,
+ fn: (input: HookInputDocumentEncode) => GraphQLObjectSelection,
+ ) => GraphQLObjectSelection
+ }
}
-}
+ : never
+
+type Input = InputForSchema
export const create = <$Input extends Input>(
input: $Input,
): Client<
- // @ts-expect-error fixme
- (undefined extends $Input['name'] ? NamedSchemas['default']['index'] : NamedSchemas[$Input['name']]['index']),
+ GlobalRegistry.GetSchemaIndexOptionally<$Input['name']>,
ApplyInputDefaults<{ returnMode: $Input['returnMode'] }>
> => {
const parentInput = input
@@ -127,8 +133,7 @@ export const create = <$Input extends Input>(
const documentString = SelectionSet.selectionSet(documentObjectEncoded)
// todo variables
const result = await executeDocumentExpression({ document: documentString })
- if (result.errors && (result.errors.length > 0)) throw new AggregateError(result.errors)
- // todo check for errors
+ // if (result.errors && (result.errors.length > 0)) throw new AggregateError(result.errors)
const dataDecoded = CustomScalars.decode(rootIndex, result.data)
return { ...result, data: dataDecoded }
}
@@ -159,16 +164,24 @@ export const create = <$Input extends Input>(
} as GraphQLObjectSelection
const result = await rootObjectExecutors[rootTypeName](documentObject)
const resultHandled = handleReturn(result)
+ if (resultHandled instanceof Error) return resultHandled
// @ts-expect-error make this type safe?
- return returnMode === `data` ? resultHandled[key] : resultHandled
+ return returnMode === `data` || returnMode === `dataAndAllErrors` ? resultHandled[key] : resultHandled
}
}
const handleReturn = (result: ExecutionResult) => {
switch (returnMode) {
+ case `dataAndAllErrors`:
case `data`: {
if (result.errors && result.errors.length > 0) {
- throw new AggregateError(result.errors)
+ const error = new Errors.ContextualAggregateError(
+ `One or more errors in the execution result.`,
+ {},
+ result.errors,
+ )
+ if (returnMode === `data`) throw error
+ return error
}
return result.data
}
@@ -188,15 +201,46 @@ export const create = <$Input extends Input>(
new Proxy({}, {
get: (_, key) => {
if (typeof key === `symbol`) throw new Error(`Symbols not supported.`)
- if (key === `$batch`) {
+
+ // todo We need to document that in order for this to 100% work none of the user's root type fields can end with "OrThrow".
+ const isOrThrow = key.endsWith(`OrThrow`)
+
+ if (key.startsWith(`$batch`)) {
return async (selectionSetOrIndicator: GraphQLObjectSelection) => {
- const result = await rootObjectExecutors[rootTypeName]({
+ const resultRaw = await rootObjectExecutors[rootTypeName]({
[rootTypeNameToOperationName[rootTypeName]]: selectionSetOrIndicator,
})
- return handleReturn(result)
+ const result = handleReturn(resultRaw)
+ if (isOrThrow && result instanceof Error) throw result
+ // todo consolidate
+ // @ts-expect-error fixme
+ if (isOrThrow && returnMode === `graphql` && result.errors && result.errors.length > 0) {
+ throw new Errors.ContextualAggregateError(
+ `One or more errors in the execution result.`,
+ {},
+ // @ts-expect-error fixme
+ result.errors,
+ )
+ }
+ return result
}
} else {
- return executeRootTypeFieldSelection(rootTypeName, key)
+ const fieldName = isOrThrow ? key.slice(0, -7) : key
+ return async (argsOrSelectionSet?: object) => {
+ const result = await executeRootTypeFieldSelection(rootTypeName, fieldName)(argsOrSelectionSet) // eslint-disable-line
+ if (isOrThrow && result instanceof Error) throw result
+ // todo consolidate
+ // eslint-disable-next-line
+ if (isOrThrow && returnMode === `graphql` && result.errors.length > 0) {
+ throw new Errors.ContextualAggregateError(
+ `One or more errors in the execution result.`,
+ {},
+ // eslint-disable-next-line
+ result.errors,
+ )
+ }
+ return result
+ }
}
},
})
@@ -207,16 +251,27 @@ export const create = <$Input extends Input>(
return await executeDocumentExpression({ document, variables, operationName })
},
document: (documentObject: DocumentObject) => {
+ const run = async (operationName: string) => {
+ // todo this does not support custom scalars
+ const documentExpression = toDocumentExpression(documentObject)
+ const result = await executeDocumentExpression({
+ document: documentExpression,
+ operationName,
+ // todo variables
+ })
+ return handleReturn(result)
+ }
return {
- run: async (operationName: string) => {
- // todo this does not support custom scalars
- const documentExpression = toDocumentExpression(documentObject)
- const result = await executeDocumentExpression({
- document: documentExpression,
- operationName,
- // todo variables
- })
- return handleReturn(result)
+ run,
+ runOrThrow: async (operationName: string) => {
+ const result = await run(operationName)
+ if (result instanceof Error) throw result
+ // @ts-expect-error fixme
+ if (returnMode === `graphql` && result.errors && result.errors.length > 0) {
+ // @ts-expect-error fixme
+ throw new Errors.ContextualAggregateError(`One or more errors in the execution result.`, {}, result.errors)
+ }
+ return result
},
}
},
diff --git a/src/client/document.ts b/src/client/document.ts
index 7a2ed9eb5..d73df0c4d 100644
--- a/src/client/document.ts
+++ b/src/client/document.ts
@@ -2,7 +2,7 @@ import type { MergeExclusive, NonEmptyObject } from 'type-fest'
import type { IsMultipleKeys } from '../lib/prelude.js'
import type { TSError } from '../lib/TSError.js'
import type { Schema } from '../Schema/__.js'
-import type { Config, ReturnMode } from './Config.js'
+import type { Config, OrThrowifyConfig, ReturnMode } from './Config.js'
import type { ResultSet } from './ResultSet/__.js'
import { SelectionSet } from './SelectionSet/__.js'
import type { DocumentObject } from './SelectionSet/toGraphQLDocumentString.js'
@@ -17,6 +17,12 @@ export type DocumentFn<$Config extends Config, $Index extends Schema.Index> =
>(...params: $Params) => Promise<
ReturnMode<$Config, ResultSet.Root, $Index, 'Query'>>
>
+ runOrThrow: <
+ $Name extends keyof $Document & string,
+ $Params extends (IsMultipleKeys<$Document> extends true ? [name: $Name] : ([] | [name: $Name | undefined])),
+ >(...params: $Params) => Promise<
+ ReturnMode, ResultSet.Root, $Index, 'Query'>>
+ >
}
export const toDocumentExpression = (
diff --git a/src/generator/__snapshots__/files.test.ts.snap b/src/generator/__snapshots__/files.test.ts.snap
index f1e466b7d..204520e6c 100644
--- a/src/generator/__snapshots__/files.test.ts.snap
+++ b/src/generator/__snapshots__/files.test.ts.snap
@@ -249,6 +249,12 @@ export namespace Root {
dateNonNull: $.Field<$Scalar.Date, null>
dateObject1: $.Field<$.Output.Nullable, null>
dateUnion: $.Field<$.Output.Nullable, null>
+ error: $.Field<
+ $.Output.Nullable<$Scalar.String>,
+ $.Args<{
+ case: $.Input.Nullable<$Scalar.String>
+ }>
+ >
id: $.Field<$.Output.Nullable<$Scalar.ID>, null>
idNonNull: $.Field<$Scalar.ID, null>
interface: $.Field<$.Output.Nullable, null>
@@ -651,6 +657,7 @@ export const Query = $.Object$(\`Query\`, {
dateNonNull: $.field($Scalar.Date),
dateObject1: $.field($.Output.Nullable(() => DateObject1)),
dateUnion: $.field($.Output.Nullable(() => DateUnion)),
+ error: $.field($.Output.Nullable($Scalar.String), $.Args({ case: $.Input.Nullable($Scalar.String) })),
id: $.field($.Output.Nullable($Scalar.ID)),
idNonNull: $.field($Scalar.ID),
interface: $.field($.Output.Nullable(() => Interface)),
diff --git a/src/generator/code/global.ts b/src/generator/code/global.ts
index 184d425c6..03d47e866 100644
--- a/src/generator/code/global.ts
+++ b/src/generator/code/global.ts
@@ -35,6 +35,9 @@ export const generateGlobal = (config: Config) => {
}).join(`\n`)
}
}
+ featureOptions: {
+ schemaErrors: ${config.options.errorTypeNamePattern ? `true` : `false`}
+ }
}
}
}
diff --git a/src/global.ts b/src/global.ts
deleted file mode 100644
index 2157491b1..000000000
--- a/src/global.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-declare global {
- // todo some kind of distinct prefix
- interface NamedSchemas {}
-}
diff --git a/src/globalRegistry.ts b/src/globalRegistry.ts
new file mode 100644
index 000000000..914e5ad58
--- /dev/null
+++ b/src/globalRegistry.ts
@@ -0,0 +1,41 @@
+import type { TSError } from './lib/TSError.js'
+import type { Schema } from './Schema/__.js'
+
+declare global {
+ // todo some kind of distinct prefix
+ interface NamedSchemas {}
+}
+
+export type GlobalRegistry = Record
+ featureOptions: {
+ schemaErrors: boolean
+ }
+}>
+
+export namespace GlobalRegistry {
+ export type Schemas = NamedSchemas
+
+ export type DefaultSchemaName = 'default'
+
+ export type SchemaNames = keyof NamedSchemas extends never
+ ? TSError<'SchemaNames', 'No schemas have been registered. Did you run graphql-request generate?'>
+ : keyof NamedSchemas
+
+ // todo use conditional types?
+ // eslint-disable-next-line
+ // @ts-ignore populated after generation
+ export type HasSchemaErrors<$Name extends SchemaNames> = NamedSchemas[$Name]['featureOptions']['schemaErrors']
+
+ // todo use conditional types?
+ // eslint-disable-next-line
+ // @ts-ignore populated after generation
+ export type GetSchemaIndexOptionally<$Name extends SchemaNames | undefined> = $Name extends SchemaNames
+ // eslint-disable-next-line
+ // @ts-ignore populated after generation
+ ? NamedSchemas[$Name]['index']
+ // eslint-disable-next-line
+ // @ts-ignore populated after generation
+ : NamedSchemas['default']['index']
+}
diff --git a/src/lib/errors/ContextualAggregateError.ts b/src/lib/errors/ContextualAggregateError.ts
new file mode 100644
index 000000000..e450727a1
--- /dev/null
+++ b/src/lib/errors/ContextualAggregateError.ts
@@ -0,0 +1,26 @@
+import { ContextualError } from './ContextualError.js'
+import type { Cause } from './types.js'
+
+/**
+ * Aggregation Error enhanced with a context object and types members.
+ *
+ * The library also exports a serializer you can use.
+ */
+export class ContextualAggregateError<
+ $Errors extends Error | ContextualError = ContextualError<
+ string,
+ object,
+ Cause | undefined
+ >,
+ $Name extends string = `ContextualAggregateError`,
+ $Context extends object = object,
+> extends ContextualError<$Name, $Context> {
+ override name: $Name = `ContextualAggregateError` as $Name
+ constructor(
+ message: string,
+ context: $Context,
+ public readonly errors: readonly $Errors[],
+ ) {
+ super(message, context, undefined)
+ }
+}
diff --git a/src/lib/errors/ContextualError.ts b/src/lib/errors/ContextualError.ts
new file mode 100644
index 000000000..48896f7a9
--- /dev/null
+++ b/src/lib/errors/ContextualError.ts
@@ -0,0 +1,23 @@
+import type { Cause, Context } from './types.js'
+
+/**
+ * Error enhanced with a context object.
+ *
+ * The library also exports a serializer you can use.
+ */
+export class ContextualError<
+ $Name extends string = string,
+ $Context extends Context = object,
+ $Cause extends Cause | undefined = undefined,
+> extends Error {
+ override name: $Name = `ContextualError` as $Name
+ constructor(
+ message: string,
+ public readonly context: $Context = {} as $Context,
+ public override readonly cause: $Cause = undefined as $Cause,
+ ) {
+ super(message, cause)
+ }
+}
+
+export type SomeContextualError = ContextualError
diff --git a/src/lib/errors/ErrorInternal.ts b/src/lib/errors/ErrorInternal.ts
new file mode 100644
index 000000000..aa881649a
--- /dev/null
+++ b/src/lib/errors/ErrorInternal.ts
@@ -0,0 +1,17 @@
+import { ContextualError } from './ContextualError.js'
+import type { Cause, Context } from './types.js'
+
+export class ErrorInternal<
+ $Name extends string = 'ErrorInternal',
+ $Context extends Context = Context,
+ $Cause extends Cause | undefined = undefined,
+> extends ContextualError<$Name, $Context, $Cause> {
+ override name: $Name = `ErrorInternal` as $Name
+ constructor(
+ message: string = `Something went wrong.`,
+ context: $Context = {} as $Context,
+ cause: $Cause = undefined as $Cause,
+ ) {
+ super(message, context, cause)
+ }
+}
diff --git a/src/lib/errors/_.ts b/src/lib/errors/_.ts
new file mode 100644
index 000000000..fbf44e545
--- /dev/null
+++ b/src/lib/errors/_.ts
@@ -0,0 +1,4 @@
+export * from './ContextualAggregateError.js'
+export * from './ContextualError.js'
+export * from './ErrorInternal.js'
+export * from './types.js'
diff --git a/src/lib/errors/__.ts b/src/lib/errors/__.ts
new file mode 100644
index 000000000..acbdaf5ea
--- /dev/null
+++ b/src/lib/errors/__.ts
@@ -0,0 +1 @@
+export * as Errors from './_.js'
diff --git a/src/lib/errors/types.ts b/src/lib/errors/types.ts
new file mode 100644
index 000000000..3249d86c4
--- /dev/null
+++ b/src/lib/errors/types.ts
@@ -0,0 +1,4 @@
+import type { ContextualError } from './ContextualError.js'
+
+export type Cause = Error | ContextualError
+export type Context = object
diff --git a/src/lib/graphql.ts b/src/lib/graphql.ts
index 2712e9f03..d3fd2dd33 100644
--- a/src/lib/graphql.ts
+++ b/src/lib/graphql.ts
@@ -1,4 +1,4 @@
-import type { GraphQLEnumValue, GraphQLField, GraphQLInputField, GraphQLSchema } from 'graphql'
+import type { GraphQLEnumValue, GraphQLError, GraphQLField, GraphQLInputField, GraphQLSchema } from 'graphql'
import {
GraphQLEnumType,
GraphQLInputObjectType,
@@ -11,6 +11,7 @@ import {
isListType,
isNonNullType,
} from 'graphql'
+import type { Errors } from './errors/__.js'
export type TypeMapByKind =
& {
@@ -230,3 +231,7 @@ export const hasSubscription = (typeMapByKind: TypeMapByKind) =>
typeMapByKind.GraphQLRootType.find((_) => _.name === `Subscription`)
export type Variables = Record // todo or any custom scalars too
+
+export type GraphQLExecutionResultError = Errors.ContextualAggregateError
+
+export type OperationName = 'query' | 'mutation'
diff --git a/src/lib/prelude.ts b/src/lib/prelude.ts
index 82fbac380..4edb5648f 100644
--- a/src/lib/prelude.ts
+++ b/src/lib/prelude.ts
@@ -185,6 +185,7 @@ import type { ConditionalSimplifyDeep } from 'type-fest/source/conditional-simpl
export type SimplifyDeep = ConditionalSimplifyDeep | Date, object>
import fs from 'node:fs/promises'
+import { $ } from 'vitest/dist/reporters-LqC_WI4d.js'
export const fileExists = async (path: string) => {
return Boolean(
@@ -229,3 +230,7 @@ export const mapValues = <
}),
) as Record>
}
+
+export type SetProperty<$Obj extends object, $Prop extends keyof $Obj, $Type extends $Obj[$Prop]> =
+ & Omit<$Obj, $Prop>
+ & { [_ in $Prop]: $Type }
diff --git a/tests/_/db.ts b/tests/_/db.ts
index 225f071ee..753509628 100644
--- a/tests/_/db.ts
+++ b/tests/_/db.ts
@@ -1,5 +1,13 @@
+import { GraphQLError } from 'graphql'
+import { Errors } from '../../src/lib/errors/__.js'
+
const date0 = new Date(0)
+// const error = { errors: [{ message: `Something went wrong.` }] }
+const error = new Errors.ContextualAggregateError(`One or more errors in the execution result.`, {}, [
+ new GraphQLError(`Something went wrong.`),
+])
+
const id = `abc`
const int = 123
@@ -54,4 +62,5 @@ export const db = {
DateInterface1: {
date1: date0,
},
+ error,
} as const
diff --git a/tests/_/schema/generated/Global.ts b/tests/_/schema/generated/Global.ts
index d41a5ff19..22c3c4790 100644
--- a/tests/_/schema/generated/Global.ts
+++ b/tests/_/schema/generated/Global.ts
@@ -9,6 +9,9 @@ declare global {
customScalars: {
Date: CustomScalar.Date
}
+ featureOptions: {
+ schemaErrors: true
+ }
}
}
}
diff --git a/tests/_/schema/generated/SchemaBuildtime.ts b/tests/_/schema/generated/SchemaBuildtime.ts
index f4bda1e7e..602b21210 100644
--- a/tests/_/schema/generated/SchemaBuildtime.ts
+++ b/tests/_/schema/generated/SchemaBuildtime.ts
@@ -71,6 +71,12 @@ export namespace Root {
dateNonNull: $.Field<$Scalar.Date, null>
dateObject1: $.Field<$.Output.Nullable, null>
dateUnion: $.Field<$.Output.Nullable, null>
+ error: $.Field<
+ $.Output.Nullable<$Scalar.String>,
+ $.Args<{
+ case: $.Input.Nullable<$Scalar.String>
+ }>
+ >
id: $.Field<$.Output.Nullable<$Scalar.ID>, null>
idNonNull: $.Field<$Scalar.ID, null>
interface: $.Field<$.Output.Nullable, null>
diff --git a/tests/_/schema/generated/SchemaRuntime.ts b/tests/_/schema/generated/SchemaRuntime.ts
index 55f64efb2..f1fcc01ca 100644
--- a/tests/_/schema/generated/SchemaRuntime.ts
+++ b/tests/_/schema/generated/SchemaRuntime.ts
@@ -143,6 +143,7 @@ export const Query = $.Object$(`Query`, {
dateNonNull: $.field($Scalar.Date),
dateObject1: $.field($.Output.Nullable(() => DateObject1)),
dateUnion: $.field($.Output.Nullable(() => DateUnion)),
+ error: $.field($.Output.Nullable($Scalar.String), $.Args({ case: $.Input.Nullable($Scalar.String) })),
id: $.field($.Output.Nullable($Scalar.ID)),
idNonNull: $.field($Scalar.ID),
interface: $.field($.Output.Nullable(() => Interface)),
diff --git a/tests/_/schema/schema.graphql b/tests/_/schema/schema.graphql
index a6856912b..31b125d7d 100644
--- a/tests/_/schema/schema.graphql
+++ b/tests/_/schema/schema.graphql
@@ -129,6 +129,7 @@ type Query {
dateNonNull: Date!
dateObject1: DateObject1
dateUnion: DateUnion
+ error(case: String): String
id: ID
idNonNull: ID!
interface: Interface
diff --git a/tests/_/schema/schema.ts b/tests/_/schema/schema.ts
index 52bc65469..ef831eb61 100644
--- a/tests/_/schema/schema.ts
+++ b/tests/_/schema/schema.ts
@@ -211,6 +211,13 @@ const ObjectUnion = builder.simpleObject(`ObjectUnion`, {
builder.queryType({
fields: t => ({
+ // error
+ error: t.string({
+ args: { case: t.arg.string({ required: false }) },
+ resolve: () => {
+ throw new Error(`Something went wrong.`)
+ },
+ }),
// Custom Scalar
date: t.field({ type: `Date`, resolve: () => db.date0 }),
dateNonNull: t.field({ nullable: false, type: `Date`, resolve: () => db.date0 }),
diff --git a/tests/_/schemaMutationOnly/generated/Global.ts b/tests/_/schemaMutationOnly/generated/Global.ts
index 057e83abb..2413e61cf 100644
--- a/tests/_/schemaMutationOnly/generated/Global.ts
+++ b/tests/_/schemaMutationOnly/generated/Global.ts
@@ -5,6 +5,9 @@ declare global {
MutationOnly: {
index: Index
customScalars: {}
+ featureOptions: {
+ schemaErrors: false
+ }
}
}
}
diff --git a/tests/_/schemaQueryOnly/generated/Global.ts b/tests/_/schemaQueryOnly/generated/Global.ts
index 3c68df87b..ab6b13dae 100644
--- a/tests/_/schemaQueryOnly/generated/Global.ts
+++ b/tests/_/schemaQueryOnly/generated/Global.ts
@@ -5,6 +5,9 @@ declare global {
QueryOnly: {
index: Index
customScalars: {}
+ featureOptions: {
+ schemaErrors: false
+ }
}
}
}
diff --git a/tsconfig.json b/tsconfig.json
index aa07d718c..ebf73a07b 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,7 +5,6 @@
],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2023"],
-
// Use ESM
"module": "NodeNext",
"moduleResolution": "nodenext",