Skip to content

Commit

Permalink
feat(ts-client): root type field methods (#779)
Browse files Browse the repository at this point in the history
* feat(ts-client): root type field methods

* fix runtime

* scalar test case

* more

* work

* work

* fix

* more cases

* interface

* work

* update snap
  • Loading branch information
jasonkuhrt committed Apr 18, 2024
1 parent 5b85f61 commit 45412c2
Show file tree
Hide file tree
Showing 30 changed files with 811 additions and 127 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@
},
"devDependencies": {
"@pothos/core": "^3.41.0",
"@pothos/plugin-simple-objects": "^3.7.0",
"@tsconfig/node16": "^16.1.3",
"@types/body-parser": "^1.19.5",
"@types/express": "^4.17.21",
Expand All @@ -113,6 +114,7 @@
"express": "^4.19.2",
"get-port": "^7.1.0",
"graphql": "^16.8.1",
"graphql-scalars": "^1.23.0",
"graphql-tag": "^2.12.6",
"happy-dom": "^14.7.1",
"json-bigint": "^1.0.0",
Expand Down
28 changes: 28 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions src/Schema/Output/Output.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { TSError } from '../../lib/TSError.js'
import { readMaybeThunk } from '../core/helpers.js'
import type { Any, Named } from './typeGroups.js'
import type { __typename } from './types/__typename.js'
import type { List } from './types/List.js'
Expand Down Expand Up @@ -31,7 +32,7 @@ export const unwrapNullable = <$Type extends Any>(type: $Type): UnwrapNullable<$
return type as UnwrapNullable<$Type>
}

