From 2a774933b1b9a4017e01bec7582364267a343640 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Fri, 19 Apr 2024 12:40:35 +0200 Subject: [PATCH] feat(ts-client): isError helper function --- src/Schema/core/Index.ts | 3 + src/client/client.error.test-d.ts | 17 +++ .../__snapshots__/files.test.ts.snap | 139 ++++++++++++++++-- src/generator/code/Error.ts | 34 +++++ src/generator/code/Index.ts | 7 + src/generator/code/SchemaRuntime.ts | 5 + src/generator/code/generateCode.ts | 44 ++++-- src/generator/files.test.ts | 6 + src/generator/files.ts | 3 + tests/_/db.ts | 4 +- tests/_/schema/generated/Error.ts | 17 +++ tests/_/schema/generated/Index.ts | 10 ++ tests/_/schema/generated/SchemaBuildtime.ts | 24 ++- tests/_/schema/generated/SchemaRuntime.ts | 29 ++++ tests/_/schema/generated/Select.ts | 15 ++ tests/_/schema/schema.graphql | 25 +++- tests/_/schema/schema.ts | 38 ++++- tests/_/schemaGenerate.ts | 7 +- tests/_/schemaMutationOnly/generated/Error.ts | 14 ++ tests/_/schemaMutationOnly/generated/Index.ts | 3 + .../generated/SchemaRuntime.ts | 3 + tests/_/schemaQueryOnly/generated/Error.ts | 14 ++ tests/_/schemaQueryOnly/generated/Index.ts | 3 + .../generated/SchemaRuntime.ts | 3 + tests/ts/_/schema/generated/Error.ts | 14 ++ tests/ts/_/schema/generated/Index.ts | 3 + tests/ts/_/schema/generated/SchemaRuntime.ts | 3 + 27 files changed, 461 insertions(+), 26 deletions(-) create mode 100644 src/client/client.error.test-d.ts create mode 100644 src/generator/code/Error.ts create mode 100644 tests/_/schema/generated/Error.ts create mode 100644 tests/_/schemaMutationOnly/generated/Error.ts create mode 100644 tests/_/schemaQueryOnly/generated/Error.ts create mode 100644 tests/ts/_/schema/generated/Error.ts diff --git a/src/Schema/core/Index.ts b/src/Schema/core/Index.ts index 1982368d7..d45bffc27 100644 --- a/src/Schema/core/Index.ts +++ b/src/Schema/core/Index.ts @@ -9,4 +9,7 @@ export interface Index { objects: Record unions: Record interfaces: Record + error: { + objects: Record + } } diff --git a/src/client/client.error.test-d.ts b/src/client/client.error.test-d.ts new file mode 100644 index 000000000..6af603e29 --- /dev/null +++ b/src/client/client.error.test-d.ts @@ -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: 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() + } +}) diff --git a/src/generator/__snapshots__/files.test.ts.snap b/src/generator/__snapshots__/files.test.ts.snap index b3a436e3b..f9121a1a1 100644 --- a/src/generator/__snapshots__/files.test.ts.snap +++ b/src/generator/__snapshots__/files.test.ts.snap @@ -1,6 +1,24 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`generates types from GraphQL SDL file 1`] = ` +"type Include = Exclude> + +type ObjectWithTypeName = { __typename: string } + +const ErrorObjectsTypeNameSelectedEnum = {} as Record + +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' @@ -86,7 +104,7 @@ export type Interface<$SelectionSet extends SelectionSet.Interface = Exclude> + +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' @@ -683,6 +728,12 @@ export type Bar<$SelectionSet extends SelectionSet.Object> = ResultSet.Object$<$SelectionSet, Index['objects']['DateObject1'], Index> +export type ErrorOne<$SelectionSet extends SelectionSet.Object> = + ResultSet.Object$<$SelectionSet, Index['objects']['ErrorOne'], Index> + +export type ErrorTwo<$SelectionSet extends SelectionSet.Object> = + ResultSet.Object$<$SelectionSet, Index['objects']['ErrorTwo'], Index> + export type Foo<$SelectionSet extends SelectionSet.Object> = ResultSet.Object$< $SelectionSet, Index['objects']['Foo'], @@ -709,15 +760,24 @@ export type Object2ImplementingInterface< export type FooBarUnion<$SelectionSet extends SelectionSet.Union> = ResultSet.Union<$SelectionSet, Index['unions']['FooBarUnion'], Index> +export type Result<$SelectionSet extends SelectionSet.Union> = ResultSet.Union< + $SelectionSet, + Index['unions']['Result'], + Index +> + // Interface Types // --------------- +export type Error<$SelectionSet extends SelectionSet.Interface> = + ResultSet.Interface<$SelectionSet, Index['interfaces']['Error'], Index> + export type Interface<$SelectionSet extends SelectionSet.Interface> = ResultSet.Interface<$SelectionSet, Index['interfaces']['Interface'], Index> " `; -exports[`schema2 2`] = ` +exports[`schema2 3`] = ` "/* eslint-disable */ import type * as Schema from './SchemaBuildtime.js' @@ -731,6 +791,8 @@ 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 @@ -738,15 +800,23 @@ export interface Index { } 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' @@ -789,6 +859,12 @@ export namespace Root { string: $.Input.Nullable<$Scalar.String> }> > + result: $.Field< + $.Output.Nullable, + $.Args<{ + case: Enum.Case + }> + > unionFooBar: $.Field<$.Output.Nullable, null> }> } @@ -798,7 +874,7 @@ export namespace Root { // ------------------------------------------------------------ // export namespace Enum { - // -- no types -- + export type Case = $.Enum<'Case', ['ErrorOne', 'ErrorTwo', 'Object1']> } // ------------------------------------------------------------ // @@ -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]> @@ -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. */ @@ -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 { @@ -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)), @@ -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)), @@ -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, @@ -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)), }) @@ -978,6 +1089,8 @@ export const $Index = { objects: { Bar, DateObject1, + ErrorOne, + ErrorTwo, Foo, Object1, Object1ImplementingInterface, @@ -985,10 +1098,18 @@ export const $Index = { }, unions: { FooBarUnion, + Result, }, interfaces: { + Error, Interface, }, + error: { + objects: { + ErrorOne, + ErrorTwo, + }, + }, } " `; diff --git a/src/generator/code/Error.ts b/src/generator/code/Error.ts new file mode 100644 index 000000000..89090d87d --- /dev/null +++ b/src/generator/code/Error.ts @@ -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 = Exclude>`, + `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`} + + 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, + } +} diff --git a/src/generator/code/Index.ts b/src/generator/code/Index.ts index c41392595..46493bb3f 100644 --- a/src/generator/code/Index.ts +++ b/src/generator/code/Index.ts @@ -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}`]), + ), + }), }), ), )) diff --git a/src/generator/code/SchemaRuntime.ts b/src/generator/code/SchemaRuntime.ts index 4fc9838fd..624932c79 100644 --- a/src/generator/code/SchemaRuntime.ts +++ b/src/generator/code/SchemaRuntime.ts @@ -74,6 +74,11 @@ const index = (config: Config) => { }, interfaces: { ${config.typeMapByKind.GraphQLInterfaceType.map(type => type.name).join(`,\n`)} + }, + error: { + objects: { + ${config.error.objects.map(type => type.name).join(`,\n`)} + } } } ` diff --git a/src/generator/code/generateCode.ts b/src/generator/code/generateCode.ts index 271771864..634a2d32d 100644 --- a/src/generator/code/generateCode.ts +++ b/src/generator/code/generateCode.ts @@ -1,15 +1,28 @@ import type { Formatter } from '@dprint/formatter' -import type { GraphQLSchema } from 'graphql' +import type { GraphQLObjectType, GraphQLSchema } from 'graphql' import { buildSchema } from 'graphql' import * as Path from 'node:path' import type { TypeMapByKind } from '../../lib/graphql.js' import { getTypeMapByKind } from '../../lib/graphql.js' +import { generateError } from './Error.js' import { generateIndex } from './Index.js' import { generateScalar } from './Scalar.js' import { generateSchemaBuildtime } from './SchemaBuildtime.js' import { generateRuntimeSchema } from './SchemaRuntime.js' import { generateSelect } from './Select.js' +export interface OptionsInput { + errorTypeNamePattern?: RegExp + /** + * Should custom scalars definitions be imported into the generated output? + */ + customScalars?: boolean + formatter?: Formatter + TSDoc?: { + noDocPolicy?: 'message' | 'ignore' + } +} + export interface Input { libraryPaths?: { schema?: string @@ -22,21 +35,16 @@ export interface Input { * The GraphQL SDL source code. */ schemaSource: string - options?: { - /** - * Should custom scalars definitions be imported into the generated output? - */ - customScalars?: boolean - formatter?: Formatter - TSDoc?: { - noDocPolicy?: 'message' | 'ignore' - } - } + options?: OptionsInput } export interface Config { schema: GraphQLSchema typeMapByKind: TypeMapByKind + error: { + objects: GraphQLObjectType[] + enabled: boolean + } libraryPaths: { schema: string scalars: string @@ -45,6 +53,7 @@ export interface Config { customScalarCodecs: string } options: { + errorTypeNamePattern: RegExp | null customScalars: boolean TSDoc: { noDocPolicy: 'message' | 'ignore' @@ -53,9 +62,18 @@ export interface Config { } export const resolveOptions = (input: Input): Config => { + const errorTypeNamePattern = input.options?.errorTypeNamePattern ?? null const schema = buildSchema(input.schemaSource) + const typeMapByKind = getTypeMapByKind(schema) + const errorObjects = errorTypeNamePattern + ? Object.values(typeMapByKind.GraphQLObjectType).filter(_ => _.name.match(errorTypeNamePattern)) + : [] return { schema, + error: { + enabled: Boolean(errorTypeNamePattern), + objects: errorObjects, + }, importPaths: { customScalarCodecs: input.importPaths?.customScalarCodecs ?? Path.join(process.cwd(), `customScalarCodecs.js`), }, @@ -63,8 +81,9 @@ export const resolveOptions = (input: Input): Config => { scalars: input.libraryPaths?.scalars ?? `graphql-request/alpha/schema/scalars`, schema: input.libraryPaths?.schema ?? `graphql-request/alpha/schema`, }, - typeMapByKind: getTypeMapByKind(schema), + typeMapByKind, options: { + errorTypeNamePattern, customScalars: input.options?.customScalars ?? false, TSDoc: { noDocPolicy: input.options?.TSDoc?.noDocPolicy ?? `ignore`, @@ -84,6 +103,7 @@ export const generateCode = (input: Input) => { const config = resolveOptions(input) return [ + generateError, generateIndex, generateScalar, generateSchemaBuildtime, diff --git a/src/generator/files.test.ts b/src/generator/files.test.ts index 20af3276c..810d01731 100644 --- a/src/generator/files.test.ts +++ b/src/generator/files.test.ts @@ -2,6 +2,9 @@ import { readFile } from 'fs/promises' import { expect, test } from 'vitest' test(`generates types from GraphQL SDL file`, async () => { + expect( + await readFile(`./tests/ts/_/schema/generated/Error.ts`, `utf8`), + ).toMatchSnapshot() expect( await readFile(`./tests/ts/_/schema/generated/Select.ts`, `utf8`), ).toMatchSnapshot() @@ -20,6 +23,9 @@ test(`generates types from GraphQL SDL file`, async () => { }) test(`schema2`, async () => { + expect( + await readFile(`./tests/_/schema/generated/Error.ts`, `utf8`), + ).toMatchSnapshot() expect( await readFile(`./tests/_/schema/generated/Select.ts`, `utf8`), ).toMatchSnapshot() diff --git a/src/generator/files.ts b/src/generator/files.ts index 6b5443fea..265b0f1d4 100644 --- a/src/generator/files.ts +++ b/src/generator/files.ts @@ -4,6 +4,7 @@ import _ from 'json-bigint' import fs from 'node:fs/promises' import * as Path from 'node:path' import { fileExists } from '../lib/prelude.js' +import type { OptionsInput } from './code/generateCode.js' import { generateCode, type Input as GenerateInput } from './code/generateCode.js' export interface Input { @@ -13,6 +14,7 @@ export interface Input { sourceCustomScalarCodecsFilePath?: string schemaPath?: string format?: boolean + errorTypeNamePattern?: OptionsInput['errorTypeNamePattern'] } export const generateFiles = async (input: Input) => { @@ -39,6 +41,7 @@ export const generateFiles = async (input: Input) => { options: { formatter: typeScriptFormatter, customScalars: customScalarCodecsPathExists, + errorTypeNamePattern: input.errorTypeNamePattern, }, }) diff --git a/tests/_/db.ts b/tests/_/db.ts index 86cd87585..047a8cb4f 100644 --- a/tests/_/db.ts +++ b/tests/_/db.ts @@ -1,4 +1,6 @@ export const db = { + ErrorOne: { message: `errorOne`, infoId: `abc` }, + ErrorTwo: { message: `errorOne`, infoInt: 123 }, int: 123, id: `abc`, id1: `abc`, @@ -10,7 +12,7 @@ export const db = { bar: { int: 123, }, - object1: { + Object1: { string: `abc`, int: 123, float: 123.456, diff --git a/tests/_/schema/generated/Error.ts b/tests/_/schema/generated/Error.ts new file mode 100644 index 000000000..116c46f0b --- /dev/null +++ b/tests/_/schema/generated/Error.ts @@ -0,0 +1,17 @@ +type Include = Exclude> + +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) +} diff --git a/tests/_/schema/generated/Index.ts b/tests/_/schema/generated/Index.ts index 6c85f299e..c11d0b086 100644 --- a/tests/_/schema/generated/Index.ts +++ b/tests/_/schema/generated/Index.ts @@ -11,6 +11,8 @@ 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 @@ -18,8 +20,16 @@ export interface Index { } 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 + } + } } diff --git a/tests/_/schema/generated/SchemaBuildtime.ts b/tests/_/schema/generated/SchemaBuildtime.ts index aa9f4cb61..139c7739f 100644 --- a/tests/_/schema/generated/SchemaBuildtime.ts +++ b/tests/_/schema/generated/SchemaBuildtime.ts @@ -40,6 +40,12 @@ export namespace Root { string: $.Input.Nullable<$Scalar.String> }> > + result: $.Field< + $.Output.Nullable, + $.Args<{ + case: Enum.Case + }> + > unionFooBar: $.Field<$.Output.Nullable, null> }> } @@ -49,7 +55,7 @@ export namespace Root { // ------------------------------------------------------------ // export namespace Enum { - // -- no types -- + export type Case = $.Enum<'Case', ['ErrorOne', 'ErrorTwo', 'Object1']> } // ------------------------------------------------------------ // @@ -65,6 +71,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]> @@ -83,6 +93,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. */ @@ -123,4 +143,6 @@ 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]> } diff --git a/tests/_/schema/generated/SchemaRuntime.ts b/tests/_/schema/generated/SchemaRuntime.ts index 40628df9b..48a57ea97 100644 --- a/tests/_/schema/generated/SchemaRuntime.ts +++ b/tests/_/schema/generated/SchemaRuntime.ts @@ -3,6 +3,8 @@ 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)), @@ -13,6 +15,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)), @@ -42,6 +56,10 @@ export const Object2ImplementingInterface = $.Object$(`Object2ImplementingInterf // @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, @@ -73,6 +91,7 @@ export const Query = $.Object$(`Query`, { string: $.Input.Nullable($Scalar.String), }), ), + result: $.field($.Output.Nullable(() => Result), $.Args({ case: Case })), unionFooBar: $.field($.Output.Nullable(() => FooBarUnion)), }) @@ -85,6 +104,8 @@ export const $Index = { objects: { Bar, DateObject1, + ErrorOne, + ErrorTwo, Foo, Object1, Object1ImplementingInterface, @@ -92,8 +113,16 @@ export const $Index = { }, unions: { FooBarUnion, + Result, }, interfaces: { + Error, Interface, }, + error: { + objects: { + ErrorOne, + ErrorTwo, + }, + }, } diff --git a/tests/_/schema/generated/Select.ts b/tests/_/schema/generated/Select.ts index e25806010..61e6e78a6 100644 --- a/tests/_/schema/generated/Select.ts +++ b/tests/_/schema/generated/Select.ts @@ -28,6 +28,12 @@ export type Bar<$SelectionSet extends SelectionSet.Object> = ResultSet.Object$<$SelectionSet, Index['objects']['DateObject1'], Index> +export type ErrorOne<$SelectionSet extends SelectionSet.Object> = + ResultSet.Object$<$SelectionSet, Index['objects']['ErrorOne'], Index> + +export type ErrorTwo<$SelectionSet extends SelectionSet.Object> = + ResultSet.Object$<$SelectionSet, Index['objects']['ErrorTwo'], Index> + export type Foo<$SelectionSet extends SelectionSet.Object> = ResultSet.Object$< $SelectionSet, Index['objects']['Foo'], @@ -54,8 +60,17 @@ export type Object2ImplementingInterface< export type FooBarUnion<$SelectionSet extends SelectionSet.Union> = ResultSet.Union<$SelectionSet, Index['unions']['FooBarUnion'], Index> +export type Result<$SelectionSet extends SelectionSet.Union> = ResultSet.Union< + $SelectionSet, + Index['unions']['Result'], + Index +> + // Interface Types // --------------- +export type Error<$SelectionSet extends SelectionSet.Interface> = + ResultSet.Interface<$SelectionSet, Index['interfaces']['Error'], Index> + export type Interface<$SelectionSet extends SelectionSet.Interface> = ResultSet.Interface<$SelectionSet, Index['interfaces']['Interface'], Index> diff --git a/tests/_/schema/schema.graphql b/tests/_/schema/schema.graphql index c0a64c12f..41a570fcf 100644 --- a/tests/_/schema/schema.graphql +++ b/tests/_/schema/schema.graphql @@ -2,6 +2,12 @@ type Bar { int: Int } +enum Case { + ErrorOne + ErrorTwo + Object1 +} + """ A date-time string at UTC, such as 2007-12-03T10:15:30Z, compliant with the `date-time` format outlined in section 5.6 of the RFC 3339 profile of the ISO 8601 standard for representation of dates and times using the Gregorian calendar.This scalar is serialized to a string in ISO 8601 format and parsed from a string in ISO 8601 format. """ @@ -11,6 +17,20 @@ type DateObject1 { date1: Date } +interface Error { + message: String! +} + +type ErrorOne implements Error { + infoId: ID + message: String! +} + +type ErrorTwo implements Error { + infoInt: Int + message: String! +} + """Object documentation.""" type Foo { """Field documentation.""" @@ -57,5 +77,8 @@ type Query { idNonNull: ID! interface: Interface objectWithArgs(boolean: Boolean, float: Float, id: ID, int: Int, string: String): Object1 + result(case: Case!): Result unionFooBar: FooBarUnion -} \ No newline at end of file +} + +union Result = ErrorOne | ErrorTwo | Object1 \ No newline at end of file diff --git a/tests/_/schema/schema.ts b/tests/_/schema/schema.ts index afab072ea..9140b75d5 100644 --- a/tests/_/schema/schema.ts +++ b/tests/_/schema/schema.ts @@ -92,8 +92,44 @@ builder.simpleObject(`Object2ImplementingInterface`, { }), }) +const ErrorInterface = builder.simpleInterface(`Error`, { + fields: t => ({ + message: t.string({ nullable: false }), + }), +}) + +const ErrorOne = builder.simpleObject(`ErrorOne`, { + interfaces: [ErrorInterface], + fields: t => ({ + infoId: t.id(), + }), +}) + +const ErrorTwo = builder.simpleObject(`ErrorTwo`, { + interfaces: [ErrorInterface], + fields: t => ({ + infoInt: t.int(), + }), +}) + +const Result = builder.unionType(`Result`, { + types: [Object1, ErrorOne, ErrorTwo], + resolveType: (data) => { + return `infoId` in data ? `ErrorOne` : `infoInt` in data ? `ErrorTwo` : `Object1` + }, +}) + +const ResultCase = builder.enumType(`Case`, { values: [`Object1`, `ErrorOne`, `ErrorTwo`] as const }) + builder.queryType({ fields: t => ({ + result: t.field({ + args: { case: t.arg({ type: ResultCase, required: true }) }, + type: Result, + resolve: (_, args) => { + return db[args.case] + }, + }), // Custom Scalar date: t.field({ type: `Date`, resolve: () => db.date0 }), dateNonNull: t.field({ nullable: false, type: `Date`, resolve: () => db.date0 }), @@ -117,7 +153,7 @@ builder.queryType({ id: t.arg.id(), }, type: Object1, - resolve: (_, args) => ({ ...db.object1, ...args }), + resolve: (_, args) => ({ ...db.Object1, ...args }), }), interface: t.field({ type: Interface, diff --git a/tests/_/schemaGenerate.ts b/tests/_/schemaGenerate.ts index fa16d8336..5cd085407 100644 --- a/tests/_/schemaGenerate.ts +++ b/tests/_/schemaGenerate.ts @@ -2,12 +2,15 @@ import type { GraphQLSchema } from 'graphql' import { printSchema } from 'graphql' import fs from 'node:fs/promises' import { dirname, join } from 'node:path' +import type { OptionsInput } from '../../src/generator/code/generateCode.js' import { generateFiles } from '../../src/generator/files.js' import { schema as schema } from './schema/schema.js' import { schema as schemaMutationOnly } from './schemaMutationOnly/schema.js' import { schema as schemaQueryOnly } from './schemaQueryOnly/schema.js' -const generate = async ({ schema, outputSchemaPath }: { schema: GraphQLSchema; outputSchemaPath: string }) => { +const generate = async ( + { schema, outputSchemaPath, options }: { schema: GraphQLSchema; outputSchemaPath: string; options?: OptionsInput }, +) => { const sourceDirPath = dirname(outputSchemaPath) await fs.writeFile( outputSchemaPath, @@ -23,6 +26,7 @@ const generate = async ({ schema, outputSchemaPath }: { schema: GraphQLSchema; o scalars: `../../../../src/Schema/Hybrid/types/Scalar/Scalar.js`, }, }, + ...options, }) } @@ -39,6 +43,7 @@ await generate({ await generate({ schema, outputSchemaPath: `./tests/_/schema/schema.graphql`, + options: { errorTypeNamePattern: /^Error.+/ }, }) await generateFiles({ diff --git a/tests/_/schemaMutationOnly/generated/Error.ts b/tests/_/schemaMutationOnly/generated/Error.ts new file mode 100644 index 000000000..6a480e85c --- /dev/null +++ b/tests/_/schemaMutationOnly/generated/Error.ts @@ -0,0 +1,14 @@ +type Include = Exclude> + +type ObjectWithTypeName = { __typename: string } + +const ErrorObjectsTypeNameSelectedEnum = {} as Record + +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) +} diff --git a/tests/_/schemaMutationOnly/generated/Index.ts b/tests/_/schemaMutationOnly/generated/Index.ts index d1a54c73d..883eed113 100644 --- a/tests/_/schemaMutationOnly/generated/Index.ts +++ b/tests/_/schemaMutationOnly/generated/Index.ts @@ -11,4 +11,7 @@ export interface Index { objects: {} unions: {} interfaces: {} + error: { + objects: {} + } } diff --git a/tests/_/schemaMutationOnly/generated/SchemaRuntime.ts b/tests/_/schemaMutationOnly/generated/SchemaRuntime.ts index 0c4b8a931..2720357b9 100644 --- a/tests/_/schemaMutationOnly/generated/SchemaRuntime.ts +++ b/tests/_/schemaMutationOnly/generated/SchemaRuntime.ts @@ -18,4 +18,7 @@ export const $Index = { objects: {}, unions: {}, interfaces: {}, + error: { + objects: {}, + }, } diff --git a/tests/_/schemaQueryOnly/generated/Error.ts b/tests/_/schemaQueryOnly/generated/Error.ts new file mode 100644 index 000000000..6a480e85c --- /dev/null +++ b/tests/_/schemaQueryOnly/generated/Error.ts @@ -0,0 +1,14 @@ +type Include = Exclude> + +type ObjectWithTypeName = { __typename: string } + +const ErrorObjectsTypeNameSelectedEnum = {} as Record + +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) +} diff --git a/tests/_/schemaQueryOnly/generated/Index.ts b/tests/_/schemaQueryOnly/generated/Index.ts index 650cb8494..4ac156d9a 100644 --- a/tests/_/schemaQueryOnly/generated/Index.ts +++ b/tests/_/schemaQueryOnly/generated/Index.ts @@ -11,4 +11,7 @@ export interface Index { objects: {} unions: {} interfaces: {} + error: { + objects: {} + } } diff --git a/tests/_/schemaQueryOnly/generated/SchemaRuntime.ts b/tests/_/schemaQueryOnly/generated/SchemaRuntime.ts index dd8dedd20..8b983dc21 100644 --- a/tests/_/schemaQueryOnly/generated/SchemaRuntime.ts +++ b/tests/_/schemaQueryOnly/generated/SchemaRuntime.ts @@ -18,4 +18,7 @@ export const $Index = { objects: {}, unions: {}, interfaces: {}, + error: { + objects: {}, + }, } diff --git a/tests/ts/_/schema/generated/Error.ts b/tests/ts/_/schema/generated/Error.ts new file mode 100644 index 000000000..6a480e85c --- /dev/null +++ b/tests/ts/_/schema/generated/Error.ts @@ -0,0 +1,14 @@ +type Include = Exclude> + +type ObjectWithTypeName = { __typename: string } + +const ErrorObjectsTypeNameSelectedEnum = {} as Record + +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) +} diff --git a/tests/ts/_/schema/generated/Index.ts b/tests/ts/_/schema/generated/Index.ts index 62ee03acd..259ef4c2f 100644 --- a/tests/ts/_/schema/generated/Index.ts +++ b/tests/ts/_/schema/generated/Index.ts @@ -30,4 +30,7 @@ export interface Index { DateInterface1: Schema.Interface.DateInterface1 Interface: Schema.Interface.Interface } + error: { + objects: {} + } } diff --git a/tests/ts/_/schema/generated/SchemaRuntime.ts b/tests/ts/_/schema/generated/SchemaRuntime.ts index e4f1695e2..1f1170e3f 100644 --- a/tests/ts/_/schema/generated/SchemaRuntime.ts +++ b/tests/ts/_/schema/generated/SchemaRuntime.ts @@ -212,4 +212,7 @@ export const $Index = { DateInterface1, Interface, }, + error: { + objects: {}, + }, }