Skip to content

Commit

Permalink
feat(ts-client): isError helper function
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonkuhrt committed Apr 20, 2024
1 parent ee66c10 commit 2a77493
Show file tree
Hide file tree
Showing 27 changed files with 461 additions and 26 deletions.
3 changes: 3 additions & 0 deletions src/Schema/core/Index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,7 @@ export interface Index {
objects: Record<string, Output.Object$2>
unions: Record<string, Output.Union>
interfaces: Record<string, Output.Interface>
error: {
objects: Record<string, Output.Object$2>
}
}
17 changes: 17 additions & 0 deletions src/client/client.error.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* eslint-disable */
import { expectTypeOf, test } from 'vitest'
import { isError } from '../../tests/_/schema/generated/Error.js'
import * as Schema from '../../tests/_/schema/schema.js'
import { create } from './client.js'

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

test('isError utility function narrows for error objects', async () => {
const result = await client.query.result({ $: { case: 'Object1' }, __typename: true })

if (isError(result)) {
expectTypeOf(result).toEqualTypeOf<{ __typename: 'ErrorOne' } | { __typename: 'ErrorTwo' }>()
} else {
expectTypeOf(result).toEqualTypeOf<null | { __typename: 'Object1' }>()
}
})
139 changes: 130 additions & 9 deletions src/generator/__snapshots__/files.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,24 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`generates types from GraphQL SDL file 1`] = `
"type Include<T, U> = Exclude<T, Exclude<T, U>>

type ObjectWithTypeName = { __typename: string }

const ErrorObjectsTypeNameSelectedEnum = {} as Record<string, ObjectWithTypeName>

const ErrorObjectsTypeNameSelected = Object.values(ErrorObjectsTypeNameSelectedEnum)

type ErrorObjectsTypeNameSelected = (typeof ErrorObjectsTypeNameSelected)[number]

export const isError = <$Value>(value: $Value): value is Include<$Value, ErrorObjectsTypeNameSelected> => {
return typeof value === 'object' && value !== null && '__typename' in value
&& ErrorObjectsTypeNameSelected.some(_ => _.__typename === value.__typename)
}
"
`;

exports[`generates types from GraphQL SDL file 2`] = `
"import { ResultSet, SelectionSet } from '../../../../../src/entrypoints/alpha/schema.js'
import { Index } from './Index.js'

Expand Down Expand Up @@ -86,7 +104,7 @@ export type Interface<$SelectionSet extends SelectionSet.Interface<Index['interf
"
`;

exports[`generates types from GraphQL SDL file 2`] = `
exports[`generates types from GraphQL SDL file 3`] = `
"/* eslint-disable */

import type * as Schema from './SchemaBuildtime.js'
Expand Down Expand Up @@ -119,11 +137,14 @@ export interface Index {
DateInterface1: Schema.Interface.DateInterface1
Interface: Schema.Interface.Interface
}
error: {
objects: {}
}
}
"
`;

exports[`generates types from GraphQL SDL file 3`] = `
exports[`generates types from GraphQL SDL file 4`] = `
"import type * as $ from '../../../../../src/entrypoints/alpha/schema.js'
import type * as $Scalar from './Scalar.ts'

Expand Down Expand Up @@ -419,7 +440,7 @@ export namespace Union {
"
`;

exports[`generates types from GraphQL SDL file 4`] = `
exports[`generates types from GraphQL SDL file 5`] = `
"import type * as CustomScalar from '../../../../_/customScalarCodecs.js'

