Skip to content

Commit

Permalink
feat(ts-client): custom scalar runtime codecs (#746)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonkuhrt committed Apr 9, 2024
1 parent be14f81 commit 5658370
Show file tree
Hide file tree
Showing 39 changed files with 2,078 additions and 578 deletions.
11 changes: 6 additions & 5 deletions src/ResultSet/ResultSet.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
/* eslint-disable @typescript-eslint/ban-types */

import { expectTypeOf, test } from 'vitest'
import type * as Schema from '../../tests/ts/_/schema/Schema.js'
import type { $ } from '../../tests/ts/_/schema/generated/Index.js'
import type * as Schema from '../../tests/ts/_/schema/generated/SchemaBuildtime.js'
import type { SelectionSet } from '../SelectionSet/__.js'
import type { ResultSet } from './__.js'

type I = Schema.$.Index
type I = $.Index
type RS<$selectionSet extends SelectionSet.Query<I>> = ResultSet.Query<$selectionSet, I>

// dprint-ignore
Expand All @@ -24,7 +25,7 @@ test(`general`, () => {
expectTypeOf<RS<{ id: true; string: undefined }>>().toEqualTypeOf<{ id: null | string }>()

// Custom Scalar
expectTypeOf<RS<{ date: true }>>().toEqualTypeOf<{ date: null | string }>()
expectTypeOf<RS<{ date: true }>>().toEqualTypeOf<{ date: null | Date }>()

// List
expectTypeOf<RS<{ listIntNonNull: true }>>().toEqualTypeOf<{ listIntNonNull: number[] }>()
Expand All @@ -41,11 +42,11 @@ test(`general`, () => {
expectTypeOf<RS<{ objectNonNull: { id: true } }>>().toEqualTypeOf<{ objectNonNull: { id: string | null } }>()

// scalars-wildcard
expectTypeOf<RS<{ objectNonNull: { $scalars: true } }>>().toEqualTypeOf<{ objectNonNull: { __typename: "Object"; string: null|string; int: null|number; float: null|number; boolean: null|boolean; id: null|string; } }>()
expectTypeOf<RS<{ objectNonNull: { $scalars: true } }>>().toEqualTypeOf<{ objectNonNull: { __typename: "Object1"; string: null|string; int: null|number; float: null|number; boolean: null|boolean; id: null|string } }>()
// scalars-wildcard with nested object
expectTypeOf<RS<{ objectNested: { $scalars: true } }>>().toEqualTypeOf<{ objectNested: null | { __typename: "ObjectNested"; id: null|string } }>()
// __typename
expectTypeOf<RS<{ objectNonNull: { __typename: true } }>>().toEqualTypeOf<{ objectNonNull: { __typename: "Object" } }>()
expectTypeOf<RS<{ objectNonNull: { __typename: true } }>>().toEqualTypeOf<{ objectNonNull: { __typename: "Object1" } }>()

// Union
expectTypeOf<RS<{ fooBarUnion: { __typename: true } }>>().toEqualTypeOf<{ fooBarUnion: null | { __typename: "Foo" } | { __typename: "Bar" } }>()
Expand Down
18 changes: 9 additions & 9 deletions src/ResultSet/ResultSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@ import type { SelectionSet } from '../SelectionSet/__.js'

// dprint-ignore
export type Query<$SelectionSetQuery extends object, $Index extends Schema.Index> =
SimplifyDeep<Object<$SelectionSetQuery, Exclude<$Index['Root']['Query'], null>, $Index>>
SimplifyDeep<Object$<$SelectionSetQuery, Exclude<$Index['Root']['Query'], null>, $Index>>

// dprint-ignore
export type Mutation<$SelectionSetMutation extends object, $Index extends Schema.Index> =
SimplifyDeep<Object<$SelectionSetMutation, Exclude<$Index['Root']['Mutation'], null>, $Index>>
SimplifyDeep<Object$<$SelectionSetMutation, Exclude<$Index['Root']['Mutation'], null>, $Index>>

// dprint-ignore
export type Subscription<$SelectionSetSubscription extends object, $Index extends Schema.Index> =
SimplifyDeep<Object<$SelectionSetSubscription, Exclude<$Index['Root']['Subscription'], null>, $Index>>
SimplifyDeep<Object$<$SelectionSetSubscription, Exclude<$Index['Root']['Subscription'], null>, $Index>>

// dprint-ignore
export type Object<$SelectionSet, $Node extends Schema.Named.Object, $Index extends Schema.Index> =
export type Object$<$SelectionSet, $Node extends Schema.Named.Object$2, $Index extends Schema.Index> =
SelectionSet.IsSelectScalarsWildcard<$SelectionSet> extends true

/**
Expand Down Expand Up @@ -52,9 +52,9 @@ type Interface<$SelectionSet, $Node extends Schema.Named.Interface, $Index exten
OnTypeFragment<$SelectionSet, $Node['implementors'][number], $Index>

// dprint-ignore
type OnTypeFragment<$SelectionSet, $Node extends Schema.Named.Object, $Index extends Schema.Index> =
type OnTypeFragment<$SelectionSet, $Node extends Schema.Named.Object$2, $Index extends Schema.Index> =
$Node extends any // force distribution
? Object<
? Object$<
GetKeyOr<$SelectionSet, `on${Capitalize<$Node['fields']['__typename']['type']['type']>}`, {}> & SelectionSet.OmitOnTypeFragments<$SelectionSet>,
$Node,
$Index
Expand All @@ -81,8 +81,8 @@ type FieldType<
$Type extends Schema.Field.Type.Output.Nullable<infer $InnerType> ? null | FieldType<$SelectionSet, $InnerType, $Index> :
$Type extends Schema.Field.Type.Output.List<infer $InnerType> ? Array<FieldType<$SelectionSet, $InnerType, $Index>> :
$Type extends Schema.Named.Enum<infer _, infer $Members> ? $Members[number] :
$Type extends Schema.Named.Scalar.Any ? ReturnType<$Type['constructor']> :
$Type extends Schema.Named.Object ? Object<$SelectionSet,$Type,$Index> :
$Type extends Schema.Named.Scalar.Any ? ReturnType<$Type['codec']['decode']> :
$Type extends Schema.Named.Object$2 ? Object$<$SelectionSet,$Type,$Index> :
$Type extends Schema.Named.Interface ? Interface<$SelectionSet,$Type,$Index> :
$Type extends Schema.Named.Union ? Union<$SelectionSet,$Type,$Index> :
TSError<'FieldType', `Unknown type`, { $Type: $Type }>
Expand All @@ -104,5 +104,5 @@ type FieldDirectiveSkip<$SelectionSet> =

// dprint-ignore
export namespace Errors {
export type UnknownFieldName<$FieldName extends string, $Object extends Schema.Named.Object> = TSError<'Object', `field "${$FieldName}" does not exist on object "${$Object['fields']['__typename']['type']['type']}"`>
export type UnknownFieldName<$FieldName extends string, $Object extends Schema.Named.Object$2> = TSError<'Object', `field "${$FieldName}" does not exist on object "${$Object['fields']['__typename']['type']['type']}"`>
}
16 changes: 6 additions & 10 deletions src/Schema/Field/Field.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { NamedType } from '../NamedType/__.js'
import type { Scalar } from '../NamedType/Scalar/_.js'
import * as Type from './Type.js'

import type * as Type from './Type.js'

export type * as Type from './Type.js'

Expand All @@ -22,16 +23,11 @@ export interface Args<$Fields extends any = any> {
fields: $Fields
}

export const field = <$Type extends Type.Output.Any, $Args extends null | Args = null>(
type: $Type,
args: $Args = null as $Args,
): Field<$Type, $Args> => {
export const Args = <F>(fields: F): Args<F> => {
return {
// eslint-disable-next-line
// @ts-ignore infinite depth issue, can this be fixed?
typeUnwrapped: Type.Output.unwrap(type),
type,
args,
// @ts-expect-error todo
allOptional: false,
fields,
}
}

Expand Down
89 changes: 86 additions & 3 deletions src/Schema/Field/Type.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { TSError } from '../../lib/TSError.js'
import type { NamedType } from '../NamedType/__.js'
import type { Args } from './Field.js'

const buildTimeOnly: any = undefined
export namespace Base {
export interface Nullable<$Type> {
kind: 'nullable'
Expand All @@ -17,17 +19,29 @@ export namespace Output {
kind: 'typename'
type: $Type
}
export type Nullable<$Type extends Any> = Base.Nullable<$Type>

export type Named = NamedType.AnyOutput

export type Nullable<$Type extends Output.List<any> | __typename<any> | NamedType.AnyOutput> = Base.Nullable<$Type>

export type List<$Type extends Any> = Base.List<$Type>

export type Any = Output.List<any> | __typename<any> | Base.Nullable<any> | NamedType.AnyOutput

export const __typename = <$Type extends string>(type: $Type): __typename<$Type> => ({ kind: `typename`, type })
export const nullable = <$Type extends __typename<any> | List<any>>(type: $Type): Nullable<$Type> => ({

export const Nullable = <$Type extends __typename<any> | List<any> | NamedType.AnyOutput>(
type: MaybeThunk<$Type>,
): Nullable<$Type> => ({
kind: `nullable`,
// at type level "type" is not a thunk
type: type as any, // eslint-disable-line
})

export const List = <$Type extends Any>(type: $Type): List<$Type> => ({
kind: `list`,
type,
})
export const list = <$Type extends Any>(type: $Type): List<$Type> => ({ kind: `list`, type })

// todo extends any because of infinite depth issue in generated schema types
// dprint-ignore
Expand All @@ -37,15 +51,84 @@ export namespace Output {
$Type extends __typename ? $Type['type'] :
$Type extends NamedType.AnyOutput ? $Type :
TSError<'Unwrap', 'Unknown $Type', { $Type: $Type }>
// dprint-ignore
export type UnwrapNonNull<$Type> =
$Type extends Nullable<infer $innerType> ? UnwrapNonNull<$innerType>
: $Type

export const unwrapNonNull = <$Type extends Any>(type: $Type): UnwrapNonNull<$Type> => {
if (type.kind === `nullable`) return type.type
return type as UnwrapNonNull<$Type>
}

export const unwrap = <$Type extends Any>(type: $Type): Unwrap<$Type> => {
console.log({ type })
// @ts-expect-error fixme
return type.kind === `named` ? type.type : unwrap(type.type)
}

export const field = <$Type extends Any, $Args extends null | Args = null>(
type: MaybeThunk<$Type>,
args: $Args = null as $Args,
): Field<$Type, $Args> => {
return {
typeUnwrapped: buildTimeOnly, // eslint-disable-line
// At type level "type" is not a thunk
type: type as any, // eslint-disable-line
args,
}
}

export type Field<$Type extends any = any, $Args extends Args | null = Args | null> = {
typeUnwrapped: Unwrap<$Type>
type: $Type
args: $Args
}
}

export namespace Input {
export type Nullable<$InnerType extends Any = Any> = Base.Nullable<$InnerType>
export type List<$InnerType extends Any = Any> = Base.List<$InnerType>
export type Any = List<any> | Nullable<any> | NamedType.AnyInput

export const Nullable = <$InnerType extends Any>(type: MaybeThunk<$InnerType>): Nullable<$InnerType> => ({
kind: `nullable`,
// at type level "type" is not a thunk
type: type as any, // eslint-disable-line
})

export const List = <$InnerType extends Any>(type: $InnerType): List<$InnerType> => ({
kind: `list`,
type,
})

export const field = <$Type extends Any>(type: $Type): Field<$Type> => {
return {
type: type,
}
}

// dprint-ignore
type UnwrapNonNull<$Type> =
$Type extends Nullable<infer $innerType> ? UnwrapNonNull<$innerType>
: $Type

export const unwrapNullable = <$Type extends Any>(type: $Type): UnwrapNonNull<$Type> => {
if (type.kind === `nullable`) return type.type
// @ts-expect-error fixme
return type
}

export type Field<$Type extends any = any> = {
// typeUnwrapped: Type.Output.Unwrap<$Type>
type: $Type
}
}

type MaybeThunk<$Type> = $Type | Thunk<$Type>

type Thunk<$Type> = () => $Type

export const readMaybeThunk = <T>(maybeThunk: MaybeThunk<T>): T =>
// @ts-expect-error fixme
typeof maybeThunk === `function` ? maybeThunk() : maybeThunk
14 changes: 6 additions & 8 deletions src/Schema/Index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
/* eslint-disable @typescript-eslint/ban-types */

import type { Object, Union } from './__.js'
import type { Object$2, Union } from './__.js'

export interface Index {
Root: {
Query: null | Object
Mutation: null | Object
Subscription: null | Object
}
objects: Record<string, Object>
unions: {
Union: null | Union
Query: null | Object$2
Mutation: null | Object$2
Subscription: null | Object$2
}
objects: Record<string, Object$2>
unions: Record<string, Union>
}
12 changes: 6 additions & 6 deletions src/Schema/NamedType/Interface.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/* eslint-disable @typescript-eslint/ban-types */

import type { Field } from '../Field/Field.js'
import type { Object } from './Object.js'
import type { Output } from '../__.js'
import type { Object$2 } from './Object.js'

export type Interface<
$Name extends string = string,
$Fields extends Record<string, Field<any>> = Record<string, Field<any>>,
$Implementors extends [Object, ...Object[]] = [Object, ...Object[]],
$Fields extends Record<string, Output.Field<any>> = Record<string, Output.Field<any>>,
$Implementors extends [Object$2, ...Object$2[]] = [Object$2, ...Object$2[]],
> = {
kind: 'Interface'
name: $Name
Expand All @@ -16,8 +16,8 @@ export type Interface<

export const Interface = <
$Name extends string,
$Fields extends Record<keyof $Fields, Field>,
$Implementors extends [Object, ...Object[]],
$Fields extends Record<keyof $Fields, Output.Field>,
$Implementors extends [Object$2, ...Object$2[]],
>(
name: $Name,
fields: $Fields,
Expand Down
4 changes: 2 additions & 2 deletions src/Schema/NamedType/NamedType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import type { Digit, Letter } from '../../lib/prelude.js'
import type { Enum } from './Enum.js'
import type { InputObject } from './InputObjet.js'
import type { Interface } from './Interface.js'
import type { Object } from './Object.js'
import type { Object$2 } from './Object.js'
import type { Scalar } from './Scalar/_.js'
import type { Union } from './Union.js'

export type AnyOutput = Interface | Enum | Object | Scalar.Any | Union
export type AnyOutput = Interface | Enum | Object$2 | Scalar.Any | Union
export type AnyInput = Enum | Scalar.Any | InputObject
export type Any = AnyOutput | AnyInput

Expand Down
26 changes: 17 additions & 9 deletions src/Schema/NamedType/Object.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,40 @@
/* eslint-disable @typescript-eslint/ban-types */

import type { Field } from '../Field/Field.js'
import { field } from '../Field/Field.js'
import { Output } from '../Field/Type.js'
import type { Scalar } from './_.js'
import type { Enum } from './Enum.js'

export type Fields = Record<string, Field<any>>
export type Fields = Record<
string,
Output.Field<Output.List<any> | Output.Nullable<any> | Object$2 | Enum | Scalar.Any>
>

export type ObjectFields = {
__typename: Field<Output.__typename>
__typename: Output.Field<Output.__typename>
} & Fields

export interface Object<
export interface Object$2<
$Name extends string = string,
$Fields extends Fields = Fields,
> {
kind: 'Object'
fields: {
__typename: Field<Output.__typename<$Name>>
__typename: Output.Field<Output.__typename<$Name>>
} & $Fields
}

export const Object = <$Name extends string, $Fields extends Record<keyof $Fields, Field>>(
// Naming this "Object" breaks Vitest: https://github.com/vitest-dev/vitest/issues/5463
export const Object$ = <$Name extends string, $Fields extends Record<keyof $Fields, Output.Field>>(
name: $Name,
fields: $Fields,
): Object<$Name, $Fields> => ({
// eslint-disable-next-line
// @ts-ignore infinite depth issue
): Object$2<$Name, $Fields> => ({
kind: `Object`,
fields: {
__typename: field(Output.__typename(name)),
__typename: Output.field(Output.__typename(name)),
...fields,
},
})

export { Object$ as Object }

0 comments on commit 5658370

Please sign in to comment.