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(ts-client): root type field methods #779

Merged
merged 12 commits into from
Apr 18, 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
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({})
})
Loading