declare global {
Expand All @@ -433,7 +454,7 @@ export * from '../../../../_/customScalarCodecs.js'
"
`;

exports[`generates types from GraphQL SDL file 5`] = `
exports[`generates types from GraphQL SDL file 6`] = `
"/* eslint-disable */

import * as $ from '../../../../../src/entrypoints/alpha/schema.js'
Expand Down Expand Up @@ -648,11 +669,35 @@ export const $Index = {
DateInterface1,
Interface,
},
error: {
objects: {},
},
}
"
`;

exports[`schema2 1`] = `
"type Include<T, U> = Exclude<T, Exclude<T, U>>

type ObjectWithTypeName = { __typename: string }

const ErrorObjectsTypeNameSelectedEnum = {
ErrorOne: { __typename: 'ErrorOne' },
ErrorTwo: { __typename: 'ErrorTwo' },
} as const

const ErrorObjectsTypeNameSelected = Object.values(ErrorObjectsTypeNameSelectedEnum)

type ErrorObjectsTypeNameSelected = (typeof ErrorObjectsTypeNameSelected)[number]

export const isError = <$Value>(value: $Value): value is Include<$Value, ErrorObjectsTypeNameSelected> => {
return typeof value === 'object' && value !== null && '__typename' in value
&& ErrorObjectsTypeNameSelected.some(_ => _.__typename === value.__typename)
}
"
`;

exports[`schema2 2`] = `
"import { ResultSet, SelectionSet } from '../../../../src/entrypoints/alpha/schema.js'
import { Index } from './Index.js'

Expand Down Expand Up @@ -683,6 +728,12 @@ export type Bar<$SelectionSet extends SelectionSet.Object<Index['objects']['Bar'
export type DateObject1<$SelectionSet extends SelectionSet.Object<Index['objects']['DateObject1'], Index>> =
ResultSet.Object$<$SelectionSet, Index['objects']['DateObject1'], Index>

export type ErrorOne<$SelectionSet extends SelectionSet.Object<Index['objects']['ErrorOne'], Index>> =
ResultSet.Object$<$SelectionSet, Index['objects']['ErrorOne'], Index>

export type ErrorTwo<$SelectionSet extends SelectionSet.Object<Index['objects']['ErrorTwo'], Index>> =
ResultSet.Object$<$SelectionSet, Index['objects']['ErrorTwo'], Index>

export type Foo<$SelectionSet extends SelectionSet.Object<Index['objects']['Foo'], Index>> = ResultSet.Object$<
$SelectionSet,
Index['objects']['Foo'],
Expand All @@ -709,15 +760,24 @@ export type Object2ImplementingInterface<
export type FooBarUnion<$SelectionSet extends SelectionSet.Union<Index['unions']['FooBarUnion'], Index>> =
ResultSet.Union<$SelectionSet, Index['unions']['FooBarUnion'], Index>

export type Result<$SelectionSet extends SelectionSet.Union<Index['unions']['Result'], Index>> = ResultSet.Union<
$SelectionSet,
Index['unions']['Result'],
Index
>

// Interface Types
// ---------------

export type Error<$SelectionSet extends SelectionSet.Interface<Index['interfaces']['Error'], Index>> =
ResultSet.Interface<$SelectionSet, Index['interfaces']['Error'], Index>

export type Interface<$SelectionSet extends SelectionSet.Interface<Index['interfaces']['Interface'], Index>> =
ResultSet.Interface<$SelectionSet, Index['interfaces']['Interface'], Index>
"
`;

exports[`schema2 2`] = `
exports[`schema2 3`] = `
"/* eslint-disable */

import type * as Schema from './SchemaBuildtime.js'
Expand All @@ -731,22 +791,32 @@ export interface Index {
objects: {
Bar: Schema.Object.Bar
DateObject1: Schema.Object.DateObject1
ErrorOne: Schema.Object.ErrorOne
ErrorTwo: Schema.Object.ErrorTwo
Foo: Schema.Object.Foo
Object1: Schema.Object.Object1
Object1ImplementingInterface: Schema.Object.Object1ImplementingInterface
Object2ImplementingInterface: Schema.Object.Object2ImplementingInterface
}
unions: {
FooBarUnion: Schema.Union.FooBarUnion
Result: Schema.Union.Result
}
interfaces: {
Error: Schema.Interface.Error
Interface: Schema.Interface.Interface
}
error: {
objects: {
ErrorOne: Schema.Object.ErrorOne
ErrorTwo: Schema.Object.ErrorTwo
}
}
}
"
`;

exports[`schema2 3`] = `
exports[`schema2 4`] = `
"import type * as $ from '../../../../src/entrypoints/alpha/schema.js'
import type * as $Scalar from './Scalar.ts'

Expand Down Expand Up @@ -789,6 +859,12 @@ export namespace Root {
string: $.Input.Nullable<$Scalar.String>
}>
>
result: $.Field<
$.Output.Nullable<Union.Result>,
$.Args<{
case: Enum.Case
}>
>
unionFooBar: $.Field<$.Output.Nullable<Union.FooBarUnion>, null>
}>
}
Expand All @@ -798,7 +874,7 @@ export namespace Root {
// ------------------------------------------------------------ //

export namespace Enum {
// -- no types --
export type Case = $.Enum<'Case', ['ErrorOne', 'ErrorTwo', 'Object1']>
}

// ------------------------------------------------------------ //
Expand All @@ -814,6 +890,10 @@ export namespace InputObject {
// ------------------------------------------------------------ //

export namespace Interface {
export type Error = $.Interface<'Error', {
message: $.Field<$Scalar.String, null>
}, [Object.ErrorOne, Object.ErrorTwo]>

export type Interface = $.Interface<'Interface', {
id: $.Field<$.Output.Nullable<$Scalar.ID>, null>
}, [Object.Object1ImplementingInterface, Object.Object2ImplementingInterface]>
Expand All @@ -832,6 +912,16 @@ export namespace Object {
date1: $.Field<$.Output.Nullable<$Scalar.Date>, null>
}>

export type ErrorOne = $.Object$2<'ErrorOne', {
infoId: $.Field<$.Output.Nullable<$Scalar.ID>, null>
message: $.Field<$Scalar.String, null>
}>

export type ErrorTwo = $.Object$2<'ErrorTwo', {
infoInt: $.Field<$.Output.Nullable<$Scalar.Int>, null>
message: $.Field<$Scalar.String, null>
}>

/**
* Object documentation.
*/
Expand Down Expand Up @@ -872,11 +962,13 @@ export namespace Union {
* Union documentation.
*/
export type FooBarUnion = $.Union<'FooBarUnion', [Object.Bar, Object.Foo]>

export type Result = $.Union<'Result', [Object.ErrorOne, Object.ErrorTwo, Object.Object1]>
}
"
`;

exports[`schema2 4`] = `
exports[`schema2 5`] = `
"import type * as CustomScalar from '../../customScalarCodecs.js'

declare global {
Expand All @@ -890,12 +982,14 @@ export * from '../../customScalarCodecs.js'
"
`;

exports[`schema2 5`] = `
exports[`schema2 6`] = `
"/* eslint-disable */

import * as $ from '../../../../src/entrypoints/alpha/schema.js'
import * as $Scalar from './Scalar.js'

export const Case = $.Enum(\`Case\`, [\`ErrorOne\`, \`ErrorTwo\`, \`Object1\`])

// @ts-ignore - circular types cannot infer. Ignore in case there are any. This comment is always added, it does not indicate if this particular type could infer or not.
export const Bar = $.Object$(\`Bar\`, {
int: $.field($.Output.Nullable($Scalar.Int)),
Expand All @@ -906,6 +1000,18 @@ export const DateObject1 = $.Object$(\`DateObject1\`, {
date1: $.field($.Output.Nullable($Scalar.Date)),
})

// @ts-ignore - circular types cannot infer. Ignore in case there are any. This comment is always added, it does not indicate if this particular type could infer or not.
export const ErrorOne = $.Object$(\`ErrorOne\`, {
infoId: $.field($.Output.Nullable($Scalar.ID)),
message: $.field($Scalar.String),
})

// @ts-ignore - circular types cannot infer. Ignore in case there are any. This comment is always added, it does not indicate if this particular type could infer or not.
export const ErrorTwo = $.Object$(\`ErrorTwo\`, {
infoInt: $.field($.Output.Nullable($Scalar.Int)),
message: $.field($Scalar.String),
})

// @ts-ignore - circular types cannot infer. Ignore in case there are any. This comment is always added, it does not indicate if this particular type could infer or not.
export const Foo = $.Object$(\`Foo\`, {
id: $.field($.Output.Nullable($Scalar.ID)),
Expand Down Expand Up @@ -935,6 +1041,10 @@ export const Object2ImplementingInterface = $.Object$(\`Object2ImplementingInter
// @ts-ignore - circular types cannot infer. Ignore in case there are any. This comment is always added, it does not indicate if this particular type could infer or not.
export const FooBarUnion = $.Union(\`FooBarUnion\`, [Bar, Foo])

// @ts-ignore - circular types cannot infer. Ignore in case there are any. This comment is always added, it does not indicate if this particular type could infer or not.
export const Result = $.Union(\`Result\`, [ErrorOne, ErrorTwo, Object1])

export const Error = $.Interface(\`Error\`, { message: $.field($Scalar.String) }, [ErrorOne, ErrorTwo])
export const Interface = $.Interface(\`Interface\`, { id: $.field($.Output.Nullable($Scalar.ID)) }, [
Object1ImplementingInterface,
Object2ImplementingInterface,
Expand Down Expand Up @@ -966,6 +1076,7 @@ export const Query = $.Object$(\`Query\`, {
string: $.Input.Nullable($Scalar.String),
}),
),
result: $.field($.Output.Nullable(() => Result), $.Args({ case: Case })),
unionFooBar: $.field($.Output.Nullable(() => FooBarUnion)),
})

Expand All @@ -978,17 +1089,27 @@ export const $Index = {
objects: {
Bar,
DateObject1,
ErrorOne,
ErrorTwo,
Foo,
Object1,
Object1ImplementingInterface,
Object2ImplementingInterface,
},
unions: {
FooBarUnion,
Result,
},
interfaces: {
Error,
Interface,
},
error: {
objects: {
ErrorOne,
ErrorTwo,
},
},
}
"
`;
34 changes: 34 additions & 0 deletions src/generator/code/Error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { Config } from './generateCode.js'

export const moduleNameError = `Error`

export const generateError = (config: Config) => {
const code: string[] = []

code.push(
`type Include<T, U> = Exclude<T, Exclude<T, U>>`,
`type ObjectWithTypeName = { __typename: string }`,
)

code.push(`
const ErrorObjectsTypeNameSelectedEnum = {
${config.error.objects.map(_ => `${_.name}: { __typename: '${_.name}' }`).join(`,\n`)}
} as ${config.error.objects.length > 0 ? `const` : `Record<string,ObjectWithTypeName>`}
const ErrorObjectsTypeNameSelected = Object.values(ErrorObjectsTypeNameSelectedEnum)
type ErrorObjectsTypeNameSelected = (typeof ErrorObjectsTypeNameSelected)[number]
`)

code.push(
`export const isError = <$Value>(value:$Value): value is Include<$Value, ErrorObjectsTypeNameSelected> => {
return typeof value === 'object' && value !== null && '__typename' in value &&
ErrorObjectsTypeNameSelected.some(_ => _.__typename === value.__typename)
}`,
)

return {
code: code.join(`\n\n`),
moduleName: moduleNameError,
}
}
7 changes: 7 additions & 0 deletions src/generator/code/Index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ export const generateIndex = (config: Config) => {
interfaces: Code.objectFromEntries(
config.typeMapByKind.GraphQLInterfaceType.map(_ => [_.name, `${namespace}.Interface.${_.name}`]),
),
// todo jsdoc comment saying:
// Objects that match this pattern name: /.../
error: Code.objectFrom({
objects: Code.objectFromEntries(
config.error.objects.map(_ => [_.name, `${namespace}.Object.${_.name}`]),
),
}),
}),
),
))
Expand Down

0 comments on commit 2a77493

Please sign in to comment.