From 7d49caff03aaad96e8c1eb69f347f848f2e96acd Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Mon, 15 Apr 2024 18:39:28 +0200 Subject: [PATCH 01/12] feat(ts-client): document method --- package.json | 4 +- pnpm-lock.yaml | 11 ++ .../SelectionSet/toGraphQLDocumentString.ts | 8 +- src/client/client.document.test.ts | 29 +++++ src/client/client.ts | 108 ++++++++++++++---- src/client/document.ts | 19 +++ .../__snapshots__/files.test.ts.snap | 94 ++++++++++++++- src/generator/code/index.ts | 11 +- src/generator/code/schemaRuntime.ts | 5 +- src/generator/files.test.ts | 22 ++++ src/lib/Code.ts | 5 +- tests/_/schema/generated/Index.ts | 14 +++ tests/_/schema/generated/Scalar.ts | 6 + tests/_/schema/generated/SchemaBuildtime.ts | 52 +++++++++ tests/_/schema/generated/SchemaRuntime.ts | 20 ++++ tests/_/schema/schema.graphql | 3 + tests/_/schema/schema.ts | 21 ++++ tests/_/schema/schemaGenerate.ts | 8 ++ tests/ts/_/schema/generated/Index.ts | 2 + tests/ts/_/schema/generated/SchemaRuntime.ts | 2 + 20 files changed, 408 insertions(+), 36 deletions(-) create mode 100644 src/client/client.document.test.ts create mode 100644 src/client/document.ts create mode 100644 tests/_/schema/generated/Index.ts create mode 100644 tests/_/schema/generated/Scalar.ts create mode 100644 tests/_/schema/generated/SchemaBuildtime.ts create mode 100644 tests/_/schema/generated/SchemaRuntime.ts create mode 100644 tests/_/schema/schema.graphql create mode 100644 tests/_/schema/schema.ts create mode 100644 tests/_/schema/schemaGenerate.ts diff --git a/package.json b/package.json index 10348ab06..a992c4e65 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,8 @@ }, "homepage": "https://github.com/jasonkuhrt/graphql-request", "scripts": { - "demo": "tsx src/cli/generate.ts && dprint fmt src/demo.ts", + "gen:test:schema": "tsx tests/_/schema/schema.ts", + "demo": "tsx src/cli/generateSchema.ts && dprint fmt src/demo.ts", "dev": "rm -rf dist && tsc --watch", "format": "pnpm build:docs && dprint fmt", "lint": "eslint . --fix", @@ -89,6 +90,7 @@ "graphql": "14 - 16" }, "devDependencies": { + "@pothos/core": "^3.41.0", "@tsconfig/node16": "^16.1.3", "@types/body-parser": "^1.19.5", "@types/express": "^4.17.21", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f0b30684..b1f4e6129 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,6 +25,9 @@ dependencies: version: 3.22.4 devDependencies: + '@pothos/core': + specifier: ^3.41.0 + version: 3.41.0(graphql@16.8.1) '@tsconfig/node16': specifier: ^16.1.3 version: 16.1.3 @@ -1226,6 +1229,14 @@ packages: '@octokit/openapi-types': 12.11.0 dev: true + /@pothos/core@3.41.0(graphql@16.8.1): + resolution: {integrity: sha512-Nb7uPDTXVjdrWqHs5aoD1r6JEdQ9FnJYlf7gv47o1b/bb8rVDAZQaviVvaChal7YQcyFGgCFb0/YNNHLNBEjNw==} + peerDependencies: + graphql: '>=15.1.0' + dependencies: + graphql: 16.8.1 + dev: true + /@protobufjs/aspromise@1.1.2: resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} dev: true diff --git a/src/client/SelectionSet/toGraphQLDocumentString.ts b/src/client/SelectionSet/toGraphQLDocumentString.ts index d29d5c194..762bc2eb9 100644 --- a/src/client/SelectionSet/toGraphQLDocumentString.ts +++ b/src/client/SelectionSet/toGraphQLDocumentString.ts @@ -24,11 +24,13 @@ type SS = { } & SpecialFields export const toGraphQLDocumentString = (ss: GraphQLDocumentObject) => { - let docString = `` - docString += `query { + return `query ${toGraphQLDocumentSelectionSet(ss)}` +} + +export const toGraphQLDocumentSelectionSet = (ss: GraphQLDocumentObject) => { + return `{ ${selectionSet(ss)} }` - return docString } const directiveArgs = (config: object) => { diff --git a/src/client/client.document.test.ts b/src/client/client.document.test.ts new file mode 100644 index 000000000..0f5e67255 --- /dev/null +++ b/src/client/client.document.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from 'vitest' +import type { Index } from '../../tests/_/schema/generated/Index.js' +import { $Index } from '../../tests/_/schema/generated/SchemaRuntime.js' +// import { setupMockServer } from '../../tests/raw/__helpers.js' +import { schema } from '../../tests/_/schema/schema.js' +import { create } from './client.js' +import { toDocumentExpression } from './document.js' + +// const ctx = setupMockServer() +const data = { id: `abc` } + +// todo different error now +// @ts-expect-error infinite depth +const client = () => create({ schema, schemaIndex: $Index }) + +test(`document`, async () => { + // const mockRes = ctx.res({ body: { data } }).spec.body! + console.log(toDocumentExpression({ + $run: `foo`, + query: { id: true }, + query_foo: { id: true }, + })) + const result = await client().document({ + // $run: `foo`, + query: { date: true }, + query_foo: { date: true }, + }) + expect(result).toEqual(data) +}) diff --git a/src/client/client.ts b/src/client/client.ts index 7e9a13b51..20e9faee2 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -1,30 +1,68 @@ -import type { DocumentNode } from 'graphql' +import { type DocumentNode, execute, graphql, type GraphQLSchema } from 'graphql' import type { ExcludeUndefined } from 'type-fest/source/required-deep.js' import request from '../entrypoints/main.js' import { type RootTypeName } from '../lib/graphql.js' import type { Exact } from '../lib/prelude.js' import type { Object$2, Schema } from '../Schema/__.js' import * as CustomScalars from './customScalars.js' +import { toDocumentExpression } from './document.js' import type { ResultSet } from './ResultSet/__.js' import { SelectionSet } from './SelectionSet/__.js' import type { GraphQLDocumentObject } from './SelectionSet/toGraphQLDocumentString.js' +type Variables = Record // todo or any custom scalars too + +// dprint-ignore +type RootMethod<$Index extends Schema.Index, $RootTypeName extends Schema.RootTypeName> = + <$SelectionSet extends object>(selectionSet: Exact<$SelectionSet, SelectionSet.Root<$Index, $RootTypeName>>) => + Promise> + +// dprint-ignore +type ObjectMethod<$Index extends Schema.Index, $ObjectName extends keyof $Index['objects']> = + <$SelectionSet extends object>(selectionSet: Exact<$SelectionSet, SelectionSet.Object<$Index['objects'][$ObjectName], $Index>>) => + Promise> + // dprint-ignore type RootTypeMethods<$Index extends Schema.Index, $RootTypeName extends Schema.RootTypeName> = & { - $batch: <$SelectionSet extends object>(selectionSet: Exact<$SelectionSet, SelectionSet.Root<$Index, $RootTypeName>>) => - Promise> + $batch: RootMethod<$Index, $RootTypeName> } // todo test this & { - [$ObjectName in keyof $Index['objects']]: <$SelectionSet extends object>(selectionSet: Exact<$SelectionSet, SelectionSet.Object<$Index['objects'][$ObjectName], $Index>>) => - Promise> + [$ObjectName in keyof $Index['objects']]: ObjectMethod<$Index, $ObjectName> } +// dprint-ignore +type Document<$Index extends Schema.Index, Names extends string> = { + // todo should not be required? Only if >1? Even then only of no query for query? + $run: Names + query?: SelectionSet.Root<$Index, 'Query'> + [namedQuery: `query_${string}`]: SelectionSet.Root<$Index, 'Query'> + mutation?: SelectionSet.Root<$Index, 'Mutation'> + [namedMutation: `mutation_${string}`]: SelectionSet.Root<$Index, 'Mutation'> + // todo + // subscription?: RootMethod<$Index, 'Subscription'> + // [namedSubscription: `subscription_${string}`]: RootMethod<$Index, 'Subscription'> +} + +type Infer = Infer_ + +// todo the $name below should be limited to a valid graphql root type name +// dprint-ignore +type Infer_ = + T extends 'query' ? 'query' : + T extends 'mutation' ? 'mutation' : + T extends `query_${infer $Name}` ? $Name : + T extends `mutation_${infer $Name}` ? $Name : + never + // dprint-ignore export type Client<$Index extends Schema.Index> = & { - raw: (document: DocumentNode, variables?:object) => Promise + // todo test raw + raw: (document: string|DocumentNode, variables?:Variables) => Promise + // todo test + document: <$Document extends Document<$Index, Infer<$Document>>>(document: $Document) => Promise } & ( $Index['Root']['Query'] extends null @@ -53,7 +91,7 @@ interface HookInputDocumentEncode { } interface Input { - url: URL | string + schema: URL | string | GraphQLSchema headers?: HeadersInit // If there are no custom scalars then this property is useless. Improve types. schemaIndex: Schema.Index @@ -76,7 +114,36 @@ export const create = <$SchemaIndex extends Schema.Index>(input: Input): Client< return parentInput.hooks?.[name](input, fn) ?? fn(input) } - const sendDocumentObject = (rootType: RootTypeName) => async (documentObject: GraphQLDocumentObject) => { + const executeDocumentExpression = async (document: string | DocumentNode, variables?: Variables) => { + if (input.schema instanceof URL || typeof input.schema === `string`) { + return await request({ + url: new URL(input.schema).href, // todo allow relative urls - what does fetch in node do? + requestHeaders: input.headers, + document, + variables, + }) + } else { + if (typeof document === `string`) { + return await graphql({ + schema: input.schema, + source: document, + // contextValue: createContextValue(), // todo + variableValues: variables, + // operationName: input.operationName, + }) + } else if (typeof document === `object`) { + return await execute({ + schema: input.schema, + document, + // contextValue: createContextValue(), // todo + variableValues: variables, + // operationName: input.operationName, + }) + } + } + } + + const executeDocumentObject = (rootType: RootTypeName) => async (documentObject: GraphQLDocumentObject) => { const rootIndex = input.schemaIndex.Root[rootType] if (!rootIndex) throw new Error(`Root type not found: ${rootType}`) @@ -86,12 +153,8 @@ export const create = <$SchemaIndex extends Schema.Index>(input: Input): Client< ({ rootIndex, documentObject }) => CustomScalars.encode({ index: rootIndex, documentObject }), ) const documentString = SelectionSet.toGraphQLDocumentString(documentObjectEncoded) - const result = await request({ - url: new URL(input.url).href, - requestHeaders: input.headers, - document: documentString, - // todo handle variables - }) + // todo variables + const result = await executeDocumentExpression(documentString) const resultDecoded = CustomScalars.decode(rootIndex, result as object) return resultDecoded } @@ -99,19 +162,20 @@ export const create = <$SchemaIndex extends Schema.Index>(input: Input): Client< // @ts-expect-error ignoreme const client: Client<$SchemaIndex> = { raw: async (document, variables) => { - return await request({ - url: new URL(input.url).href, - requestHeaders: input.headers, - document, - variables, - }) + return await executeDocumentExpression(document, variables) + }, + + document: async (documentObject) => { + const documentString = toDocumentExpression(documentObject as any) + // todo variables + return await executeDocumentExpression(documentString) }, query: { - $batch: sendDocumentObject(`Query`), + $batch: executeDocumentObject(`Query`), // todo proxy that allows calling any query field }, mutation: { - $batch: sendDocumentObject(`Mutation`), + $batch: executeDocumentObject(`Mutation`), // todo proxy that allows calling any mutation field }, // todo diff --git a/src/client/document.ts b/src/client/document.ts new file mode 100644 index 000000000..f9f422d81 --- /dev/null +++ b/src/client/document.ts @@ -0,0 +1,19 @@ +import { SelectionSet } from './SelectionSet/__.js' +import type { GraphQLDocumentObject } from './SelectionSet/toGraphQLDocumentString.js' + +export const toDocumentExpression = (document: { $run?: string } & { [operation: string]: GraphQLDocumentObject }) => { + const { $run: _, ...documentWithout$run } = document + return Object.entries(documentWithout$run).map(([rootName, rootDocument]) => { + const documentString = SelectionSet.toGraphQLDocumentSelectionSet(rootDocument) + const splitIndex = rootName.search(`_`) + const operationType = splitIndex === -1 ? rootName : rootName.slice(0, splitIndex) + if (!(operationType in operationTypes)) throw new Error(`Invalid operation type: ${operationType}`) + const operationName = splitIndex === -1 ? `` : rootName.slice(splitIndex + 1) + return `${operationType} ${operationName} ${documentString}` + }).join(`\n\n`) +} + +const operationTypes = { + query: `query`, + mutation: `mutation`, +} diff --git a/src/generator/__snapshots__/files.test.ts.snap b/src/generator/__snapshots__/files.test.ts.snap index a99b69969..65c17108e 100644 --- a/src/generator/__snapshots__/files.test.ts.snap +++ b/src/generator/__snapshots__/files.test.ts.snap @@ -299,7 +299,9 @@ export * from '../customScalarCodecs.js' `; exports[`generates types from GraphQL SDL file 3`] = ` -"import * as $ from '../../../../../src/Schema/__.js' +"/* eslint-disable */ + +import * as $ from '../../../../../src/Schema/__.js' import * as $Scalar from './Scalar.js' export const ABCEnum = $.Enum(\`ABCEnum\`, [\`A\`, \`B\`, \`C\`]) @@ -523,3 +525,93 @@ export const $Index = { } " `; + +exports[`schema2 1`] = ` +"import type * as $ from '../../../../src/Schema/__.js' +import type * as $Scalar from './Scalar.ts' + +// ------------------------------------------------------------ // +// Root // +// ------------------------------------------------------------ // + +export namespace Root { + export type Query = $.Object$2<'Query', { + id: $.Field<$.Output.Nullable<$Scalar.ID>, null> + }> +} + +// ------------------------------------------------------------ // +// Enum // +// ------------------------------------------------------------ // + +export namespace Enum { + // -- no types -- +} + +// ------------------------------------------------------------ // +// InputObject // +// ------------------------------------------------------------ // + +export namespace InputObject { + // -- no types -- +} + +// ------------------------------------------------------------ // +// Interface // +// ------------------------------------------------------------ // + +export namespace Interface { + // -- no types -- +} + +// ------------------------------------------------------------ // +// Object // +// ------------------------------------------------------------ // + +export namespace Object { + // -- no types -- +} + +// ------------------------------------------------------------ // +// Union // +// ------------------------------------------------------------ // + +export namespace Union { + // -- no types -- +} +" +`; + +exports[`schema2 2`] = ` +"declare global { + interface SchemaCustomScalars { + } +} + +export * from '../../../../src/Schema/Hybrid/types/Scalar/Scalar.js' +" +`; + +exports[`schema2 3`] = ` +"/* eslint-disable */ + +import * as $ from '../../../../src/Schema/__.js' +import * as $Scalar from './Scalar.js' + +// eslint-disable-next-line +// @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 Query = $.Object$(\`Query\`, { + id: $.field($.Output.Nullable($Scalar.ID)), +}) + +export const $Index = { + Root: { + Query, + Mutation: null, + Subscription: null, + }, + objects: {}, + unions: {}, +} +" +`; diff --git a/src/generator/code/index.ts b/src/generator/code/index.ts index 1960a56e6..09dfedcca 100644 --- a/src/generator/code/index.ts +++ b/src/generator/code/index.ts @@ -4,9 +4,10 @@ import type { Config } from './code.js' export const generateIndex = (config: Config) => { const namespace = `Schema` - let code = `` - code += `import type * as ${namespace} from './SchemaBuildtime.js'\n\n` - code += Code.export$( + const code = [] + code.push(`/* eslint-disable */\n`) + code.push(`import type * as ${namespace} from './SchemaBuildtime.js'\n`) + code.push(Code.export$( Code.interface$( `Index`, Code.objectFrom({ @@ -28,7 +29,7 @@ export const generateIndex = (config: Config) => { ), }), ), - ) + )) - return code + return code.join(`\n`) } diff --git a/src/generator/code/schemaRuntime.ts b/src/generator/code/schemaRuntime.ts index ee4f25523..2d2f5f267 100644 --- a/src/generator/code/schemaRuntime.ts +++ b/src/generator/code/schemaRuntime.ts @@ -27,6 +27,7 @@ export const generateRuntimeSchema = ( ) => { const code: string[] = [] + code.push(`/* eslint-disable */\n`) code.push( ` import * as $ from '${config.libraryPaths.schema}' @@ -60,10 +61,10 @@ const index = (config: Config) => { Subscription ${hasSubscription(config.typeMapByKind) ? `` : `:null`} }, objects: { - ${config.typeMapByKind.GraphQLObjectType.map(type => type.name).join(`,\n`)}, + ${config.typeMapByKind.GraphQLObjectType.map(type => type.name).join(`,\n`)} }, unions: { - ${config.typeMapByKind.GraphQLUnionType.map(type => type.name).join(`,\n`)}, + ${config.typeMapByKind.GraphQLUnionType.map(type => type.name).join(`,\n`)} } } ` diff --git a/src/generator/files.test.ts b/src/generator/files.test.ts index 7e81dfbfe..c15abcca8 100644 --- a/src/generator/files.test.ts +++ b/src/generator/files.test.ts @@ -23,3 +23,25 @@ test(`generates types from GraphQL SDL file`, async () => { await readFile(`./tests/ts/_/schema/generated/SchemaRuntime.ts`, `utf8`), ).toMatchSnapshot() }) + +test(`schema2`, async () => { + await generateFiles({ + sourceDirPath: `./tests/_/schema`, + outputDirPath: `./tests/_/schema/generated`, + code: { + libraryPaths: { + schema: `../../../../src/Schema/__.js`, + scalars: `../../../../src/Schema/Hybrid/types/Scalar/Scalar.js`, + }, + }, + }) + expect( + await readFile(`./tests/_/schema/generated/SchemaBuildtime.ts`, `utf8`), + ).toMatchSnapshot() + expect( + await readFile(`./tests/_/schema/generated/Scalar.ts`, `utf8`), + ).toMatchSnapshot() + expect( + await readFile(`./tests/_/schema/generated/SchemaRuntime.ts`, `utf8`), + ).toMatchSnapshot() +}) diff --git a/src/lib/Code.ts b/src/lib/Code.ts index 4bcc8af85..0ecb00a7c 100644 --- a/src/lib/Code.ts +++ b/src/lib/Code.ts @@ -21,8 +21,8 @@ export namespace Code { string, null | string | boolean | number | { type: null | string | boolean | number; optional?: boolean; tsdoc?: string } >, - ) => - Code.object( + ) => { + return Code.object( Code.fields( Object.entries(object).map(([name, spec]) => [name, spec && typeof spec === `object` ? spec : { type: spec }] as const @@ -32,6 +32,7 @@ export namespace Code { ) => Code.field(name, String(spec.type), { optional: spec.optional })), ), ) + } export const type = (name: string, type: string) => `type ${name} = ${type}` export const interface$ = (name: string, object: string) => `interface ${name} ${object}` export const export$ = (thing: string) => `export ${thing}` diff --git a/tests/_/schema/generated/Index.ts b/tests/_/schema/generated/Index.ts new file mode 100644 index 000000000..650cb8494 --- /dev/null +++ b/tests/_/schema/generated/Index.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ + +import type * as Schema from './SchemaBuildtime.js' + +export interface Index { + Root: { + Query: Schema.Root.Query + Mutation: null + Subscription: null + } + objects: {} + unions: {} + interfaces: {} +} diff --git a/tests/_/schema/generated/Scalar.ts b/tests/_/schema/generated/Scalar.ts new file mode 100644 index 000000000..08647652d --- /dev/null +++ b/tests/_/schema/generated/Scalar.ts @@ -0,0 +1,6 @@ +declare global { + interface SchemaCustomScalars { + } +} + +export * from '../../../../src/Schema/Hybrid/types/Scalar/Scalar.js' diff --git a/tests/_/schema/generated/SchemaBuildtime.ts b/tests/_/schema/generated/SchemaBuildtime.ts new file mode 100644 index 000000000..2885bc0f3 --- /dev/null +++ b/tests/_/schema/generated/SchemaBuildtime.ts @@ -0,0 +1,52 @@ +import type * as $ from '../../../../src/Schema/__.js' +import type * as $Scalar from './Scalar.ts' + +// ------------------------------------------------------------ // +// Root // +// ------------------------------------------------------------ // + +export namespace Root { + export type Query = $.Object$2<'Query', { + id: $.Field<$.Output.Nullable<$Scalar.ID>, null> + }> +} + +// ------------------------------------------------------------ // +// Enum // +// ------------------------------------------------------------ // + +export namespace Enum { + // -- no types -- +} + +// ------------------------------------------------------------ // +// InputObject // +// ------------------------------------------------------------ // + +export namespace InputObject { + // -- no types -- +} + +// ------------------------------------------------------------ // +// Interface // +// ------------------------------------------------------------ // + +export namespace Interface { + // -- no types -- +} + +// ------------------------------------------------------------ // +// Object // +// ------------------------------------------------------------ // + +export namespace Object { + // -- no types -- +} + +// ------------------------------------------------------------ // +// Union // +// ------------------------------------------------------------ // + +export namespace Union { + // -- no types -- +} diff --git a/tests/_/schema/generated/SchemaRuntime.ts b/tests/_/schema/generated/SchemaRuntime.ts new file mode 100644 index 000000000..1c7aa00b1 --- /dev/null +++ b/tests/_/schema/generated/SchemaRuntime.ts @@ -0,0 +1,20 @@ +/* eslint-disable */ + +import * as $ from '../../../../src/Schema/__.js' +import * as $Scalar from './Scalar.js' + +// eslint-disable-next-line +// @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 Query = $.Object$(`Query`, { + id: $.field($.Output.Nullable($Scalar.ID)), +}) + +export const $Index = { + Root: { + Query, + Mutation: null, + Subscription: null, + }, + objects: {}, + unions: {}, +} diff --git a/tests/_/schema/schema.graphql b/tests/_/schema/schema.graphql new file mode 100644 index 000000000..0ed600e65 --- /dev/null +++ b/tests/_/schema/schema.graphql @@ -0,0 +1,3 @@ +type Query { + id: ID +} \ No newline at end of file diff --git a/tests/_/schema/schema.ts b/tests/_/schema/schema.ts new file mode 100644 index 000000000..15b190255 --- /dev/null +++ b/tests/_/schema/schema.ts @@ -0,0 +1,21 @@ +import SchemaBuilder from '@pothos/core' + +const db = { + id1: `abc`, +} + +const builder = new SchemaBuilder<{ + DefaultFieldNullability: true +}>({ + defaultFieldNullability: true, +}) + +builder.queryType({ + fields: t => ({ + id: t.id({ resolve: () => db.id1 }), + }), +}) + +export const schema = builder.toSchema({ + sortSchema: true, +}) diff --git a/tests/_/schema/schemaGenerate.ts b/tests/_/schema/schemaGenerate.ts new file mode 100644 index 000000000..6625da591 --- /dev/null +++ b/tests/_/schema/schemaGenerate.ts @@ -0,0 +1,8 @@ +import { printSchema } from 'graphql' +import fs from 'node:fs/promises' +import { schema } from './schema.js' + +await fs.writeFile( + `./tests/_/schema/schema.graphql`, + printSchema(schema), +) diff --git a/tests/ts/_/schema/generated/Index.ts b/tests/ts/_/schema/generated/Index.ts index cb0a235a4..62ee03acd 100644 --- a/tests/ts/_/schema/generated/Index.ts +++ b/tests/ts/_/schema/generated/Index.ts @@ -1,3 +1,5 @@ +/* eslint-disable */ + import type * as Schema from './SchemaBuildtime.js' export interface Index { diff --git a/tests/ts/_/schema/generated/SchemaRuntime.ts b/tests/ts/_/schema/generated/SchemaRuntime.ts index a2300614e..d6ff29f33 100644 --- a/tests/ts/_/schema/generated/SchemaRuntime.ts +++ b/tests/ts/_/schema/generated/SchemaRuntime.ts @@ -1,3 +1,5 @@ +/* eslint-disable */ + import * as $ from '../../../../../src/Schema/__.js' import * as $Scalar from './Scalar.js' From 3d35d6aaaef166d298b41c512683a1b9a64b1df1 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 16 Apr 2024 08:39:51 +0200 Subject: [PATCH 02/12] work --- src/client/client.document.test-d.ts | 37 ++++++++++++++ src/client/client.document.test.ts | 30 +++++------- src/client/client.ts | 73 ++++++++++++++++------------ src/client/document.ts | 23 +++++---- tests/_/schema/schema.ts | 2 +- vite.config.ts | 13 +++++ 6 files changed, 114 insertions(+), 64 deletions(-) create mode 100644 src/client/client.document.test-d.ts create mode 100644 vite.config.ts diff --git a/src/client/client.document.test-d.ts b/src/client/client.document.test-d.ts new file mode 100644 index 000000000..a604631a6 --- /dev/null +++ b/src/client/client.document.test-d.ts @@ -0,0 +1,37 @@ +import { describe, expectTypeOf, test } from 'vitest' +import type { Index } from '../../tests/_/schema/generated/Index.js' +import { $Index } from '../../tests/_/schema/generated/SchemaRuntime.js' +import { schema } from '../../tests/_/schema/schema.js' +import { create } from './client.js' + +// todo different error now +// @ts-expect-error infinite depth +const client = create({ schema, schemaIndex: $Index }) + +describe(`input`, () => { + test(`document`, () => { + const run = client.document({ + foo: {query:{ id: true }}, + }).run + expectTypeOf(run).toMatchTypeOf<<$Name extends "foo">(name: $Name) => Promise>() + }) + + test(`document`, () => { + const run = client.document({ + foo: {query:{ id: true }}, + bar: {query:{ id: true }}, + }).run + expectTypeOf(run).toMatchTypeOf<<$Name extends "foo" | "bar">(name: $Name) => Promise>() + }) +}) + +describe(`output`, () => { + test(`document`, () => { + const result = client.document({ + foo: { query:{ id: true }}, + bar: { query:{ id: true }}, + }).run(`foo`) + expectTypeOf(result).toMatchTypeOf>() + }) + // todo mutation test +}) diff --git a/src/client/client.document.test.ts b/src/client/client.document.test.ts index 0f5e67255..ecde6c513 100644 --- a/src/client/client.document.test.ts +++ b/src/client/client.document.test.ts @@ -1,29 +1,21 @@ import { expect, test } from 'vitest' import type { Index } from '../../tests/_/schema/generated/Index.js' import { $Index } from '../../tests/_/schema/generated/SchemaRuntime.js' -// import { setupMockServer } from '../../tests/raw/__helpers.js' -import { schema } from '../../tests/_/schema/schema.js' +import { db, schema } from '../../tests/_/schema/schema.js' import { create } from './client.js' -import { toDocumentExpression } from './document.js' - -// const ctx = setupMockServer() -const data = { id: `abc` } // todo different error now // @ts-expect-error infinite depth -const client = () => create({ schema, schemaIndex: $Index }) +const client = create({ schema, schemaIndex: $Index }) test(`document`, async () => { - // const mockRes = ctx.res({ body: { data } }).spec.body! - console.log(toDocumentExpression({ - $run: `foo`, - query: { id: true }, - query_foo: { id: true }, - })) - const result = await client().document({ - // $run: `foo`, - query: { date: true }, - query_foo: { date: true }, - }) - expect(result).toEqual(data) + const result = await client.document({ + foo: { + query: { id: true }, + }, + bar: { + query: { id: true }, + }, + }).run(`foo`) + expect(result).toEqual({ data: { id: db.id1 } }) }) diff --git a/src/client/client.ts b/src/client/client.ts index 20e9faee2..425244c3e 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -1,4 +1,5 @@ import { type DocumentNode, execute, graphql, type GraphQLSchema } from 'graphql' +import type { MergeExclusive } from 'type-fest' import type { ExcludeUndefined } from 'type-fest/source/required-deep.js' import request from '../entrypoints/main.js' import { type RootTypeName } from '../lib/graphql.js' @@ -32,37 +33,33 @@ type RootTypeMethods<$Index extends Schema.Index, $RootTypeName extends Schema.R [$ObjectName in keyof $Index['objects']]: ObjectMethod<$Index, $ObjectName> } +// todo the name below should be limited to a valid graphql root type name // dprint-ignore -type Document<$Index extends Schema.Index, Names extends string> = { - // todo should not be required? Only if >1? Even then only of no query for query? - $run: Names - query?: SelectionSet.Root<$Index, 'Query'> - [namedQuery: `query_${string}`]: SelectionSet.Root<$Index, 'Query'> - mutation?: SelectionSet.Root<$Index, 'Mutation'> - [namedMutation: `mutation_${string}`]: SelectionSet.Root<$Index, 'Mutation'> - // todo - // subscription?: RootMethod<$Index, 'Subscription'> - // [namedSubscription: `subscription_${string}`]: RootMethod<$Index, 'Subscription'> -} - -type Infer = Infer_ +type Document<$Index extends Schema.Index> = + { + [name: string]: MergeExclusive<{ + query: SelectionSet.Root<$Index, 'Query'> + }, { + mutation: SelectionSet.Root<$Index, 'Mutation'> + }> + } -// todo the $name below should be limited to a valid graphql root type name // dprint-ignore -type Infer_ = - T extends 'query' ? 'query' : - T extends 'mutation' ? 'mutation' : - T extends `query_${infer $Name}` ? $Name : - T extends `mutation_${infer $Name}` ? $Name : - never +type GetOperation = + T extends {query:infer U} ? U : + T extends {mutation:infer U} ? U : + never // dprint-ignore export type Client<$Index extends Schema.Index> = & { // todo test raw raw: (document: string|DocumentNode, variables?:Variables) => Promise - // todo test - document: <$Document extends Document<$Index, Infer<$Document>>>(document: $Document) => Promise + document: <$Document extends Document<$Index>> + (document: Exact<$Document, Document<$Index>>) => + { + run: <$Name extends keyof $Document & string>(name: $Name) => Promise, $Index, 'Query'>> + } } & ( $Index['Root']['Query'] extends null @@ -114,7 +111,13 @@ export const create = <$SchemaIndex extends Schema.Index>(input: Input): Client< return parentInput.hooks?.[name](input, fn) ?? fn(input) } - const executeDocumentExpression = async (document: string | DocumentNode, variables?: Variables) => { + const executeDocumentExpression = async ( + { document, variables, operationName }: { + document: string | DocumentNode + variables?: Variables + operationName?: string + }, + ) => { if (input.schema instanceof URL || typeof input.schema === `string`) { return await request({ url: new URL(input.schema).href, // todo allow relative urls - what does fetch in node do? @@ -129,7 +132,7 @@ export const create = <$SchemaIndex extends Schema.Index>(input: Input): Client< source: document, // contextValue: createContextValue(), // todo variableValues: variables, - // operationName: input.operationName, + operationName, }) } else if (typeof document === `object`) { return await execute({ @@ -137,7 +140,7 @@ export const create = <$SchemaIndex extends Schema.Index>(input: Input): Client< document, // contextValue: createContextValue(), // todo variableValues: variables, - // operationName: input.operationName, + operationName, }) } } @@ -154,7 +157,7 @@ export const create = <$SchemaIndex extends Schema.Index>(input: Input): Client< ) const documentString = SelectionSet.toGraphQLDocumentString(documentObjectEncoded) // todo variables - const result = await executeDocumentExpression(documentString) + const result = await executeDocumentExpression({ document: documentString }) const resultDecoded = CustomScalars.decode(rootIndex, result as object) return resultDecoded } @@ -162,13 +165,19 @@ export const create = <$SchemaIndex extends Schema.Index>(input: Input): Client< // @ts-expect-error ignoreme const client: Client<$SchemaIndex> = { raw: async (document, variables) => { - return await executeDocumentExpression(document, variables) + return await executeDocumentExpression({ document, variables }) }, - - document: async (documentObject) => { - const documentString = toDocumentExpression(documentObject as any) - // todo variables - return await executeDocumentExpression(documentString) + document: (documentObject) => { + return { + run: async (operationName) => { + const documentExpression = toDocumentExpression(documentObject as any) + return await executeDocumentExpression({ + document: documentExpression, + operationName, + // todo variables + }) + }, + } }, query: { $batch: executeDocumentObject(`Query`), diff --git a/src/client/document.ts b/src/client/document.ts index f9f422d81..d08c7225f 100644 --- a/src/client/document.ts +++ b/src/client/document.ts @@ -1,19 +1,18 @@ import { SelectionSet } from './SelectionSet/__.js' import type { GraphQLDocumentObject } from './SelectionSet/toGraphQLDocumentString.js' -export const toDocumentExpression = (document: { $run?: string } & { [operation: string]: GraphQLDocumentObject }) => { - const { $run: _, ...documentWithout$run } = document - return Object.entries(documentWithout$run).map(([rootName, rootDocument]) => { - const documentString = SelectionSet.toGraphQLDocumentSelectionSet(rootDocument) - const splitIndex = rootName.search(`_`) - const operationType = splitIndex === -1 ? rootName : rootName.slice(0, splitIndex) - if (!(operationType in operationTypes)) throw new Error(`Invalid operation type: ${operationType}`) - const operationName = splitIndex === -1 ? `` : rootName.slice(splitIndex + 1) +export const toDocumentExpression = ( + document: Record, +) => { + return Object.entries(document).map(([operationName, operationInput]) => { + const operationType = `query` in operationInput ? `query` : `mutation` + const operation = `query` in operationInput ? operationInput[`query`] : operationInput[`mutation`] + const documentString = SelectionSet.toGraphQLDocumentSelectionSet(operation) return `${operationType} ${operationName} ${documentString}` }).join(`\n\n`) } -const operationTypes = { - query: `query`, - mutation: `mutation`, -} +// const operationTypes = { +// query: `query`, +// mutation: `mutation`, +// } diff --git a/tests/_/schema/schema.ts b/tests/_/schema/schema.ts index 15b190255..6d33b697b 100644 --- a/tests/_/schema/schema.ts +++ b/tests/_/schema/schema.ts @@ -1,6 +1,6 @@ import SchemaBuilder from '@pothos/core' -const db = { +export const db = { id1: `abc`, } diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 000000000..83ff5abd8 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,13 @@ +// This config file is to get around this Vitest bug https://github.com/vitest-dev/vitest/issues/4605 +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + resolve: { + alias: { + 'graphql/language/ast.js': 'graphql/language/ast.js', + 'graphql/language/parser.js': 'graphql/language/parser.js', + 'graphql/language/printer.js': 'graphql/language/printer.js', + graphql: 'graphql/index.js', + }, + }, +}) From ca371ed823659ac865a2a0b5fdf0b6fa1f5ff656 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 16 Apr 2024 13:00:10 +0200 Subject: [PATCH 03/12] fix --- src/client/client.document.test-d.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/client/client.document.test-d.ts b/src/client/client.document.test-d.ts index a604631a6..a335308d2 100644 --- a/src/client/client.document.test-d.ts +++ b/src/client/client.document.test-d.ts @@ -11,27 +11,27 @@ const client = create({ schema, schemaIndex: $Index }) describe(`input`, () => { test(`document`, () => { const run = client.document({ - foo: {query:{ id: true }}, + foo: { query: { id: true } }, }).run - expectTypeOf(run).toMatchTypeOf<<$Name extends "foo">(name: $Name) => Promise>() + expectTypeOf(run).toMatchTypeOf<(name: 'foo') => Promise>() }) test(`document`, () => { const run = client.document({ - foo: {query:{ id: true }}, - bar: {query:{ id: true }}, + foo: { query: { id: true } }, + bar: { query: { id: true } }, }).run - expectTypeOf(run).toMatchTypeOf<<$Name extends "foo" | "bar">(name: $Name) => Promise>() + expectTypeOf(run).toMatchTypeOf<(name: 'foo' | 'bar') => Promise>() }) }) describe(`output`, () => { test(`document`, () => { const result = client.document({ - foo: { query:{ id: true }}, - bar: { query:{ id: true }}, + foo: { query: { id: true } }, + bar: { query: { id: true } }, }).run(`foo`) - expectTypeOf(result).toMatchTypeOf>() + expectTypeOf(result).toEqualTypeOf>() }) // todo mutation test }) From 216af4fdf1a8dabaa45ab75c0eb3b77372a7cb6a Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 16 Apr 2024 13:31:19 +0200 Subject: [PATCH 04/12] work --- src/client/client.document.test-d.ts | 40 ++++++++++++++----- src/client/client.ts | 25 ++++++++++-- src/client/document.ts | 5 --- .../__snapshots__/files.test.ts.snap | 21 +++------- src/generator/code/schemaRuntime.ts | 5 ++- tests/_/schema/generated/SchemaRuntime.ts | 2 +- tests/ts/_/schema/generated/SchemaRuntime.ts | 19 ++------- 7 files changed, 64 insertions(+), 53 deletions(-) diff --git a/src/client/client.document.test-d.ts b/src/client/client.document.test-d.ts index a335308d2..8f2b9e3a2 100644 --- a/src/client/client.document.test-d.ts +++ b/src/client/client.document.test-d.ts @@ -4,19 +4,22 @@ import { $Index } from '../../tests/_/schema/generated/SchemaRuntime.js' import { schema } from '../../tests/_/schema/schema.js' import { create } from './client.js' -// todo different error now -// @ts-expect-error infinite depth const client = create({ schema, schemaIndex: $Index }) +test(`requires input`, () => { + // @ts-expect-error missing input + client.document() + // @ts-expect-error empty object + client.document({}) +}) + describe(`input`, () => { - test(`document`, () => { - const run = client.document({ - foo: { query: { id: true } }, - }).run - expectTypeOf(run).toMatchTypeOf<(name: 'foo') => Promise>() + test(`document with one query`, () => { + const run = client.document({ foo: { query: { id: true } } }).run + expectTypeOf(run).toMatchTypeOf<(...params: ['foo'] | [] | [undefined]) => Promise>() }) - test(`document`, () => { + test(`document with two queries`, () => { const run = client.document({ foo: { query: { id: true } }, bar: { query: { id: true } }, @@ -26,12 +29,27 @@ describe(`input`, () => { }) describe(`output`, () => { - test(`document`, () => { - const result = client.document({ + test(`document with one query`, async () => { + { + const result = await client.document({ foo: { query: { id: true } } }).run() + expectTypeOf(result).toEqualTypeOf<{ id: string | null }>() + } + { + const result = await client.document({ foo: { query: { id: true } } }).run(`foo`) + expectTypeOf(result).toEqualTypeOf<{ id: string | null }>() + } + { + const result = await client.document({ foo: { query: { id: true } } }).run(undefined) + expectTypeOf(result).toEqualTypeOf<{ id: string | null }>() + } + }) + test(`document with two queries`, async () => { + const result = await client.document({ foo: { query: { id: true } }, bar: { query: { id: true } }, }).run(`foo`) - expectTypeOf(result).toEqualTypeOf>() + expectTypeOf(result).toEqualTypeOf<{ id: string | null }>() }) // todo mutation test + // todo mutation & query mix test }) diff --git a/src/client/client.ts b/src/client/client.ts index 425244c3e..5728c1bb0 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -1,5 +1,5 @@ import { type DocumentNode, execute, graphql, type GraphQLSchema } from 'graphql' -import type { MergeExclusive } from 'type-fest' +import type { MergeExclusive, NonEmptyObject, UnionToIntersection } from 'type-fest' import type { ExcludeUndefined } from 'type-fest/source/required-deep.js' import request from '../entrypoints/main.js' import { type RootTypeName } from '../lib/graphql.js' @@ -33,6 +33,23 @@ type RootTypeMethods<$Index extends Schema.Index, $RootTypeName extends Schema.R [$ObjectName in keyof $Index['objects']]: ObjectMethod<$Index, $ObjectName> } +type UnionToIntersection = + (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never + +type LastOf = + UnionToIntersection T : never> extends () => (infer R) ? R : never + +// TS4.0+ +type Push = [...T, V]; + +// TS4.1+ +type UnionToTuple, N = [T] extends [never] ? true : false> = + true extends N ? [] : Push>, L> + +type CountKeys = keyof T extends never ? 0 : UnionToTuple['length'] +type IsMultipleKeys = IsMultiple> +type IsMultiple = T extends 0 ? false : T extends 1 ? false : true + // todo the name below should be limited to a valid graphql root type name // dprint-ignore type Document<$Index extends Schema.Index> = @@ -56,9 +73,11 @@ export type Client<$Index extends Schema.Index> = // todo test raw raw: (document: string|DocumentNode, variables?:Variables) => Promise document: <$Document extends Document<$Index>> - (document: Exact<$Document, Document<$Index>>) => + (document: NonEmptyObject<$Document>) => { - run: <$Name extends keyof $Document & string>(name: $Name) => Promise, $Index, 'Query'>> + run: <$Name extends keyof $Document & string, $Params extends (IsMultipleKeys<$Document> extends true ? [name: $Name] : ([]|[name: $Name | undefined]))> + (...params: $Params) => + Promise, $Index, 'Query'>> } } & ( diff --git a/src/client/document.ts b/src/client/document.ts index d08c7225f..38fb56603 100644 --- a/src/client/document.ts +++ b/src/client/document.ts @@ -11,8 +11,3 @@ export const toDocumentExpression = ( return `${operationType} ${operationName} ${documentString}` }).join(`\n\n`) } - -// const operationTypes = { -// query: `query`, -// mutation: `mutation`, -// } diff --git a/src/generator/__snapshots__/files.test.ts.snap b/src/generator/__snapshots__/files.test.ts.snap index 65c17108e..ffef028bc 100644 --- a/src/generator/__snapshots__/files.test.ts.snap +++ b/src/generator/__snapshots__/files.test.ts.snap @@ -321,56 +321,47 @@ export const InputObject = $.InputObject(\`InputObject\`, { dateRequired: $.Input.field($Scalar.Date), }) -// eslint-disable-next-line // @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 DateObject1 = $.Object$(\`DateObject1\`, { date1: $.field($.Output.Nullable($Scalar.Date)), }) -// eslint-disable-next-line // @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 DateObject2 = $.Object$(\`DateObject2\`, { date2: $.field($.Output.Nullable($Scalar.Date)), }) -// eslint-disable-next-line // @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 ObjectUnion = $.Object$(\`ObjectUnion\`, { fooBarUnion: $.field($.Output.Nullable(() => FooBarUnion)), }) -// eslint-disable-next-line // @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)), }) -// eslint-disable-next-line // @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)), }) -// eslint-disable-next-line // @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 ObjectNested = $.Object$(\`ObjectNested\`, { id: $.field($.Output.Nullable($Scalar.ID)), object: $.field($.Output.Nullable(() => Object1)), }) -// eslint-disable-next-line // @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 lowerCaseObject = $.Object$(\`lowerCaseObject\`, { id: $.field($.Output.Nullable($Scalar.ID)), }) -// eslint-disable-next-line // @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 lowerCaseObject2 = $.Object$(\`lowerCaseObject2\`, { int: $.field($.Output.Nullable($Scalar.Int)), }) -// eslint-disable-next-line // @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 Object1 = $.Object$(\`Object1\`, { string: $.field($.Output.Nullable($Scalar.String)), @@ -380,29 +371,24 @@ export const Object1 = $.Object$(\`Object1\`, { id: $.field($.Output.Nullable($Scalar.ID)), }) -// eslint-disable-next-line // @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 Object1ImplementingInterface = $.Object$(\`Object1ImplementingInterface\`, { id: $.field($.Output.Nullable($Scalar.ID)), int: $.field($.Output.Nullable($Scalar.Int)), }) -// eslint-disable-next-line // @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 Object2ImplementingInterface = $.Object$(\`Object2ImplementingInterface\`, { id: $.field($.Output.Nullable($Scalar.ID)), boolean: $.field($.Output.Nullable($Scalar.Boolean)), }) -// eslint-disable-next-line // @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 DateUnion = $.Union(\`DateUnion\`, [DateObject1, DateObject2]) -// eslint-disable-next-line // @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\`, [Foo, Bar]) -// eslint-disable-next-line // @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 lowerCaseUnion = $.Union(\`lowerCaseUnion\`, [lowerCaseObject, lowerCaseObject2]) @@ -414,7 +400,6 @@ export const Interface = $.Interface(\`Interface\`, { id: $.field($.Output.Nulla Object2ImplementingInterface, ]) -// eslint-disable-next-line // @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 Query = $.Object$(\`Query\`, { date: $.field($.Output.Nullable($Scalar.Date)), @@ -522,6 +507,10 @@ export const $Index = { FooBarUnion, lowerCaseUnion, }, + interfaces: { + DateInterface1, + Interface, + }, } " `; @@ -598,7 +587,6 @@ exports[`schema2 3`] = ` import * as $ from '../../../../src/Schema/__.js' import * as $Scalar from './Scalar.js' -// eslint-disable-next-line // @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 Query = $.Object$(\`Query\`, { id: $.field($.Output.Nullable($Scalar.ID)), @@ -612,6 +600,7 @@ export const $Index = { }, objects: {}, unions: {}, + interfaces: {}, } " `; diff --git a/src/generator/code/schemaRuntime.ts b/src/generator/code/schemaRuntime.ts index 2d2f5f267..656005f69 100644 --- a/src/generator/code/schemaRuntime.ts +++ b/src/generator/code/schemaRuntime.ts @@ -65,6 +65,9 @@ const index = (config: Config) => { }, unions: { ${config.typeMapByKind.GraphQLUnionType.map(type => type.name).join(`,\n`)} + }, + interfaces: { + ${config.typeMapByKind.GraphQLInterfaceType.map(type => type.name).join(`,\n`)} } } ` @@ -74,7 +77,6 @@ const union = (config: Config, type: GraphQLUnionType) => { // todo probably need thunks here const members = type.getTypes().map(t => t.name).join(`, `) return ` - // eslint-disable-next-line // @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 ${type.name} = $.Union(\`${type.name}\`, [${members}])\n` } @@ -102,7 +104,6 @@ const object = (config: Config, type: GraphQLObjectType) => { return `${field.name}: ${outputField(config, field)}` }).join(`,\n`) return ` - // eslint-disable-next-line // @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 ${type.name} = $.Object$(\`${type.name}\`, { ${fields} diff --git a/tests/_/schema/generated/SchemaRuntime.ts b/tests/_/schema/generated/SchemaRuntime.ts index 1c7aa00b1..1f0d60e70 100644 --- a/tests/_/schema/generated/SchemaRuntime.ts +++ b/tests/_/schema/generated/SchemaRuntime.ts @@ -3,7 +3,6 @@ import * as $ from '../../../../src/Schema/__.js' import * as $Scalar from './Scalar.js' -// eslint-disable-next-line // @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 Query = $.Object$(`Query`, { id: $.field($.Output.Nullable($Scalar.ID)), @@ -17,4 +16,5 @@ export const $Index = { }, objects: {}, unions: {}, + interfaces: {}, } diff --git a/tests/ts/_/schema/generated/SchemaRuntime.ts b/tests/ts/_/schema/generated/SchemaRuntime.ts index d6ff29f33..f125d3985 100644 --- a/tests/ts/_/schema/generated/SchemaRuntime.ts +++ b/tests/ts/_/schema/generated/SchemaRuntime.ts @@ -20,56 +20,47 @@ export const InputObject = $.InputObject(`InputObject`, { dateRequired: $.Input.field($Scalar.Date), }) -// eslint-disable-next-line // @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 DateObject1 = $.Object$(`DateObject1`, { date1: $.field($.Output.Nullable($Scalar.Date)), }) -// eslint-disable-next-line // @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 DateObject2 = $.Object$(`DateObject2`, { date2: $.field($.Output.Nullable($Scalar.Date)), }) -// eslint-disable-next-line // @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 ObjectUnion = $.Object$(`ObjectUnion`, { fooBarUnion: $.field($.Output.Nullable(() => FooBarUnion)), }) -// eslint-disable-next-line // @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)), }) -// eslint-disable-next-line // @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)), }) -// eslint-disable-next-line // @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 ObjectNested = $.Object$(`ObjectNested`, { id: $.field($.Output.Nullable($Scalar.ID)), object: $.field($.Output.Nullable(() => Object1)), }) -// eslint-disable-next-line // @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 lowerCaseObject = $.Object$(`lowerCaseObject`, { id: $.field($.Output.Nullable($Scalar.ID)), }) -// eslint-disable-next-line // @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 lowerCaseObject2 = $.Object$(`lowerCaseObject2`, { int: $.field($.Output.Nullable($Scalar.Int)), }) -// eslint-disable-next-line // @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 Object1 = $.Object$(`Object1`, { string: $.field($.Output.Nullable($Scalar.String)), @@ -79,29 +70,24 @@ export const Object1 = $.Object$(`Object1`, { id: $.field($.Output.Nullable($Scalar.ID)), }) -// eslint-disable-next-line // @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 Object1ImplementingInterface = $.Object$(`Object1ImplementingInterface`, { id: $.field($.Output.Nullable($Scalar.ID)), int: $.field($.Output.Nullable($Scalar.Int)), }) -// eslint-disable-next-line // @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 Object2ImplementingInterface = $.Object$(`Object2ImplementingInterface`, { id: $.field($.Output.Nullable($Scalar.ID)), boolean: $.field($.Output.Nullable($Scalar.Boolean)), }) -// eslint-disable-next-line // @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 DateUnion = $.Union(`DateUnion`, [DateObject1, DateObject2]) -// eslint-disable-next-line // @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`, [Foo, Bar]) -// eslint-disable-next-line // @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 lowerCaseUnion = $.Union(`lowerCaseUnion`, [lowerCaseObject, lowerCaseObject2]) @@ -113,7 +99,6 @@ export const Interface = $.Interface(`Interface`, { id: $.field($.Output.Nullabl Object2ImplementingInterface, ]) -// eslint-disable-next-line // @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 Query = $.Object$(`Query`, { date: $.field($.Output.Nullable($Scalar.Date)), @@ -221,4 +206,8 @@ export const $Index = { FooBarUnion, lowerCaseUnion, }, + interfaces: { + DateInterface1, + Interface, + }, } From 4cc2200eca110a9676da2792a77ae8646573caf4 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 16 Apr 2024 13:46:56 +0200 Subject: [PATCH 05/12] tests --- package.json | 2 +- src/client/client.document.test.ts | 32 +++++++++++++++---- src/client/client.ts | 23 ++----------- .../__snapshots__/files.test.ts.snap | 2 ++ src/lib/prelude.ts | 15 +++++++++ tests/_/schema/generated/SchemaBuildtime.ts | 1 + tests/_/schema/generated/SchemaRuntime.ts | 1 + tests/_/schema/schema.graphql | 1 + tests/_/schema/schema.ts | 1 + 9 files changed, 51 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index a992c4e65..800416418 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ }, "homepage": "https://github.com/jasonkuhrt/graphql-request", "scripts": { - "gen:test:schema": "tsx tests/_/schema/schema.ts", + "gen:test:schema": "tsx tests/_/schema/schemaGenerate.ts", "demo": "tsx src/cli/generateSchema.ts && dprint fmt src/demo.ts", "dev": "rm -rf dist && tsc --watch", "format": "pnpm build:docs && dprint fmt", diff --git a/src/client/client.document.test.ts b/src/client/client.document.test.ts index ecde6c513..12f8bbe7f 100644 --- a/src/client/client.document.test.ts +++ b/src/client/client.document.test.ts @@ -4,18 +4,38 @@ import { $Index } from '../../tests/_/schema/generated/SchemaRuntime.js' import { db, schema } from '../../tests/_/schema/schema.js' import { create } from './client.js' -// todo different error now -// @ts-expect-error infinite depth const client = create({ schema, schemaIndex: $Index }) -test(`document`, async () => { - const result = await client.document({ +test(`document with two queries`, async () => { + const { run } = client.document({ foo: { query: { id: true }, }, bar: { + query: { idNonNull: true }, + }, + }) + expect(await run(`foo`)).toEqual({ data: { id: db.id1 } }) + expect(await run(`bar`)).toEqual({ data: { idNonNull: db.id1 } }) + // @ts-expect-error + const result = await run().catch((e: unknown) => e) as { errors: Error[] } + expect(result.errors.length).toBe(1) + const error = result.errors[0]! + expect(error instanceof Error).toBe(true) + expect(error.message).toMatch(`Must provide operation name if query contains multiple operations`) +}) + +test(`document with one query`, async () => { + const { run } = client.document({ + foo: { query: { id: true }, }, - }).run(`foo`) - expect(result).toEqual({ data: { id: db.id1 } }) + }) + expect(await run(`foo`)).toEqual({ data: { id: db.id1 } }) + expect(await run()).toEqual({ data: { id: db.id1 } }) + expect(await run(undefined)).toEqual({ data: { id: db.id1 } }) }) + +// todo passing wrong operation name should throw +// todo mutation +// todo mutation and query mixed diff --git a/src/client/client.ts b/src/client/client.ts index 5728c1bb0..75bcb7f5a 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -1,9 +1,9 @@ import { type DocumentNode, execute, graphql, type GraphQLSchema } from 'graphql' -import type { MergeExclusive, NonEmptyObject, UnionToIntersection } from 'type-fest' +import type { MergeExclusive, NonEmptyObject } from 'type-fest' import type { ExcludeUndefined } from 'type-fest/source/required-deep.js' import request from '../entrypoints/main.js' import { type RootTypeName } from '../lib/graphql.js' -import type { Exact } from '../lib/prelude.js' +import type { Exact, IsMultipleKeys } from '../lib/prelude.js' import type { Object$2, Schema } from '../Schema/__.js' import * as CustomScalars from './customScalars.js' import { toDocumentExpression } from './document.js' @@ -33,23 +33,6 @@ type RootTypeMethods<$Index extends Schema.Index, $RootTypeName extends Schema.R [$ObjectName in keyof $Index['objects']]: ObjectMethod<$Index, $ObjectName> } -type UnionToIntersection = - (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never - -type LastOf = - UnionToIntersection T : never> extends () => (infer R) ? R : never - -// TS4.0+ -type Push = [...T, V]; - -// TS4.1+ -type UnionToTuple, N = [T] extends [never] ? true : false> = - true extends N ? [] : Push>, L> - -type CountKeys = keyof T extends never ? 0 : UnionToTuple['length'] -type IsMultipleKeys = IsMultiple> -type IsMultiple = T extends 0 ? false : T extends 1 ? false : true - // todo the name below should be limited to a valid graphql root type name // dprint-ignore type Document<$Index extends Schema.Index> = @@ -75,7 +58,7 @@ export type Client<$Index extends Schema.Index> = document: <$Document extends Document<$Index>> (document: NonEmptyObject<$Document>) => { - run: <$Name extends keyof $Document & string, $Params extends (IsMultipleKeys<$Document> extends true ? [name: $Name] : ([]|[name: $Name | undefined]))> + run: <$Name extends keyof $Document & string, $Params extends (IsMultipleKeys<$Document> extends true ? [name: $Name] : ([] | [name: $Name | undefined]))> (...params: $Params) => Promise, $Index, 'Query'>> } diff --git a/src/generator/__snapshots__/files.test.ts.snap b/src/generator/__snapshots__/files.test.ts.snap index ffef028bc..d7b3ad539 100644 --- a/src/generator/__snapshots__/files.test.ts.snap +++ b/src/generator/__snapshots__/files.test.ts.snap @@ -526,6 +526,7 @@ import type * as $Scalar from './Scalar.ts' export namespace Root { export type Query = $.Object$2<'Query', { id: $.Field<$.Output.Nullable<$Scalar.ID>, null> + idNonNull: $.Field<$Scalar.ID, null> }> } @@ -590,6 +591,7 @@ import * as $Scalar from './Scalar.js' // @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 Query = $.Object$(\`Query\`, { id: $.field($.Output.Nullable($Scalar.ID)), + idNonNull: $.field($Scalar.ID), }) export const $Index = { diff --git a/src/lib/prelude.ts b/src/lib/prelude.ts index cd1a05bfd..a02256c72 100644 --- a/src/lib/prelude.ts +++ b/src/lib/prelude.ts @@ -188,3 +188,18 @@ export const fileExists = async (path: string) => { } export type As = U extends T ? U : never + +export type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never + +export type LastOf = UnionToIntersection T : never> extends () => infer R ? R : never + +// TS4.0+ +export type Push = [...T, V] + +// TS4.1+ +export type UnionToTuple, N = [T] extends [never] ? true : false> = true extends N ? [] + : Push>, L> + +export type CountKeys = keyof T extends never ? 0 : UnionToTuple['length'] +export type IsMultipleKeys = IsMultiple> +export type IsMultiple = T extends 0 ? false : T extends 1 ? false : true diff --git a/tests/_/schema/generated/SchemaBuildtime.ts b/tests/_/schema/generated/SchemaBuildtime.ts index 2885bc0f3..125f4596a 100644 --- a/tests/_/schema/generated/SchemaBuildtime.ts +++ b/tests/_/schema/generated/SchemaBuildtime.ts @@ -8,6 +8,7 @@ import type * as $Scalar from './Scalar.ts' export namespace Root { export type Query = $.Object$2<'Query', { id: $.Field<$.Output.Nullable<$Scalar.ID>, null> + idNonNull: $.Field<$Scalar.ID, null> }> } diff --git a/tests/_/schema/generated/SchemaRuntime.ts b/tests/_/schema/generated/SchemaRuntime.ts index 1f0d60e70..8826aa9e4 100644 --- a/tests/_/schema/generated/SchemaRuntime.ts +++ b/tests/_/schema/generated/SchemaRuntime.ts @@ -6,6 +6,7 @@ import * as $Scalar from './Scalar.js' // @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 Query = $.Object$(`Query`, { id: $.field($.Output.Nullable($Scalar.ID)), + idNonNull: $.field($Scalar.ID), }) export const $Index = { diff --git a/tests/_/schema/schema.graphql b/tests/_/schema/schema.graphql index 0ed600e65..a17a05bf7 100644 --- a/tests/_/schema/schema.graphql +++ b/tests/_/schema/schema.graphql @@ -1,3 +1,4 @@ type Query { id: ID + idNonNull: ID! } \ No newline at end of file diff --git a/tests/_/schema/schema.ts b/tests/_/schema/schema.ts index 6d33b697b..7efbe0619 100644 --- a/tests/_/schema/schema.ts +++ b/tests/_/schema/schema.ts @@ -13,6 +13,7 @@ const builder = new SchemaBuilder<{ builder.queryType({ fields: t => ({ id: t.id({ resolve: () => db.id1 }), + idNonNull: t.id({ nullable: false, resolve: () => db.id1 }), }), }) From a418ae8cc99ce03a5fc45f9d3e8d2739c32e96ff Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 16 Apr 2024 16:30:15 +0200 Subject: [PATCH 06/12] more cases --- src/client/client.document.test.ts | 51 +++++++++++++++++++----------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/src/client/client.document.test.ts b/src/client/client.document.test.ts index 12f8bbe7f..1babe0a32 100644 --- a/src/client/client.document.test.ts +++ b/src/client/client.document.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from 'vitest' +import { describe, expect, test } from 'vitest' 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' @@ -6,23 +6,39 @@ import { create } from './client.js' const client = create({ schema, schemaIndex: $Index }) -test(`document with two queries`, async () => { - const { run } = client.document({ - foo: { - query: { id: true }, - }, - bar: { - query: { idNonNull: true }, - }, +const withTwo = client.document({ + foo: { + query: { id: true }, + }, + bar: { + query: { idNonNull: true }, + }, +}) + +describe(`document with two queries`, () => { + test(`works`, async () => { + const { run } = withTwo + expect(await run(`foo`)).toEqual({ data: { id: db.id1 } }) + expect(await run(`bar`)).toEqual({ data: { idNonNull: db.id1 } }) + }) + test(`error if no operation name is provided`, async () => { + const { run } = withTwo + // @ts-expect-error + const result = await run().catch((e: unknown) => e) as { errors: Error[] } + expect(result.errors.length).toBe(1) + const error = result.errors[0]! + expect(error instanceof Error).toBe(true) + expect(error.message).toMatch(`Must provide operation name if query contains multiple operations`) + }) + test(`error if wrong operation name is provided`, async () => { + const { run } = withTwo + // @ts-expect-error + const result = await run(`boo`).catch((e: unknown) => e) as { errors: Error[] } + expect(result.errors.length).toBe(1) + const error = result.errors[0]! + expect(error instanceof Error).toBe(true) + expect(error.message).toMatch(`Unknown operation named "boo"`) }) - expect(await run(`foo`)).toEqual({ data: { id: db.id1 } }) - expect(await run(`bar`)).toEqual({ data: { idNonNull: db.id1 } }) - // @ts-expect-error - const result = await run().catch((e: unknown) => e) as { errors: Error[] } - expect(result.errors.length).toBe(1) - const error = result.errors[0]! - expect(error instanceof Error).toBe(true) - expect(error.message).toMatch(`Must provide operation name if query contains multiple operations`) }) test(`document with one query`, async () => { @@ -36,6 +52,5 @@ test(`document with one query`, async () => { expect(await run(undefined)).toEqual({ data: { id: db.id1 } }) }) -// todo passing wrong operation name should throw // todo mutation // todo mutation and query mixed From c3b17414773b806dae9ec03644735b5f38a0a9f1 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 16 Apr 2024 16:34:52 +0200 Subject: [PATCH 07/12] fixes --- eslint.config.js | 2 +- src/client/client.customScalar.test.ts | 5 ++--- src/client/client.ts | 2 ++ tests/ts/_/schema/generated/Index.ts | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index 6343d21e1..2ee112999 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,7 +2,7 @@ import configPrisma from 'eslint-config-prisma' import tsEslint from 'typescript-eslint' export default tsEslint.config({ - ignores: ['**/build/**/*', 'eslint.config.js'], + ignores: ['**/build/**/*', 'eslint.config.js', 'vite.config.ts'], extends: configPrisma, languageOptions: { parserOptions: { diff --git a/src/client/client.customScalar.test.ts b/src/client/client.customScalar.test.ts index 1e746a639..6e26ca0c9 100644 --- a/src/client/client.customScalar.test.ts +++ b/src/client/client.customScalar.test.ts @@ -8,8 +8,7 @@ import { create } from './client.js' const ctx = setupMockServer() const data = { fooBarUnion: { int: 1 } } -// @ts-ignore infinite depth -const client = () => create({ url: ctx.url, schemaIndex }) +const client = () => create({ schema: ctx.url, schemaIndex }) describe(`output`, () => { test(`query field`, async () => { @@ -79,7 +78,7 @@ describe(`input`, () => { }) const clientExpected = (expectedDocument: (document: any) => void) => { const client = create({ - url: ctx.url, + schema: ctx.url, schemaIndex, hooks: { documentEncode: (input, run) => { diff --git a/src/client/client.ts b/src/client/client.ts index 75bcb7f5a..732f87d67 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -144,6 +144,8 @@ export const create = <$SchemaIndex extends Schema.Index>(input: Input): Client< variableValues: variables, operationName, }) + } else { + throw new Error(`Unsupported GraphQL document type: ${String(document)}`) } } } diff --git a/tests/ts/_/schema/generated/Index.ts b/tests/ts/_/schema/generated/Index.ts index 62ee03acd..b4f01656a 100644 --- a/tests/ts/_/schema/generated/Index.ts +++ b/tests/ts/_/schema/generated/Index.ts @@ -1,4 +1,4 @@ -/* eslint-disable */ + import type * as Schema from './SchemaBuildtime.js' From d746c7c52fdccf55f6ab56689a74c11eb7d82408 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 16 Apr 2024 16:35:14 +0200 Subject: [PATCH 08/12] gen --- tests/ts/_/schema/generated/Index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ts/_/schema/generated/Index.ts b/tests/ts/_/schema/generated/Index.ts index b4f01656a..62ee03acd 100644 --- a/tests/ts/_/schema/generated/Index.ts +++ b/tests/ts/_/schema/generated/Index.ts @@ -1,4 +1,4 @@ - +/* eslint-disable */ import type * as Schema from './SchemaBuildtime.js' From 15eafe2ab806cd7e16cffc7d76ad6abf3de15e37 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Tue, 16 Apr 2024 23:58:54 +0200 Subject: [PATCH 09/12] more test cases --- package.json | 2 +- src/Schema/_.ts | 4 +- src/client/client.document.test-d.ts | 31 ++++++++--- src/client/client.document.test.ts | 43 +++++++++++---- src/client/client.ts | 16 ++++-- .../__snapshots__/files.test.ts.snap | 13 ++++- src/generator/files.test.ts | 21 -------- tests/_/schema/generated/Index.ts | 2 +- tests/_/schema/generated/SchemaBuildtime.ts | 5 ++ tests/_/schema/generated/SchemaRuntime.ts | 8 ++- tests/_/schema/schema.graphql | 5 ++ tests/_/schema/schema.ts | 10 ++++ tests/_/schema/schemaGenerate.ts | 8 --- tests/_/schemaGenerate.ts | 41 ++++++++++++++ tests/_/schemaMutationOnly/generated/Index.ts | 14 +++++ .../_/schemaMutationOnly/generated/Scalar.ts | 6 +++ .../generated/SchemaBuildtime.ts | 53 +++++++++++++++++++ .../generated/SchemaRuntime.ts | 21 ++++++++ tests/_/schemaMutationOnly/schema.graphql | 4 ++ tests/_/schemaMutationOnly/schema.ts | 25 +++++++++ tests/_/schemaQueryOnly/generated/Index.ts | 14 +++++ tests/_/schemaQueryOnly/generated/Scalar.ts | 6 +++ .../generated/SchemaBuildtime.ts | 53 +++++++++++++++++++ .../generated/SchemaRuntime.ts | 21 ++++++++ tests/_/schemaQueryOnly/schema.graphql | 4 ++ tests/_/schemaQueryOnly/schema.ts | 25 +++++++++ 26 files changed, 398 insertions(+), 57 deletions(-) delete mode 100644 tests/_/schema/schemaGenerate.ts create mode 100644 tests/_/schemaGenerate.ts create mode 100644 tests/_/schemaMutationOnly/generated/Index.ts create mode 100644 tests/_/schemaMutationOnly/generated/Scalar.ts create mode 100644 tests/_/schemaMutationOnly/generated/SchemaBuildtime.ts create mode 100644 tests/_/schemaMutationOnly/generated/SchemaRuntime.ts create mode 100644 tests/_/schemaMutationOnly/schema.graphql create mode 100644 tests/_/schemaMutationOnly/schema.ts create mode 100644 tests/_/schemaQueryOnly/generated/Index.ts create mode 100644 tests/_/schemaQueryOnly/generated/Scalar.ts create mode 100644 tests/_/schemaQueryOnly/generated/SchemaBuildtime.ts create mode 100644 tests/_/schemaQueryOnly/generated/SchemaRuntime.ts create mode 100644 tests/_/schemaQueryOnly/schema.graphql create mode 100644 tests/_/schemaQueryOnly/schema.ts diff --git a/package.json b/package.json index 800416418..5cae5dfcf 100644 --- a/package.json +++ b/package.json @@ -58,7 +58,7 @@ }, "homepage": "https://github.com/jasonkuhrt/graphql-request", "scripts": { - "gen:test:schema": "tsx tests/_/schema/schemaGenerate.ts", + "gen:test:schema": "tsx tests/_/schemaGenerate.ts", "demo": "tsx src/cli/generateSchema.ts && dprint fmt src/demo.ts", "dev": "rm -rf dist && tsc --watch", "format": "pnpm build:docs && dprint fmt", diff --git a/src/Schema/_.ts b/src/Schema/_.ts index 572a8d63d..98b1cbbbd 100644 --- a/src/Schema/_.ts +++ b/src/Schema/_.ts @@ -1,5 +1,5 @@ export * from './Args.js' -export { RootTypeName } from './core/helpers.js' +export { type RootTypeName } from './core/helpers.js' export * from './core/Index.js' export * from './core/Named/__.js' export * from './Field.js' @@ -10,5 +10,5 @@ export * from './Input/types/InputObject.js' export * from './Output/__.js' export * from './Output/types/__typename.js' export * from './Output/types/Interface.js' -export { Object$, Object$2 } from './Output/types/Object.js' +export { Object$, type Object$2 } from './Output/types/Object.js' export * from './Output/types/Union.js' diff --git a/src/client/client.document.test-d.ts b/src/client/client.document.test-d.ts index 8f2b9e3a2..c60470031 100644 --- a/src/client/client.document.test-d.ts +++ b/src/client/client.document.test-d.ts @@ -1,10 +1,10 @@ import { describe, expectTypeOf, test } from 'vitest' -import type { Index } from '../../tests/_/schema/generated/Index.js' -import { $Index } from '../../tests/_/schema/generated/SchemaRuntime.js' -import { schema } from '../../tests/_/schema/schema.js' +import * as Schema from '../../tests/_/schema/schema.js' +import * as SchemaMutationOnly from '../../tests/_/schemaMutationOnly/schema.js' +import * as SchemaQueryOnly from '../../tests/_/schemaQueryOnly/schema.js' import { create } from './client.js' -const client = create({ schema, schemaIndex: $Index }) +const client = create({ schema: Schema.schema, schemaIndex: Schema.$Index }) test(`requires input`, () => { // @ts-expect-error missing input @@ -26,6 +26,27 @@ describe(`input`, () => { }).run expectTypeOf(run).toMatchTypeOf<(name: 'foo' | 'bar') => Promise>() }) + + test(`root operation not available if it is not in schema`, () => { + const clientQueryOnly = create({ + schema: SchemaQueryOnly.schema, + schemaIndex: SchemaQueryOnly.$Index, + }) + clientQueryOnly.document({ + foo: { query: { id: true } }, + // @ts-expect-error mutation not in schema + bar: { mutation: { id: true } }, + }) + const clientMutationOnly = create({ + schema: SchemaMutationOnly.schema, + schemaIndex: SchemaMutationOnly.$Index, + }) + clientMutationOnly.document({ + // @ts-expect-error mutation not in schema + foo: { query: { id: true } }, + bar: { mutation: { id: true } }, + }) + }) }) describe(`output`, () => { @@ -50,6 +71,4 @@ describe(`output`, () => { }).run(`foo`) expectTypeOf(result).toEqualTypeOf<{ id: string | null }>() }) - // todo mutation test - // todo mutation & query mix test }) diff --git a/src/client/client.document.test.ts b/src/client/client.document.test.ts index 1babe0a32..c35c0eac6 100644 --- a/src/client/client.document.test.ts +++ b/src/client/client.document.test.ts @@ -6,16 +6,16 @@ import { create } from './client.js' const client = create({ schema, schemaIndex: $Index }) -const withTwo = client.document({ - foo: { - query: { id: true }, - }, - bar: { - query: { idNonNull: true }, - }, -}) - describe(`document with two queries`, () => { + const withTwo = client.document({ + foo: { + query: { id: true }, + }, + bar: { + query: { idNonNull: true }, + }, + }) + test(`works`, async () => { const { run } = withTwo expect(await run(`foo`)).toEqual({ data: { id: db.id1 } }) @@ -52,5 +52,26 @@ test(`document with one query`, async () => { expect(await run(undefined)).toEqual({ data: { id: db.id1 } }) }) -// todo mutation -// todo mutation and query mixed +test(`document with one mutation`, async () => { + const { run } = client.document({ + foo: { + mutation: { id: true }, + }, + }) + expect(await run(`foo`)).toEqual({ data: { id: db.id1 } }) + expect(await run()).toEqual({ data: { id: db.id1 } }) + expect(await run(undefined)).toEqual({ data: { id: db.id1 } }) +}) + +test(`document with one mutation and one query`, async () => { + const { run } = client.document({ + foo: { + mutation: { id: true }, + }, + bar: { + query: { idNonNull: true }, + }, + }) + expect(await run(`foo`)).toEqual({ data: { id: db.id1 } }) + expect(await run(`bar`)).toEqual({ data: { idNonNull: db.id1 } }) +}) diff --git a/src/client/client.ts b/src/client/client.ts index 732f87d67..949509656 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -37,11 +37,17 @@ type RootTypeMethods<$Index extends Schema.Index, $RootTypeName extends Schema.R // dprint-ignore type Document<$Index extends Schema.Index> = { - [name: string]: MergeExclusive<{ - query: SelectionSet.Root<$Index, 'Query'> - }, { - mutation: SelectionSet.Root<$Index, 'Mutation'> - }> + [name: string]: + $Index['Root']['Query'] extends null ? { mutation: SelectionSet.Root<$Index, 'Mutation'> } : + $Index['Root']['Mutation'] extends null ? { query: SelectionSet.Root<$Index, 'Query'> } : + MergeExclusive< + { + query: SelectionSet.Root<$Index, 'Query'> + }, + { + mutation: SelectionSet.Root<$Index, 'Mutation'> + } + > } // dprint-ignore diff --git a/src/generator/__snapshots__/files.test.ts.snap b/src/generator/__snapshots__/files.test.ts.snap index d7b3ad539..cddeead55 100644 --- a/src/generator/__snapshots__/files.test.ts.snap +++ b/src/generator/__snapshots__/files.test.ts.snap @@ -524,6 +524,11 @@ import type * as $Scalar from './Scalar.ts' // ------------------------------------------------------------ // export namespace Root { + export type Mutation = $.Object$2<'Mutation', { + id: $.Field<$.Output.Nullable<$Scalar.ID>, null> + idNonNull: $.Field<$Scalar.ID, null> + }> + export type Query = $.Object$2<'Query', { id: $.Field<$.Output.Nullable<$Scalar.ID>, null> idNonNull: $.Field<$Scalar.ID, null> @@ -588,6 +593,12 @@ exports[`schema2 3`] = ` import * as $ from '../../../../src/Schema/__.js' import * as $Scalar from './Scalar.js' +// @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 Mutation = $.Object$(\`Mutation\`, { + id: $.field($.Output.Nullable($Scalar.ID)), + idNonNull: $.field($Scalar.ID), +}) + // @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 Query = $.Object$(\`Query\`, { id: $.field($.Output.Nullable($Scalar.ID)), @@ -597,7 +608,7 @@ export const Query = $.Object$(\`Query\`, { export const $Index = { Root: { Query, - Mutation: null, + Mutation, Subscription: null, }, objects: {}, diff --git a/src/generator/files.test.ts b/src/generator/files.test.ts index c15abcca8..149902565 100644 --- a/src/generator/files.test.ts +++ b/src/generator/files.test.ts @@ -1,18 +1,7 @@ import { readFile } from 'fs/promises' import { expect, test } from 'vitest' -import { generateFiles } from './files.js' test(`generates types from GraphQL SDL file`, async () => { - await generateFiles({ - sourceDirPath: `./tests/ts/_/schema`, - outputDirPath: `./tests/ts/_/schema/generated`, - code: { - libraryPaths: { - schema: `../../../../../src/Schema/__.js`, - scalars: `../../../../../src/Schema/Hybrid/types/Scalar/Scalar.js`, - }, - }, - }) expect( await readFile(`./tests/ts/_/schema/generated/SchemaBuildtime.ts`, `utf8`), ).toMatchSnapshot() @@ -25,16 +14,6 @@ test(`generates types from GraphQL SDL file`, async () => { }) test(`schema2`, async () => { - await generateFiles({ - sourceDirPath: `./tests/_/schema`, - outputDirPath: `./tests/_/schema/generated`, - code: { - libraryPaths: { - schema: `../../../../src/Schema/__.js`, - scalars: `../../../../src/Schema/Hybrid/types/Scalar/Scalar.js`, - }, - }, - }) expect( await readFile(`./tests/_/schema/generated/SchemaBuildtime.ts`, `utf8`), ).toMatchSnapshot() diff --git a/tests/_/schema/generated/Index.ts b/tests/_/schema/generated/Index.ts index 650cb8494..903d65eae 100644 --- a/tests/_/schema/generated/Index.ts +++ b/tests/_/schema/generated/Index.ts @@ -5,7 +5,7 @@ import type * as Schema from './SchemaBuildtime.js' export interface Index { Root: { Query: Schema.Root.Query - Mutation: null + Mutation: Schema.Root.Mutation Subscription: null } objects: {} diff --git a/tests/_/schema/generated/SchemaBuildtime.ts b/tests/_/schema/generated/SchemaBuildtime.ts index 125f4596a..f8ea08d97 100644 --- a/tests/_/schema/generated/SchemaBuildtime.ts +++ b/tests/_/schema/generated/SchemaBuildtime.ts @@ -6,6 +6,11 @@ import type * as $Scalar from './Scalar.ts' // ------------------------------------------------------------ // export namespace Root { + export type Mutation = $.Object$2<'Mutation', { + id: $.Field<$.Output.Nullable<$Scalar.ID>, null> + idNonNull: $.Field<$Scalar.ID, null> + }> + export type Query = $.Object$2<'Query', { id: $.Field<$.Output.Nullable<$Scalar.ID>, null> idNonNull: $.Field<$Scalar.ID, null> diff --git a/tests/_/schema/generated/SchemaRuntime.ts b/tests/_/schema/generated/SchemaRuntime.ts index 8826aa9e4..8e673977a 100644 --- a/tests/_/schema/generated/SchemaRuntime.ts +++ b/tests/_/schema/generated/SchemaRuntime.ts @@ -3,6 +3,12 @@ import * as $ from '../../../../src/Schema/__.js' import * as $Scalar from './Scalar.js' +// @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 Mutation = $.Object$(`Mutation`, { + id: $.field($.Output.Nullable($Scalar.ID)), + idNonNull: $.field($Scalar.ID), +}) + // @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 Query = $.Object$(`Query`, { id: $.field($.Output.Nullable($Scalar.ID)), @@ -12,7 +18,7 @@ export const Query = $.Object$(`Query`, { export const $Index = { Root: { Query, - Mutation: null, + Mutation, Subscription: null, }, objects: {}, diff --git a/tests/_/schema/schema.graphql b/tests/_/schema/schema.graphql index a17a05bf7..8b5f59560 100644 --- a/tests/_/schema/schema.graphql +++ b/tests/_/schema/schema.graphql @@ -1,3 +1,8 @@ +type Mutation { + id: ID + idNonNull: ID! +} + type Query { id: ID idNonNull: ID! diff --git a/tests/_/schema/schema.ts b/tests/_/schema/schema.ts index 7efbe0619..ab9af156d 100644 --- a/tests/_/schema/schema.ts +++ b/tests/_/schema/schema.ts @@ -17,6 +17,16 @@ builder.queryType({ }), }) +builder.mutationType({ + fields: t => ({ + id: t.id({ resolve: () => db.id1 }), + idNonNull: t.id({ nullable: false, resolve: () => db.id1 }), + }), +}) + export const schema = builder.toSchema({ sortSchema: true, }) + +export { Index } from './generated/Index.js' +export { $Index } from './generated/SchemaRuntime.js' diff --git a/tests/_/schema/schemaGenerate.ts b/tests/_/schema/schemaGenerate.ts deleted file mode 100644 index 6625da591..000000000 --- a/tests/_/schema/schemaGenerate.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { printSchema } from 'graphql' -import fs from 'node:fs/promises' -import { schema } from './schema.js' - -await fs.writeFile( - `./tests/_/schema/schema.graphql`, - printSchema(schema), -) diff --git a/tests/_/schemaGenerate.ts b/tests/_/schemaGenerate.ts new file mode 100644 index 000000000..07873ac43 --- /dev/null +++ b/tests/_/schemaGenerate.ts @@ -0,0 +1,41 @@ +import type { GraphQLSchema } from 'graphql' +import { printSchema } from 'graphql' +import fs from 'node:fs/promises' +import { dirname, join } from 'node:path' +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 sourceDirPath = dirname(outputSchemaPath) + await fs.writeFile( + outputSchemaPath, + printSchema(schema), + ) + await generateFiles({ + sourceDirPath, + outputDirPath: join(sourceDirPath, `/generated`), + code: { + libraryPaths: { + schema: `../../../../src/Schema/__.js`, + scalars: `../../../../src/Schema/Hybrid/types/Scalar/Scalar.js`, + }, + }, + }) +} + +await generate({ + schema: schemaQueryOnly, + outputSchemaPath: `./tests/_/schemaQueryOnly/schema.graphql`, +}) + +await generate({ + schema: schemaMutationOnly, + outputSchemaPath: `./tests/_/schemaMutationOnly/schema.graphql`, +}) + +await generate({ + schema, + outputSchemaPath: `./tests/_/schema/schema.graphql`, +}) diff --git a/tests/_/schemaMutationOnly/generated/Index.ts b/tests/_/schemaMutationOnly/generated/Index.ts new file mode 100644 index 000000000..d1a54c73d --- /dev/null +++ b/tests/_/schemaMutationOnly/generated/Index.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ + +import type * as Schema from './SchemaBuildtime.js' + +export interface Index { + Root: { + Query: null + Mutation: Schema.Root.Mutation + Subscription: null + } + objects: {} + unions: {} + interfaces: {} +} diff --git a/tests/_/schemaMutationOnly/generated/Scalar.ts b/tests/_/schemaMutationOnly/generated/Scalar.ts new file mode 100644 index 000000000..08647652d --- /dev/null +++ b/tests/_/schemaMutationOnly/generated/Scalar.ts @@ -0,0 +1,6 @@ +declare global { + interface SchemaCustomScalars { + } +} + +export * from '../../../../src/Schema/Hybrid/types/Scalar/Scalar.js' diff --git a/tests/_/schemaMutationOnly/generated/SchemaBuildtime.ts b/tests/_/schemaMutationOnly/generated/SchemaBuildtime.ts new file mode 100644 index 000000000..a705579d6 --- /dev/null +++ b/tests/_/schemaMutationOnly/generated/SchemaBuildtime.ts @@ -0,0 +1,53 @@ +import type * as $ from '../../../../src/Schema/__.js' +import type * as $Scalar from './Scalar.ts' + +// ------------------------------------------------------------ // +// Root // +// ------------------------------------------------------------ // + +export namespace Root { + export type Mutation = $.Object$2<'Mutation', { + id: $.Field<$.Output.Nullable<$Scalar.ID>, null> + idNonNull: $.Field<$Scalar.ID, null> + }> +} + +// ------------------------------------------------------------ // +// Enum // +// ------------------------------------------------------------ // + +export namespace Enum { + // -- no types -- +} + +// ------------------------------------------------------------ // +// InputObject // +// ------------------------------------------------------------ // + +export namespace InputObject { + // -- no types -- +} + +// ------------------------------------------------------------ // +// Interface // +// ------------------------------------------------------------ // + +export namespace Interface { + // -- no types -- +} + +// ------------------------------------------------------------ // +// Object // +// ------------------------------------------------------------ // + +export namespace Object { + // -- no types -- +} + +// ------------------------------------------------------------ // +// Union // +// ------------------------------------------------------------ // + +export namespace Union { + // -- no types -- +} diff --git a/tests/_/schemaMutationOnly/generated/SchemaRuntime.ts b/tests/_/schemaMutationOnly/generated/SchemaRuntime.ts new file mode 100644 index 000000000..b91491742 --- /dev/null +++ b/tests/_/schemaMutationOnly/generated/SchemaRuntime.ts @@ -0,0 +1,21 @@ +/* eslint-disable */ + +import * as $ from '../../../../src/Schema/__.js' +import * as $Scalar from './Scalar.js' + +// @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 Mutation = $.Object$(`Mutation`, { + id: $.field($.Output.Nullable($Scalar.ID)), + idNonNull: $.field($Scalar.ID), +}) + +export const $Index = { + Root: { + Query: null, + Mutation, + Subscription: null, + }, + objects: {}, + unions: {}, + interfaces: {}, +} diff --git a/tests/_/schemaMutationOnly/schema.graphql b/tests/_/schemaMutationOnly/schema.graphql new file mode 100644 index 000000000..5093ea4a0 --- /dev/null +++ b/tests/_/schemaMutationOnly/schema.graphql @@ -0,0 +1,4 @@ +type Mutation { + id: ID + idNonNull: ID! +} \ No newline at end of file diff --git a/tests/_/schemaMutationOnly/schema.ts b/tests/_/schemaMutationOnly/schema.ts new file mode 100644 index 000000000..0135ffecd --- /dev/null +++ b/tests/_/schemaMutationOnly/schema.ts @@ -0,0 +1,25 @@ +import SchemaBuilder from '@pothos/core' + +export const db = { + id1: `abc`, +} + +const builder = new SchemaBuilder<{ + DefaultFieldNullability: true +}>({ + defaultFieldNullability: true, +}) + +builder.mutationType({ + fields: t => ({ + id: t.id({ resolve: () => db.id1 }), + idNonNull: t.id({ nullable: false, resolve: () => db.id1 }), + }), +}) + +export const schema = builder.toSchema({ + sortSchema: true, +}) + +export { Index } from './generated/Index.js' +export { $Index } from './generated/SchemaRuntime.js' diff --git a/tests/_/schemaQueryOnly/generated/Index.ts b/tests/_/schemaQueryOnly/generated/Index.ts new file mode 100644 index 000000000..650cb8494 --- /dev/null +++ b/tests/_/schemaQueryOnly/generated/Index.ts @@ -0,0 +1,14 @@ +/* eslint-disable */ + +import type * as Schema from './SchemaBuildtime.js' + +export interface Index { + Root: { + Query: Schema.Root.Query + Mutation: null + Subscription: null + } + objects: {} + unions: {} + interfaces: {} +} diff --git a/tests/_/schemaQueryOnly/generated/Scalar.ts b/tests/_/schemaQueryOnly/generated/Scalar.ts new file mode 100644 index 000000000..08647652d --- /dev/null +++ b/tests/_/schemaQueryOnly/generated/Scalar.ts @@ -0,0 +1,6 @@ +declare global { + interface SchemaCustomScalars { + } +} + +export * from '../../../../src/Schema/Hybrid/types/Scalar/Scalar.js' diff --git a/tests/_/schemaQueryOnly/generated/SchemaBuildtime.ts b/tests/_/schemaQueryOnly/generated/SchemaBuildtime.ts new file mode 100644 index 000000000..125f4596a --- /dev/null +++ b/tests/_/schemaQueryOnly/generated/SchemaBuildtime.ts @@ -0,0 +1,53 @@ +import type * as $ from '../../../../src/Schema/__.js' +import type * as $Scalar from './Scalar.ts' + +// ------------------------------------------------------------ // +// Root // +// ------------------------------------------------------------ // + +export namespace Root { + export type Query = $.Object$2<'Query', { + id: $.Field<$.Output.Nullable<$Scalar.ID>, null> + idNonNull: $.Field<$Scalar.ID, null> + }> +} + +// ------------------------------------------------------------ // +// Enum // +// ------------------------------------------------------------ // + +export namespace Enum { + // -- no types -- +} + +// ------------------------------------------------------------ // +// InputObject // +// ------------------------------------------------------------ // + +export namespace InputObject { + // -- no types -- +} + +// ------------------------------------------------------------ // +// Interface // +// ------------------------------------------------------------ // + +export namespace Interface { + // -- no types -- +} + +// ------------------------------------------------------------ // +// Object // +// ------------------------------------------------------------ // + +export namespace Object { + // -- no types -- +} + +// ------------------------------------------------------------ // +// Union // +// ------------------------------------------------------------ // + +export namespace Union { + // -- no types -- +} diff --git a/tests/_/schemaQueryOnly/generated/SchemaRuntime.ts b/tests/_/schemaQueryOnly/generated/SchemaRuntime.ts new file mode 100644 index 000000000..8826aa9e4 --- /dev/null +++ b/tests/_/schemaQueryOnly/generated/SchemaRuntime.ts @@ -0,0 +1,21 @@ +/* eslint-disable */ + +import * as $ from '../../../../src/Schema/__.js' +import * as $Scalar from './Scalar.js' + +// @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 Query = $.Object$(`Query`, { + id: $.field($.Output.Nullable($Scalar.ID)), + idNonNull: $.field($Scalar.ID), +}) + +export const $Index = { + Root: { + Query, + Mutation: null, + Subscription: null, + }, + objects: {}, + unions: {}, + interfaces: {}, +} diff --git a/tests/_/schemaQueryOnly/schema.graphql b/tests/_/schemaQueryOnly/schema.graphql new file mode 100644 index 000000000..a17a05bf7 --- /dev/null +++ b/tests/_/schemaQueryOnly/schema.graphql @@ -0,0 +1,4 @@ +type Query { + id: ID + idNonNull: ID! +} \ No newline at end of file diff --git a/tests/_/schemaQueryOnly/schema.ts b/tests/_/schemaQueryOnly/schema.ts new file mode 100644 index 000000000..2f2ddbdbc --- /dev/null +++ b/tests/_/schemaQueryOnly/schema.ts @@ -0,0 +1,25 @@ +import SchemaBuilder from '@pothos/core' + +export const db = { + id1: `abc`, +} + +const builder = new SchemaBuilder<{ + DefaultFieldNullability: true +}>({ + defaultFieldNullability: true, +}) + +builder.queryType({ + fields: t => ({ + id: t.id({ resolve: () => db.id1 }), + idNonNull: t.id({ nullable: false, resolve: () => db.id1 }), + }), +}) + +export const schema = builder.toSchema({ + sortSchema: true, +}) + +export { Index } from './generated/Index.js' +export { $Index } from './generated/SchemaRuntime.js' From d077576ebfb8d506f16f9ae841b4221e468b87e4 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 17 Apr 2024 00:17:12 +0200 Subject: [PATCH 10/12] refactor --- src/client/client.document.test.ts | 52 +++++++++++++----------------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/src/client/client.document.test.ts b/src/client/client.document.test.ts index c35c0eac6..b3a119ea1 100644 --- a/src/client/client.document.test.ts +++ b/src/client/client.document.test.ts @@ -18,49 +18,41 @@ describe(`document with two queries`, () => { test(`works`, async () => { const { run } = withTwo - expect(await run(`foo`)).toEqual({ data: { id: db.id1 } }) - expect(await run(`bar`)).toEqual({ data: { idNonNull: db.id1 } }) + await expect(run(`foo`)).resolves.toEqual({ data: { id: db.id1 } }) + await expect(run(`bar`)).resolves.toEqual({ data: { idNonNull: db.id1 } }) }) test(`error if no operation name is provided`, async () => { const { run } = withTwo // @ts-expect-error - const result = await run().catch((e: unknown) => e) as { errors: Error[] } - expect(result.errors.length).toBe(1) - const error = result.errors[0]! - expect(error instanceof Error).toBe(true) - expect(error.message).toMatch(`Must provide operation name if query contains multiple operations`) + await expect(run()).resolves.toMatchObject({ + errors: [{ message: `Must provide operation name if query contains multiple operations.` }], + }) }) test(`error if wrong operation name is provided`, async () => { const { run } = withTwo // @ts-expect-error - const result = await run(`boo`).catch((e: unknown) => e) as { errors: Error[] } - expect(result.errors.length).toBe(1) - const error = result.errors[0]! - expect(error instanceof Error).toBe(true) - expect(error.message).toMatch(`Unknown operation named "boo"`) + await expect(run(`boo`)).resolves.toMatchObject({ errors: [{ message: `Unknown operation named "boo".` }] }) + }) + test(`error if invalid name in document`, async () => { + const { run } = client.document({ foo$: { query: { id: true } } }) + await expect(run(`foo$`)).resolves.toMatchObject({ + errors: [{ message: `Syntax Error: Expected "{", found "$".` }], + }) }) }) test(`document with one query`, async () => { - const { run } = client.document({ - foo: { - query: { id: true }, - }, - }) - expect(await run(`foo`)).toEqual({ data: { id: db.id1 } }) - expect(await run()).toEqual({ data: { id: db.id1 } }) - expect(await run(undefined)).toEqual({ data: { id: db.id1 } }) + const { run } = client.document({ foo: { query: { id: true } } }) + await expect(run(`foo`)).resolves.toEqual({ data: { id: db.id1 } }) + await expect(run()).resolves.toEqual({ data: { id: db.id1 } }) + await expect(run(undefined)).resolves.toEqual({ data: { id: db.id1 } }) }) test(`document with one mutation`, async () => { - const { run } = client.document({ - foo: { - mutation: { id: true }, - }, - }) - expect(await run(`foo`)).toEqual({ data: { id: db.id1 } }) - expect(await run()).toEqual({ data: { id: db.id1 } }) - expect(await run(undefined)).toEqual({ data: { id: db.id1 } }) + const { run } = client.document({ foo: { mutation: { id: true } } }) + await expect(run(`foo`)).resolves.toEqual({ data: { id: db.id1 } }) + await expect(run()).resolves.toEqual({ data: { id: db.id1 } }) + await expect(run(undefined)).resolves.toEqual({ data: { id: db.id1 } }) }) test(`document with one mutation and one query`, async () => { @@ -72,6 +64,6 @@ test(`document with one mutation and one query`, async () => { query: { idNonNull: true }, }, }) - expect(await run(`foo`)).toEqual({ data: { id: db.id1 } }) - expect(await run(`bar`)).toEqual({ data: { idNonNull: db.id1 } }) + await expect(run(`foo`)).resolves.toEqual({ data: { id: db.id1 } }) + await expect(run(`bar`)).resolves.toEqual({ data: { idNonNull: db.id1 } }) }) From 02fe4367da89523521fca350c787541adf713492 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 17 Apr 2024 00:54:30 +0200 Subject: [PATCH 11/12] last test --- src/Schema/core/Named/NamedType.test-d.ts | 1 + src/Schema/core/Named/NamedType.ts | 18 +++++++++--------- src/client/client.document.test-d.ts | 2 +- src/client/client.document.test.ts | 1 + src/client/client.ts | 19 +++++++++++++++++-- 5 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/Schema/core/Named/NamedType.test-d.ts b/src/Schema/core/Named/NamedType.test-d.ts index df28fbcde..e0c5b61cb 100644 --- a/src/Schema/core/Named/NamedType.test-d.ts +++ b/src/Schema/core/Named/NamedType.test-d.ts @@ -14,4 +14,5 @@ test(`NameParse`, () => { expectTypeOf>().toEqualTypeOf() expectTypeOf>().toEqualTypeOf() expectTypeOf>().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() }) diff --git a/src/Schema/core/Named/NamedType.ts b/src/Schema/core/Named/NamedType.ts index 09a761869..628edb952 100644 --- a/src/Schema/core/Named/NamedType.ts +++ b/src/Schema/core/Named/NamedType.ts @@ -4,18 +4,18 @@ import type { Digit, Letter } from '../../../lib/prelude.js' * @see http://spec.graphql.org/draft/#sec-Names */ // dprint-ignore -export type NameParse = - T extends NameHead ? T : - T extends `${NameHead}${infer Rest}` ? Rest extends NameBodyParse ? T - : never - : never +export type NameParse = + S extends NameHead ? S : + S extends `${NameHead}${infer Rest}` ? NameBodyParse extends never ? never : + S : + never // dprint-ignore -export type NameBodyParse = +type NameBodyParse = S extends NameBody ? S : - S extends `${NameBody}${infer Rest}` ? NameBodyParse extends string ? S - : never - : never + S extends `${NameBody}${infer Rest}` ? NameBodyParse extends never ? never : + S : + never export type NameHead = Letter | '_' export type NameBody = Letter | '_' | Digit diff --git a/src/client/client.document.test-d.ts b/src/client/client.document.test-d.ts index c60470031..69644ce62 100644 --- a/src/client/client.document.test-d.ts +++ b/src/client/client.document.test-d.ts @@ -42,7 +42,7 @@ describe(`input`, () => { schemaIndex: SchemaMutationOnly.$Index, }) clientMutationOnly.document({ - // @ts-expect-error mutation not in schema + // @ts-expect-error query not in schema foo: { query: { id: true } }, bar: { mutation: { id: true } }, }) diff --git a/src/client/client.document.test.ts b/src/client/client.document.test.ts index b3a119ea1..88b69d972 100644 --- a/src/client/client.document.test.ts +++ b/src/client/client.document.test.ts @@ -34,6 +34,7 @@ describe(`document with two queries`, () => { await expect(run(`boo`)).resolves.toMatchObject({ errors: [{ message: `Unknown operation named "boo".` }] }) }) test(`error if invalid name in document`, async () => { + // @ts-expect-error const { run } = client.document({ foo$: { query: { id: true } } }) await expect(run(`foo$`)).resolves.toMatchObject({ errors: [{ message: `Syntax Error: Expected "{", found "$".` }], diff --git a/src/client/client.ts b/src/client/client.ts index 949509656..79bcde172 100644 --- a/src/client/client.ts +++ b/src/client/client.ts @@ -4,6 +4,7 @@ import type { ExcludeUndefined } from 'type-fest/source/required-deep.js' import request from '../entrypoints/main.js' import { type RootTypeName } from '../lib/graphql.js' import type { Exact, IsMultipleKeys } from '../lib/prelude.js' +import type { TSError } from '../lib/TSError.js' import type { Object$2, Schema } from '../Schema/__.js' import * as CustomScalars from './customScalars.js' import { toDocumentExpression } from './document.js' @@ -56,13 +57,27 @@ type GetOperation = T extends {mutation:infer U} ? U : never +// dprint-ignore +type ValidateDocumentOperationNames<$Document> = + // This initial condition checks that the document is not already in an error state. + // Namely from for example { x: { mutation: { ... }}} where the schema has no mutations. + // Which is statically caught by the `Document` type. In that case the document type variable + // no longer functions per normal with regards to keyof utility, not returning exact keys of the object + // but instead this more general union. Not totally clear _why_, but we have tests covering this... + string | number extends keyof $Document + ? $Document + : keyof { [K in keyof $Document & string as Schema.Named.NameParse extends never ? K : never]: K } extends never + ? $Document + : TSError<'ValidateDocumentOperationNames', `One or more Invalid operation name in document: ${keyof { [K in keyof $Document & string as Schema.Named.NameParse extends never ? K : never]: K }}`> + // dprint-ignore export type Client<$Index extends Schema.Index> = & { // todo test raw - raw: (document: string|DocumentNode, variables?:Variables) => Promise + raw: (document: string | DocumentNode, variables?:Variables) => Promise document: <$Document extends Document<$Index>> - (document: NonEmptyObject<$Document>) => + (document: ValidateDocumentOperationNames>) => + // (document: $Document) => { run: <$Name extends keyof $Document & string, $Params extends (IsMultipleKeys<$Document> extends true ? [name: $Name] : ([] | [name: $Name | undefined]))> (...params: $Params) => From 6dfa41b2c5ac83b6f1285adf5837edbee7027b99 Mon Sep 17 00:00:00 2001 From: Jason Kuhrt Date: Wed, 17 Apr 2024 00:56:27 +0200 Subject: [PATCH 12/12] no lint generated code --- eslint.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/eslint.config.js b/eslint.config.js index 2ee112999..8313e1302 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,7 +2,7 @@ import configPrisma from 'eslint-config-prisma' import tsEslint from 'typescript-eslint' export default tsEslint.config({ - ignores: ['**/build/**/*', 'eslint.config.js', 'vite.config.ts'], + ignores: ['**/build/**/*', 'eslint.config.js', 'vite.config.ts', '**/generated/**/*'], extends: configPrisma, languageOptions: { parserOptions: {