export const unwrap = <$Type extends Any>(type: $Type): Unwrap<$Type> => {
export const unwrapToNamed = <$Type extends Any>(type: $Type): Unwrap<$Type> => {
// @ts-expect-error fixme
return type.kind === `named` ? type.type : unwrap(type.type)
return type.kind === `list` || type.kind === `nullable` ? unwrapToNamed(readMaybeThunk(type).type) : type
}
18 changes: 8 additions & 10 deletions src/client/ResultSet/ResultSet.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,12 @@
/* eslint-disable @typescript-eslint/ban-types */

import type { Simplify } from 'type-fest'
import type { GetKeyOr, SimplifyDeep } from '../../lib/prelude.js'
import type { ExcludeNull, GetKeyOr, SimplifyDeep } from '../../lib/prelude.js'
import type { TSError } from '../../lib/TSError.js'
import type { Schema, SomeField } from '../../Schema/__.js'
import type { PickScalarFields } from '../../Schema/Output/Output.js'
import type { SelectionSet } from '../SelectionSet/__.js'

type ExcludeNull<T> = Exclude<T, null>

export type Root<
$SelectionSet extends object,
$Index extends Schema.Index,
$RootTypeName extends Schema.RootTypeName,
> = SimplifyDeep<Object$<$SelectionSet, ExcludeNull<$Index['Root'][$RootTypeName]>, $Index>>

export type Query<$SelectionSet extends object, $Index extends Schema.Index> = Root<$SelectionSet, $Index, 'Query'>

// dprint-ignore
Expand All @@ -23,6 +15,12 @@ export type Mutation<$SelectionSet extends object, $Index extends Schema.Index>
// dprint-ignore
export type Subscription<$SelectionSet extends object, $Index extends Schema.Index> = Root<$SelectionSet, $Index, 'Subscription'>

export type Root<
$SelectionSet extends object,
$Index extends Schema.Index,
$RootTypeName extends Schema.RootTypeName,
> = SimplifyDeep<Object$<$SelectionSet, ExcludeNull<$Index['Root'][$RootTypeName]>, $Index>>

// dprint-ignore
export type Object$<$SelectionSet, $Node extends Schema.Output.Object$2, $Index extends Schema.Index> =
SelectionSet.IsSelectScalarsWildcard<$SelectionSet> extends true
Expand Down Expand Up @@ -63,7 +61,7 @@ type OnTypeFragment<$SelectionSet, $Node extends Schema.Output.Object$2, $Index
: never

// dprint-ignore
type Field<$SelectionSet, $Field extends SomeField, $Index extends Schema.Index> =
export type Field<$SelectionSet, $Field extends SomeField, $Index extends Schema.Index> =
$SelectionSet extends SelectionSet.Directive.Include.Negative | SelectionSet.Directive.Skip.Positive ?
null :
(
Expand Down
22 changes: 17 additions & 5 deletions src/client/SelectionSet/SelectionSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,22 +64,34 @@ type Fields<$Fields extends SomeFields, $Index extends Schema.Index> =

export type IsSelectScalarsWildcard<SS> = SS extends { $scalars: ClientIndicatorPositive } ? true : false

type FieldOptions = {
/**
* When using root type field methods there is no point in directives since there can be
* no no peer fields with those function that by design target sending one root type field.
*/
hideDirectives?: boolean
}

type FieldOptionsDefault = { hideDirectives: false }

// dprint-ignore
export type Field<$Field extends SomeField, $Index extends Schema.Index> = Field_<$Field['type'], $Field, $Index>
export type Field<$Field extends SomeField, $Index extends Schema.Index, $Options extends FieldOptions = FieldOptionsDefault> =
Field_<$Field['type'], $Field, $Index, $Options>

// dprint-ignore
export type Field_<
$type extends Schema.Output.Any,
$Field extends SomeField,
$Index extends Schema.Index,
$Options extends FieldOptions
> =
$type extends Schema.Output.Nullable<infer $typeInner> ? Field_<$typeInner, $Field, $Index> :
$type extends Schema.Output.List<infer $typeInner> ? Field_<$typeInner, $Field, $Index> :
$type extends Schema.Output.Nullable<infer $typeInner> ? Field_<$typeInner, $Field, $Index, $Options> :
$type extends Schema.Output.List<infer $typeInner> ? Field_<$typeInner, $Field, $Index, $Options> :
$type extends Schema.__typename ? NoArgsIndicator :
$type extends Schema.Scalar.Any ? Indicator<$Field> :
$type extends Schema.Enum ? Indicator<$Field> :
$type extends Schema.Object$2 ? Object<$type, $Index> & FieldDirectives & Arguments<$Field> :
$type extends Schema.Union ? Union<$type, $Index> :
$type extends Schema.Object$2 ? Object<$type, $Index> & ($Options['hideDirectives'] extends true ? {} : FieldDirectives) & Arguments<$Field> :
$type extends Schema.Union ? Union<$type, $Index> :
$type extends Schema.Interface ? Interface<$type, $Index> :
TSError<'Field', '$Field case not handled', { $Field: $Field }>
// dprint-ignore
Expand Down
70 changes: 34 additions & 36 deletions src/client/client.customScalar.test.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,68 @@
/* eslint-disable */
import { beforeEach, describe, expect, test } from 'vitest'
import { db } from '../../tests/_/db.js'
import { setupMockServer } from '../../tests/raw/__helpers.js'
import type { Index } from '../../tests/ts/_/schema/generated/Index.js'
import { $Index as schemaIndex } from '../../tests/ts/_/schema/generated/SchemaRuntime.js'
import { create } from './client.js'

const ctx = setupMockServer()
const data = { fooBarUnion: { int: 1 } }
const date0Encoded = db.date0.toISOString()
const date1Encoded = db.date1.toISOString()

const client = () => create<Index>({ schema: ctx.url, schemaIndex })

describe(`output`, () => {
test(`query field`, async () => {
ctx.res({ body: { data: { date: 0 } } })
expect(await client().query.$batch({ date: true })).toEqual({ date: new Date(0) })
ctx.res({ body: { data: { date: date0Encoded } } })
expect(await client().query.$batch({ date: true })).toEqual({ date: db.date0 })
})
test(`query field in non-null`, async () => {
ctx.res({ body: { data: { dateNonNull: 0 } } })
expect(await client().query.$batch({ dateNonNull: true })).toEqual({ dateNonNull: new Date(0) })
ctx.res({ body: { data: { dateNonNull: date0Encoded } } })
expect(await client().query.$batch({ dateNonNull: true })).toEqual({ dateNonNull: db.date0 })
})
test(`query field in list`, async () => {
ctx.res({ body: { data: { dateList: [0, 1] } } })
expect(await client().query.$batch({ dateList: true })).toEqual({ dateList: [new Date(0), new Date(1)] })
expect(await client().query.$batch({ dateList: true })).toEqual({ dateList: [db.date0, new Date(1)] })
})
test(`query field in list non-null`, async () => {
ctx.res({ body: { data: { dateList: [0, 1] } } })
expect(await client().query.$batch({ dateList: true })).toEqual({ dateList: [new Date(0), new Date(1)] })
expect(await client().query.$batch({ dateList: true })).toEqual({ dateList: [db.date0, new Date(1)] })
})
test(`object field`, async () => {
ctx.res({ body: { data: { dateObject1: { date1: 0 } } } })
expect(await client().query.$batch({ dateObject1: { date1: true } })).toEqual({
dateObject1: { date1: new Date(0) },
dateObject1: { date1: db.date0 },
})
})
test(`object field in interface`, async () => {
ctx.res({ body: { data: { dateInterface1: { date1: 0 } } } })
expect(await client().query.$batch({ dateInterface1: { date1: true } })).toEqual({
dateInterface1: { date1: new Date(0) },
dateInterface1: { date1: db.date0 },
})
})
describe(`object field in union`, () => {
test(`case 1 with __typename`, async () => {
ctx.res({ body: { data: { dateUnion: { __typename: `DateObject1`, date1: 0 } } } })
expect(await client().query.$batch({ dateUnion: { __typename: true, onDateObject1: { date1: true } } }))
.toEqual({
dateUnion: { __typename: `DateObject1`, date1: new Date(0) },
dateUnion: { __typename: `DateObject1`, date1: db.date0 },
})
})
test(`case 1 without __typename`, async () => {
ctx.res({ body: { data: { dateUnion: { date1: 0 } } } })
ctx.res({ body: { data: { dateUnion: { date1: date0Encoded } } } })
expect(await client().query.$batch({ dateUnion: { onDateObject1: { date1: true } } })).toEqual({
dateUnion: { date1: new Date(0) },
dateUnion: { date1: db.date0 },
})
})
test(`case 2`, async () => {
ctx.res({ body: { data: { dateUnion: { date2: 0 } } } })
ctx.res({ body: { data: { dateUnion: { date2: date0Encoded } } } })
expect(
await client().query.$batch({
dateUnion: { onDateObject1: { date1: true }, onDateObject2: { date2: true } },
}),
)
.toEqual({ dateUnion: { date2: new Date(0) } })
.toEqual({ dateUnion: { date2: db.date0 } })
})
test(`case 2 miss`, async () => {
ctx.res({ body: { data: { dateUnion: null } } })
Expand Down Expand Up @@ -92,56 +94,52 @@ describe(`input`, () => {
}

test(`arg field`, async () => {
const client = clientExpected((doc) => expect(doc.dateArg.$.date).toEqual(new Date(0).getTime()))
await client.query.$batch({ dateArg: { $: { date: new Date(0) } } })
const client = clientExpected((doc) => expect(doc.dateArg.$.date).toEqual(date0Encoded))
await client.query.$batch({ dateArg: { $: { date: db.date0 } } })
})
test('arg field in non-null', async () => {
const client = clientExpected((doc) => expect(doc.dateArgNonNull.$.date).toEqual(new Date(0).getTime()))
await client.query.$batch({ dateArgNonNull: { $: { date: new Date(0) } } })
const client = clientExpected((doc) => expect(doc.dateArgNonNull.$.date).toEqual(date0Encoded))
await client.query.$batch({ dateArgNonNull: { $: { date: db.date0 } } })
})
test('arg field in list', async () => {
const client = clientExpected((doc) =>
expect(doc.dateArgList.$.date).toEqual([new Date(0).getTime(), new Date(1).getTime()])
)
await client.query.$batch({ dateArgList: { $: { date: [new Date(0), new Date(1)] } } })
const client = clientExpected((doc) => expect(doc.dateArgList.$.date).toEqual([date0Encoded, date1Encoded]))
await client.query.$batch({ dateArgList: { $: { date: [db.date0, new Date(1)] } } })
})
test('arg field in list (null)', async () => {
const client = clientExpected((doc) => expect(doc.dateArgList.$.date).toEqual(null))
await client.query.$batch({ dateArgList: { $: { date: null } } })
})
test('arg field in non-null list (with list)', async () => {
const client = clientExpected((doc) =>
expect(doc.dateArgNonNullList.$.date).toEqual([new Date(0).getTime(), new Date(1).getTime()])
)
await client.query.$batch({ dateArgNonNullList: { $: { date: [new Date(0), new Date(1)] } } })
const client = clientExpected((doc) => expect(doc.dateArgNonNullList.$.date).toEqual([date0Encoded, date1Encoded]))
await client.query.$batch({ dateArgNonNullList: { $: { date: [db.date0, new Date(1)] } } })
})
test('arg field in non-null list (with null)', async () => {
const client = clientExpected((doc) => expect(doc.dateArgNonNullList.$.date).toEqual([null, new Date(0).getTime()]))
await client.query.$batch({ dateArgNonNullList: { $: { date: [null, new Date(0)] } } })
const client = clientExpected((doc) => expect(doc.dateArgNonNullList.$.date).toEqual([null, date0Encoded]))
await client.query.$batch({ dateArgNonNullList: { $: { date: [null, db.date0] } } })
})
test('arg field in non-null list non-null', async () => {
const client = clientExpected((doc) =>
expect(doc.dateArgNonNullListNonNull.$.date).toEqual([new Date(0).getTime(), new Date(1).getTime()])
expect(doc.dateArgNonNullListNonNull.$.date).toEqual([date0Encoded, date1Encoded])
)
await client.query.$batch({ dateArgNonNullListNonNull: { $: { date: [new Date(0), new Date(1)] } } })
await client.query.$batch({ dateArgNonNullListNonNull: { $: { date: [db.date0, new Date(1)] } } })
})
test(`input object field`, async () => {
const client = clientExpected((doc) => {
expect(doc.dateArgInputObject.$.input.dateRequired).toEqual(new Date(0).getTime())
expect(doc.dateArgInputObject.$.input.date).toEqual(new Date(1).getTime())
expect(doc.dateArgInputObject.$.input.dateRequired).toEqual(date0Encoded)
expect(doc.dateArgInputObject.$.input.date).toEqual(date1Encoded)
})
await client.query.$batch({
dateArgInputObject: { $: { input: { idRequired: '', dateRequired: new Date(0), date: new Date(1) } } },
dateArgInputObject: { $: { input: { idRequired: '', dateRequired: db.date0, date: new Date(1) } } },
})
})
test(`nested input object field`, async () => {
const client = clientExpected((doc) => {
expect(doc.InputObjectNested.$.input.InputObject.dateRequired).toEqual(new Date(0).getTime())
expect(doc.InputObjectNested.$.input.InputObject.date).toEqual(new Date(1).getTime())
expect(doc.InputObjectNested.$.input.InputObject.dateRequired).toEqual(date0Encoded)
expect(doc.InputObjectNested.$.input.InputObject.date).toEqual(date1Encoded)
})
await client.query.$batch({
InputObjectNested: {
$: { input: { InputObject: { idRequired: '', dateRequired: new Date(0), date: new Date(1) } } },
$: { input: { InputObject: { idRequired: '', dateRequired: db.date0, date: new Date(1) } } },
},
})
})
Expand Down
3 changes: 2 additions & 1 deletion src/client/client.document.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { describe, expect, test } from 'vitest'
import { db } from '../../tests/_/db.js'
import type { Index } from '../../tests/_/schema/generated/Index.js'
import { $Index } from '../../tests/_/schema/generated/SchemaRuntime.js'
import { db, schema } from '../../tests/_/schema/schema.js'
import { schema } from '../../tests/_/schema/schema.js'
import { create } from './client.js'

const client = create<Index>({ schema, schemaIndex: $Index })
Expand Down
34 changes: 34 additions & 0 deletions src/client/client.rootTypeMethods.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* eslint-disable */
import { expectTypeOf, test } from 'vitest'
import * as Schema from '../../tests/_/schema/schema.js'
import { create } from './client.js'

const client = create<Schema.Index>({ schema: Schema.schema, schemaIndex: Schema.$Index })

// dprint-ignore
test(`query`, () => {
// scalar
expectTypeOf(client.query.id).toEqualTypeOf<() => Promise<string | null>>()
expectTypeOf(client.query.idNonNull).toEqualTypeOf<() => Promise<string>>()
// custom scalar
expectTypeOf(client.query.date).toEqualTypeOf<() => Promise<Date | null>>()
expectTypeOf(client.query.dateNonNull).toEqualTypeOf<() => Promise<Date>>()
expectTypeOf(client.query.dateArg).toMatchTypeOf<(args?: { date?: Date | null }) => Promise<Date | null>>()
expectTypeOf(client.query.dateArgNonNull).toMatchTypeOf<(args: { date: Date }) => Promise<Date | null>>()
const x2 = client.query.dateObject1({ date1: true })
// object
expectTypeOf(client.query.dateObject1({ date1: true })).resolves.toEqualTypeOf<{ date1: Date | null } | null>()
expectTypeOf(client.query.dateObject1({ $scalars: true })).resolves.toEqualTypeOf<{ __typename: "DateObject1"; date1: Date | null } | null>()
expectTypeOf(client.query.unionFooBar({ onFoo: { id: true }})).resolves.toEqualTypeOf<{} | { id: string | null } | null>()
expectTypeOf(client.query.interface({ id: true })).resolves.toEqualTypeOf<null | { id: string | null }>()
expectTypeOf(client.query.interface({ onObject1ImplementingInterface: { int: true }})).resolves.toEqualTypeOf<{} | { int: number | null } | null>()

// @ts-expect-error missing input selection set
client.query.dateObject1()
// @ts-expect-error excess properties
const x = client.query.dateObject1({ abc: true })
// @ts-expect-error no directives on root type object fields
client.query.dateObject1({ $defer: true })
// todo @ts-expect-error empty object
// client.query.dateObject1({})
})

0 comments on commit 45412c2

Please sign in to comment.