From 2d4d89ac6f7ecb9eceffdb30e95ec4071394c5a2 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Fri, 21 Jan 2022 13:07:19 +0100 Subject: [PATCH 01/17] first draft --- .../typescript/typed-document-sdk/.gitignore | 1 + .../typed-document-sdk/CHANGELOG.md | 1 + .../typed-document-sdk/jest.config.js | 1 + .../typed-document-sdk/package.json | 47 +++ .../typed-document-sdk/scripts/write-sdk.js | 23 ++ .../buildObjectTypeSelectionString.spec.ts | 91 +++++ .../src/buildObjectTypeSelectionString.ts | 74 ++++ .../src/buildSDKObjectString.spec.ts | 94 +++++ .../src/buildSDKObjectString.ts | 26 ++ .../typed-document-sdk/src/config.ts | 1 + .../typed-document-sdk/src/index.ts | 40 ++ .../typed-document-sdk/src/sdk-base.spec.ts | 349 ++++++++++++++++++ .../typed-document-sdk/src/sdk-base.ts | 259 +++++++++++++ 13 files changed, 1007 insertions(+) create mode 100644 packages/plugins/typescript/typed-document-sdk/.gitignore create mode 100644 packages/plugins/typescript/typed-document-sdk/CHANGELOG.md create mode 100644 packages/plugins/typescript/typed-document-sdk/jest.config.js create mode 100644 packages/plugins/typescript/typed-document-sdk/package.json create mode 100644 packages/plugins/typescript/typed-document-sdk/scripts/write-sdk.js create mode 100644 packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.spec.ts create mode 100644 packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.ts create mode 100644 packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.spec.ts create mode 100644 packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.ts create mode 100644 packages/plugins/typescript/typed-document-sdk/src/config.ts create mode 100644 packages/plugins/typescript/typed-document-sdk/src/index.ts create mode 100644 packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts create mode 100644 packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts diff --git a/packages/plugins/typescript/typed-document-sdk/.gitignore b/packages/plugins/typescript/typed-document-sdk/.gitignore new file mode 100644 index 00000000000..62c40f91234 --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/.gitignore @@ -0,0 +1 @@ +src/sdk-static.ts diff --git a/packages/plugins/typescript/typed-document-sdk/CHANGELOG.md b/packages/plugins/typescript/typed-document-sdk/CHANGELOG.md new file mode 100644 index 00000000000..b40de4997d1 --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/CHANGELOG.md @@ -0,0 +1 @@ +# @graphql-codegen/typed-document-sdk diff --git a/packages/plugins/typescript/typed-document-sdk/jest.config.js b/packages/plugins/typescript/typed-document-sdk/jest.config.js new file mode 100644 index 00000000000..e35d633e27f --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/jest.config.js @@ -0,0 +1 @@ +module.exports = require('../../../../jest.project')({ dirname: __dirname }); \ No newline at end of file diff --git a/packages/plugins/typescript/typed-document-sdk/package.json b/packages/plugins/typescript/typed-document-sdk/package.json new file mode 100644 index 00000000000..9e16ce20866 --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/package.json @@ -0,0 +1,47 @@ +{ + "name": "@graphql-codegen/typed-document-sdk", + "version": "0.0.0", + "description": "GraphQL Code Generator plugin for generating a TypedDocumentNode builder SDK.", + "repository": { + "type": "git", + "url": "https://github.com/dotansimha/graphql-code-generator.git", + "directory": "packages/plugins/typescript/typed-document-sdk" + }, + "license": "MIT", + "scripts": { + "lint": "eslint **/*.ts", + "test": "jest --no-watchman --config ../../../../jest.config.js", + "prepack": "bob prepack" + }, + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" + }, + "dependencies": { + "@graphql-codegen/plugin-helpers": "^2.3.2", + "common-tags": "^1.8.0" + }, + "main": "dist/index.js", + "module": "dist/index.mjs", + "exports": { + "./package.json": "./package.json", + ".": { + "require": "./dist/index.js", + "import": "./dist/index.mjs" + }, + "./*": { + "require": "./dist/*.js", + "import": "./dist/*.mjs" + } + }, + "typings": "dist/index.d.ts", + "typescript": { + "definition": "dist/index.d.ts" + }, + "buildOptions": { + "input": "./src/index.ts" + }, + "publishConfig": { + "directory": "dist", + "access": "public" + } +} diff --git a/packages/plugins/typescript/typed-document-sdk/scripts/write-sdk.js b/packages/plugins/typescript/typed-document-sdk/scripts/write-sdk.js new file mode 100644 index 00000000000..d4c1c547e48 --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/scripts/write-sdk.js @@ -0,0 +1,23 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); + +const sdkBasePath = path.resolve(__dirname, '..', 'src', 'sdk-base.ts'); +const sdkOutPath = path.resolve(__dirname, '..', 'src', 'sdk-static.ts'); + +const sdkBaseContents = fs.readFileSync(sdkBasePath, 'utf-8'); + +const [imports, body] = sdkBaseContents.split('// IMPORTS END'); + +if (body === undefined) { + throw new Error(`Missing '// IMPORTS END' that splits the body from the head within '${sdkBasePath}'.`); +} + +fs.writeFileSync( + sdkOutPath, + ` +export const importsString = \`${imports}\` +export const contentsString = \`${body}\` +` +); diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.spec.ts b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.spec.ts new file mode 100644 index 00000000000..7fcdab92a0f --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.spec.ts @@ -0,0 +1,91 @@ +import { buildObjectTypeSelectionString } from './buildObjectTypeSelectionString'; +import { GraphQLInt, GraphQLNonNull, GraphQLObjectType } from 'graphql'; + +describe('buildObjectTypeSelectionString', () => { + it('primitive field', () => { + const graphQLObjectType = new GraphQLObjectType({ + name: 'Hello', + fields: { + a: { + type: GraphQLInt, + }, + }, + }); + + expect(buildObjectTypeSelectionString(graphQLObjectType)).toMatchInlineSnapshot(` + "type SDKHelloSelectionSet = SDKSelectionSet<{ + __typename?: true; + a?: true; + }>;" + `); + }); + it('object field', () => { + const graphQLObjectType = new GraphQLObjectType({ + name: 'Hello', + fields: () => ({ + a: { + type: graphQLObjectType, + }, + }), + }); + + expect(buildObjectTypeSelectionString(graphQLObjectType)).toMatchInlineSnapshot(` + "type SDKHelloSelectionSet = SDKSelectionSet<{ + __typename?: true; + a?: SDKHelloSelectionSet; + }>;" + `); + }); + it('primitive field with optional arg.', () => { + const graphQLObjectType = new GraphQLObjectType({ + name: 'Hello', + fields: () => ({ + a: { + type: GraphQLInt, + args: { + arg: { + type: GraphQLInt, + }, + }, + }, + }), + }); + + expect(buildObjectTypeSelectionString(graphQLObjectType)).toMatchInlineSnapshot(` + "type SDKHelloSelectionSet = SDKSelectionSet<{ + __typename?: true; + a?: true | { + [SDKFieldArgumentSymbol]?: { + arg?: true; + } + }; + }>;" + `); + }); + it('primitive field with required arg.', () => { + const graphQLObjectType = new GraphQLObjectType({ + name: 'Hello', + fields: () => ({ + a: { + type: GraphQLInt, + args: { + arg: { + type: new GraphQLNonNull(GraphQLInt), + }, + }, + }, + }), + }); + + expect(buildObjectTypeSelectionString(graphQLObjectType)).toMatchInlineSnapshot(` + "type SDKHelloSelectionSet = SDKSelectionSet<{ + __typename?: true; + a?: { + [SDKFieldArgumentSymbol]: { + arg: true; + } + }; + }>;" + `); + }); +}); diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.ts b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.ts new file mode 100644 index 00000000000..e85e52874b7 --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.ts @@ -0,0 +1,74 @@ +import { stripIndent } from 'common-tags'; +import { + getNamedType, + GraphQLField, + GraphQLObjectType, + isInterfaceType, + isNonNullType, + isObjectType, + isUnionType, +} from 'graphql'; + +export const buildObjectSelectionSetName = (name: string) => `SDK${name}SelectionSet`; + +const buildFieldSelectionSetString = (field: GraphQLField): string => { + const resultType = getNamedType(field.type); + + let value = `true`; + + if (isInterfaceType(resultType) || isUnionType(resultType)) { + throw new Error('Not yet supported. Interfaces and Union.'); + } + + if (isObjectType(resultType)) { + value = buildObjectSelectionSetName(resultType.name); + } + + if (field.args.length) { + let requireArguments = false; + const argumentPartials: Array = []; + + for (const arg of field.args) { + const isNonNull = isNonNullType(arg.type); + requireArguments = isNonNull === true || requireArguments; + argumentPartials.push(`${arg.name}${isNonNull ? `` : `?`}: true`); + } + + const args = stripIndent` + { + [SDKFieldArgumentSymbol]${requireArguments ? `` : `?`}: { + ${argumentPartials.join(`;\n`)}; + } + } + ` + .split(`\n`) + .map((line, i) => (i === 0 ? line : ` ${line}`)) + .join(`\n`); + + if (value === `true`) { + if (requireArguments) { + value = args; + } else { + value = `${value} | ${args}`; + } + } else { + value = `${value} & ${args}`; + } + } + + return `${field.name}?: ${value};`; +}; + +export const buildObjectTypeSelectionString = (objectType: GraphQLObjectType): string => { + const fields: Array = []; + for (const field of Object.values(objectType.getFields())) { + fields.push(buildFieldSelectionSetString(field)); + } + + return stripIndent` + type ${buildObjectSelectionSetName(objectType.name)} = SDKSelectionSet<{ + __typename?: true; + ${fields.join(`;\n `)} + }>; + `; +}; diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.spec.ts b/packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.spec.ts new file mode 100644 index 00000000000..ce58a10e4a9 --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.spec.ts @@ -0,0 +1,94 @@ +import { GraphQLBoolean, GraphQLObjectType } from 'graphql'; +import { buildSDKObjectString } from './buildSDKObjectString'; + +describe('buildSDKObjectString', () => { + it('missing query type', () => { + expect(() => { + buildSDKObjectString(null, null, null); + }).toThrow(); + }); + it('existing query type', () => { + const objectType = new GraphQLObjectType({ + name: 'QueryRoot', + fields: { + a: { + type: GraphQLBoolean, + }, + }, + }); + expect(buildSDKObjectString(objectType, null, null)).toMatchInlineSnapshot(` + "export const sdk = createSDK< + SDKQueryRootSelectionSet, + QueryRoot, + void, + void, + void, + void, + >()" + `); + }); + it('existing query and mutation type', () => { + const queryType = new GraphQLObjectType({ + name: 'QueryRoot', + fields: { + a: { + type: GraphQLBoolean, + }, + }, + }); + const mutationType = new GraphQLObjectType({ + name: 'MutationRoot', + fields: { + a: { + type: GraphQLBoolean, + }, + }, + }); + expect(buildSDKObjectString(queryType, mutationType, null)).toMatchInlineSnapshot(` + "export const sdk = createSDK< + SDKQueryRootSelectionSet, + QueryRoot, + SDKMutationRootSelectionSet, + MutationRoot, + void, + void, + >()" + `); + }); + it('existing query, mutation and subscription type', () => { + const queryType = new GraphQLObjectType({ + name: 'QueryRoot', + fields: { + a: { + type: GraphQLBoolean, + }, + }, + }); + const mutationType = new GraphQLObjectType({ + name: 'MutationRoot', + fields: { + a: { + type: GraphQLBoolean, + }, + }, + }); + const subscriptionType = new GraphQLObjectType({ + name: 'Subscription', + fields: { + a: { + type: GraphQLBoolean, + }, + }, + }); + expect(buildSDKObjectString(queryType, mutationType, subscriptionType)).toMatchInlineSnapshot(` + "export const sdk = createSDK< + SDKQueryRootSelectionSet, + QueryRoot, + SDKMutationRootSelectionSet, + MutationRoot, + SDKSubscriptionSelectionSet, + Subscription, + >()" + `); + }); +}); diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.ts b/packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.ts new file mode 100644 index 00000000000..38aab9701e9 --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.ts @@ -0,0 +1,26 @@ +import { stripIndent } from 'common-tags'; +import { GraphQLObjectType } from 'graphql'; +import { buildObjectSelectionSetName } from './buildObjectTypeSelectionString'; + +type Maybe = T | null | undefined; + +export const buildSDKObjectString = ( + queryType: Maybe, + mutationType: Maybe, + subscriptionType: Maybe +): string => { + if (!queryType) { + throw new TypeError('Query type is missing.'); + } + + return stripIndent` + export const sdk = createSDK< + ${buildObjectSelectionSetName(queryType.name)}, + ${queryType.name}, + ${mutationType ? buildObjectSelectionSetName(mutationType.name) : 'void'}, + ${mutationType ? mutationType : 'void'}, + ${subscriptionType ? buildObjectSelectionSetName(subscriptionType.name) : 'void'}, + ${subscriptionType ? subscriptionType : 'void'}, + >() + `; +}; diff --git a/packages/plugins/typescript/typed-document-sdk/src/config.ts b/packages/plugins/typescript/typed-document-sdk/src/config.ts new file mode 100644 index 00000000000..6e339d355ca --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/src/config.ts @@ -0,0 +1 @@ +export interface TypedDocumentSDKConfig {} diff --git a/packages/plugins/typescript/typed-document-sdk/src/index.ts b/packages/plugins/typescript/typed-document-sdk/src/index.ts new file mode 100644 index 00000000000..77be6710ba6 --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/src/index.ts @@ -0,0 +1,40 @@ +import { Types, PluginValidateFn, PluginFunction } from '@graphql-codegen/plugin-helpers'; +import { GraphQLSchema, isEnumType, isObjectType, isScalarType } from 'graphql'; +import { buildObjectTypeSelectionString } from './buildObjectTypeSelectionString'; +import { buildSDKObjectString } from './buildSDKObjectString'; +import { TypedDocumentSDKConfig } from './config'; +import { importsString, contentsString } from './sdk-static'; + +export const plugin: PluginFunction = ( + schema: GraphQLSchema, + _: Types.DocumentFile[], + _config: TypedDocumentSDKConfig +) => { + const contents: Array = [contentsString]; + const inputTypeMap: Array = []; + + for (const graphQLType of Object.values(schema.getTypeMap())) { + // selection set objects + if (isObjectType(graphQLType)) { + contents.push(buildObjectTypeSelectionString(graphQLType)); + } + // input types + if (isScalarType(graphQLType) || isEnumType(graphQLType) || isScalarType(graphQLType)) { + inputTypeMap.push(graphQLType.name, graphQLType.name); + } + } + + contents.push(`type SDKInputTypes =${inputTypeMap.join(`\n | `)}`); + + // sdk object + contents.push(buildSDKObjectString(schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType())); + + return { + prepend: [importsString], + content: contents.join(`\n\n`), + }; +}; + +export const validate: PluginValidateFn = async () => {}; + +export { TypedDocumentSDKConfig }; diff --git a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts new file mode 100644 index 00000000000..ae7e4aa77e1 --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts @@ -0,0 +1,349 @@ +import { DocumentNode, Kind, OperationTypeNode } from 'graphql'; +import { createSDK, SDKFieldArgumentSymbol, SDKSelectionSet } from './sdk-base'; + +describe('SDKLogic', () => { + it('anonymous query operation', () => { + const sdk = createSDK< + string, + SDKSelectionSet<{ + __typename?: true; + }>, + { + __typename?: 'Query'; + } + >(); + const operation = sdk.query({ + selection: { + __typename: true, + }, + }); + + expect(operation).toStrictEqual({ + kind: Kind.DOCUMENT, + definitions: [ + { + kind: Kind.OPERATION_DEFINITION, + name: undefined, + operation: OperationTypeNode.QUERY, + variableDefinitions: [], + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: '__typename', + }, + }, + ], + }, + }, + ], + }); + }); + + it('named query operation', () => { + const sdk = createSDK< + string, + SDKSelectionSet<{ + __typename?: true; + }>, + { + __typename?: 'Query'; + } + >(); + const operation = sdk.query({ + name: 'Brrt', + selection: { + __typename: true, + }, + }); + + expect(operation).toStrictEqual({ + kind: Kind.DOCUMENT, + definitions: [ + { + kind: Kind.OPERATION_DEFINITION, + name: { + kind: Kind.NAME, + value: 'Brrt', + }, + operation: OperationTypeNode.QUERY, + variableDefinitions: [], + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: '__typename', + }, + }, + ], + }, + }, + ], + }); + }); + + it('simple mutation operation', () => { + type SelectionType = SDKSelectionSet<{ + __typename?: true; + }>; + type ResultType = { + __typename?: 'Query'; + }; + + const sdk = createSDK(); + const operation = sdk.mutation({ + selection: { + __typename: true, + }, + }); + + expect(operation).toStrictEqual({ + kind: Kind.DOCUMENT, + definitions: [ + { + kind: Kind.OPERATION_DEFINITION, + name: undefined, + operation: OperationTypeNode.MUTATION, + variableDefinitions: [], + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: '__typename', + }, + }, + ], + }, + }, + ], + }); + }); + + it('simple subscription operation', () => { + type SelectionType = SDKSelectionSet<{ + __typename?: true; + }>; + type ResultType = { + __typename?: 'Query'; + }; + + const sdk = createSDK(); + const operation = sdk.subscription({ + selection: { + __typename: true, + }, + }); + + expect(operation).toStrictEqual({ + kind: Kind.DOCUMENT, + definitions: [ + { + kind: Kind.OPERATION_DEFINITION, + name: undefined, + operation: OperationTypeNode.SUBSCRIPTION, + variableDefinitions: [], + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: '__typename', + }, + }, + ], + }, + }, + ], + }); + }); + + it('nested operation', () => { + const sdk = createSDK< + string, + SDKSelectionSet<{ + __typename?: true; + foo?: { + a?: true; + }; + }>, + { + __typename?: 'Query'; + foo?: { + a?: boolean; + }; + } + >(); + + const operation = sdk.query({ + selection: { + __typename: true, + foo: { + a: true, + }, + }, + }); + + expect(operation).toStrictEqual({ + kind: Kind.DOCUMENT, + definitions: [ + { + kind: Kind.OPERATION_DEFINITION, + name: undefined, + operation: OperationTypeNode.QUERY, + variableDefinitions: [], + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: '__typename', + }, + }, + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: 'foo', + }, + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: 'a', + }, + }, + ], + }, + }, + ], + }, + }, + ], + }); + }); + + it('query with variables', () => { + type SelectionType = SDKSelectionSet<{ + __typename?: true; + user?: SDKSelectionSet<{ + id?: boolean; + }> & { + [SDKFieldArgumentSymbol]?: { + id?: 'String'; + }; + }; + }>; + type ResultType = { + __typename?: 'Query'; + }; + type InputTypes = 'String' | 'Int' | 'Boolean'; + + const sdk = createSDK(); + + const document = sdk.query({ + name: 'UserById', + variables: { + idVariableName: 'String', + }, + selection: { + user: { + [SDKFieldArgumentSymbol]: { + id: 'idVariableName', + }, + id: true, + }, + }, + }); + + const expectedDocument: DocumentNode = { + kind: Kind.DOCUMENT, + definitions: [ + { + kind: Kind.OPERATION_DEFINITION, + name: { + kind: Kind.NAME, + value: 'UserById', + }, + operation: OperationTypeNode.QUERY, + variableDefinitions: [ + { + kind: Kind.VARIABLE_DEFINITION, + type: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'String', + }, + }, + variable: { + kind: Kind.VARIABLE, + name: { + kind: Kind.NAME, + value: 'idVariableName', + }, + }, + }, + ], + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: 'user', + }, + arguments: [ + { + kind: Kind.ARGUMENT, + name: { + kind: Kind.NAME, + value: 'id', + }, + value: { + kind: Kind.VARIABLE, + name: { + kind: Kind.NAME, + value: 'idVariableName', + }, + }, + }, + ], + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: 'id', + }, + }, + ], + }, + }, + ], + }, + }, + ], + }; + + expect(document).toStrictEqual(expectedDocument); + }); +}); diff --git a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts new file mode 100644 index 00000000000..45af1d9f3d4 --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts @@ -0,0 +1,259 @@ +import type { TypedDocumentNode } from '@graphql-typed-document-node/core'; +import type { + ArgumentNode, + DocumentNode, + FieldNode, + Kind, + OperationTypeNode, + SelectionNode, + SelectionSetNode, + VariableDefinitionNode, +} from 'graphql'; +// IMPORTS END + +type Mutable = { -readonly [P in keyof T]: T[P] }; + +type Impossible = { + [P in K]: never; +}; + +/** Do not allow any other properties as the ones defined in the base type. */ +type NoExtraProperties = U & Impossible>; + +/** Require at least one property in the object defined. */ +type AtLeastOnePropertyOf = { + [K in keyof T]: { [L in K]-?: T[L] } & { [L in Exclude]?: T[L] }; +}[keyof T]; + +type SDKSelection = { [key: string]: any }; + +type ResultType = { [key: string]: any }; + +type SDKOperationType = { + [TKey in keyof TSelection]: TKey extends keyof TType + ? TType[TKey] extends Record + ? SDKOperationType + : TType[TKey] + : never; +}; + +type InternalSDKSelectionSet = Record; + +export type SDKSelectionSet = any> = AtLeastOnePropertyOf>; + +type SDKArgumentType = {}; // TODO IMPLEMENT + +type SDKSelectionTypedDocumentNode = TypedDocumentNode< + SDKOperationType, + SDKArgumentType +>; + +export const SDKFieldArgumentSymbol = Symbol('SDKFieldArguments'); + +type SDKVariableDefinitions = { [key: string]: TInputTypes }; + +type SDK< + SDKInputTypes extends string, + SDKQuerySelectionSet extends SDKSelectionSet, + QueryResultType extends ResultType, + SDKMutationSelectionSet extends SDKSelectionSet | void = void, + MutationResultType extends ResultType | void = void, + SDKSubscriptionSelectionSet extends SDKSelectionSet | void = void, + SubscriptionResultType extends ResultType | void = void +> = { + query>( + args: ( + | { + name: string; + variables?: TVariableDefinitions; + } + | { + name?: never; + variables?: never; + } + ) & { + selection: TSelection; + } + ): SDKSelectionTypedDocumentNode; +} & (SDKMutationSelectionSet extends SDKSelectionSet + ? MutationResultType extends ResultType + ? { + mutation< + TSelection extends SDKMutationSelectionSet, + TVariableDefinitions extends SDKVariableDefinitions + >( + args: ( + | { + name: string; + variables?: TVariableDefinitions; + } + | { + name?: never; + variables?: never; + } + ) & { + selection: TSelection; + } + ): SDKSelectionTypedDocumentNode; + } + : {} + : {}) & + (SDKSubscriptionSelectionSet extends SDKSelectionSet + ? SubscriptionResultType extends ResultType + ? { + subscription< + TSelection extends SDKSubscriptionSelectionSet, + TVariableDefinitions extends SDKVariableDefinitions + >( + args: ( + | { + name: string; + variables?: TVariableDefinitions; + } + | { + name?: never; + variables?: never; + } + ) & { + selection: TSelection; + } + ): SDKSelectionTypedDocumentNode; + } + : {} + : {}); + +const getBaseDocument = ( + operation: 'query' | 'mutation' | 'subscription', + name: string | undefined, + variableDefinitions: Array, + selectionSet: SelectionSetNode +): DocumentNode => ({ + kind: 'Document' as Kind.DOCUMENT, + definitions: [ + { + kind: 'OperationDefinition' as Kind.OPERATION_DEFINITION, + name: name + ? { + kind: 'Name' as Kind.NAME, + value: name, + } + : undefined, + operation: operation as OperationTypeNode, + variableDefinitions, + selectionSet, + }, + ], +}); + +const buildSelectionSet = (sdkSelectionSet: InternalSDKSelectionSet): SelectionSetNode => { + const selections: Array = []; + + for (const [fieldName, selectionValue] of Object.entries(sdkSelectionSet)) { + const fieldNode: Mutable = { + kind: 'Field' as Kind.FIELD, + name: { + kind: 'Name' as Kind.NAME, + value: fieldName, + }, + }; + if (typeof selectionValue === 'object') { + fieldNode.selectionSet = buildSelectionSet(selectionValue); + + if (SDKFieldArgumentSymbol in selectionValue) { + const args: Array = []; + for (const [argumentName, variableName] of Object.entries(selectionValue[SDKFieldArgumentSymbol])) { + if (typeof variableName !== 'string') { + continue; + } + args.push({ + kind: 'Argument' as Kind.ARGUMENT, + name: { + kind: 'Name' as Kind.NAME, + value: argumentName, + }, + value: { + kind: 'Variable' as Kind.VARIABLE, + name: { + kind: 'Name' as Kind.NAME, + value: variableName, + }, + }, + }); + } + if (args.length) { + fieldNode.arguments = args; + } + } + } + selections.push(fieldNode); + } + + const selectionSet: SelectionSetNode = { + kind: 'SelectionSet' as Kind.SELECTION_SET, + selections, + }; + + return selectionSet; +}; + +const buildVariableDefinitions = (args: Record): Array => { + const variableDefinitions: Array = []; + for (const [variableName, inputType] of Object.entries(args)) { + variableDefinitions.push({ + kind: 'VariableDefinition' as Kind.VARIABLE_DEFINITION, + variable: { + kind: 'Variable' as Kind.VARIABLE, + name: { + kind: 'Name' as Kind.NAME, + value: variableName, + }, + }, + type: { + // TODO: ! (non-null) and [] (list) handling + kind: 'NamedType' as Kind.NAMED_TYPE, + name: { + kind: 'Name' as Kind.NAME, + value: inputType, + }, + }, + }); + } + + return variableDefinitions; +}; + +const sdkHandler = + (operationType: 'query' | 'mutation' | 'subscription') => + (args: { name?: string; variables?: Record; selection: SDKSelection }) => { + const variableDefinitions = buildVariableDefinitions(args.variables ?? {}); + const selectionSet = buildSelectionSet(args.selection); + + const document = getBaseDocument(operationType, args.name, variableDefinitions, selectionSet); + + // type as any so the TypeScript compiler has less work to do :) + return document as any; + }; + +export function createSDK< + SDKInputTypes extends string, + SDKQuerySelectionSet extends SDKSelectionSet, + QueryResultType extends ResultType, + SDKMutationSelectionSet extends SDKSelectionSet | void = void, + MutationResultType extends ResultType | void = void, + SDKSubscriptionSelectionSet extends SDKSelectionSet | void = void, + SubscriptionResultType extends ResultType | void = void +>(): SDK< + SDKInputTypes, + SDKQuerySelectionSet, + QueryResultType, + SDKMutationSelectionSet, + MutationResultType, + SDKSubscriptionSelectionSet, + SubscriptionResultType +> { + return { + query: sdkHandler('query'), + mutation: sdkHandler('mutation'), + subscription: sdkHandler('subscription'), + } as any; +} From 9548df9cd8ff63f29a046d4e4db9a78dfd8f4c0b Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Fri, 21 Jan 2022 15:24:06 +0100 Subject: [PATCH 02/17] type-safe variables --- .../src/buildObjectTypeSelectionString.ts | 2 +- .../src/buildSDKObjectString.ts | 1 + .../typed-document-sdk/src/index.ts | 16 +- .../typed-document-sdk/src/sdk-base.spec.ts | 26 +++- .../typed-document-sdk/src/sdk-base.ts | 145 ++++++++++++------ .../tests/typed-document-sdk.spec.ts | 9 ++ 6 files changed, 139 insertions(+), 60 deletions(-) create mode 100644 packages/plugins/typescript/typed-document-sdk/tests/typed-document-sdk.spec.ts diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.ts b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.ts index e85e52874b7..93fb5b82595 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.ts @@ -9,7 +9,7 @@ import { isUnionType, } from 'graphql'; -export const buildObjectSelectionSetName = (name: string) => `SDK${name}SelectionSet`; +export const buildObjectSelectionSetName = (name: string) => `GeneratedSDKSelectionSet${name}`; const buildFieldSelectionSetString = (field: GraphQLField): string => { const resultType = getNamedType(field.type); diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.ts b/packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.ts index 38aab9701e9..7df8e2a60b1 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.ts @@ -15,6 +15,7 @@ export const buildSDKObjectString = ( return stripIndent` export const sdk = createSDK< + GeneratedSDKInputTypes, ${buildObjectSelectionSetName(queryType.name)}, ${queryType.name}, ${mutationType ? buildObjectSelectionSetName(mutationType.name) : 'void'}, diff --git a/packages/plugins/typescript/typed-document-sdk/src/index.ts b/packages/plugins/typescript/typed-document-sdk/src/index.ts index 77be6710ba6..55cbc5137b7 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/index.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/index.ts @@ -1,5 +1,5 @@ import { Types, PluginValidateFn, PluginFunction } from '@graphql-codegen/plugin-helpers'; -import { GraphQLSchema, isEnumType, isObjectType, isScalarType } from 'graphql'; +import { GraphQLSchema, isEnumType, isInputObjectType, isObjectType, isScalarType } from 'graphql'; import { buildObjectTypeSelectionString } from './buildObjectTypeSelectionString'; import { buildSDKObjectString } from './buildSDKObjectString'; import { TypedDocumentSDKConfig } from './config'; @@ -19,12 +19,20 @@ export const plugin: PluginFunction = ( contents.push(buildObjectTypeSelectionString(graphQLType)); } // input types - if (isScalarType(graphQLType) || isEnumType(graphQLType) || isScalarType(graphQLType)) { - inputTypeMap.push(graphQLType.name, graphQLType.name); + if (isScalarType(graphQLType)) { + inputTypeMap.push(` ${graphQLType.name}: Scalars['${graphQLType.name}'];`); + } + + if (isEnumType(graphQLType)) { + inputTypeMap.push(` ${graphQLType.name}: ${graphQLType.name};`); + } + + if (isInputObjectType(graphQLType)) { + inputTypeMap.push(` ${graphQLType.name}: ${graphQLType.name};`); } } - contents.push(`type SDKInputTypes =${inputTypeMap.join(`\n | `)}`); + contents.push(`type GeneratedSDKInputTypes = {\n${inputTypeMap.join('')}`); // sdk object contents.push(buildSDKObjectString(schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType())); diff --git a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts index ae7e4aa77e1..a7f586cc77e 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts @@ -4,7 +4,7 @@ import { createSDK, SDKFieldArgumentSymbol, SDKSelectionSet } from './sdk-base'; describe('SDKLogic', () => { it('anonymous query operation', () => { const sdk = createSDK< - string, + {}, SDKSelectionSet<{ __typename?: true; }>, @@ -45,7 +45,7 @@ describe('SDKLogic', () => { it('named query operation', () => { const sdk = createSDK< - string, + {}, SDKSelectionSet<{ __typename?: true; }>, @@ -96,7 +96,7 @@ describe('SDKLogic', () => { __typename?: 'Query'; }; - const sdk = createSDK(); + const sdk = createSDK<{}, SelectionType, ResultType, SelectionType, ResultType>(); const operation = sdk.mutation({ selection: { __typename: true, @@ -136,7 +136,7 @@ describe('SDKLogic', () => { __typename?: 'Query'; }; - const sdk = createSDK(); + const sdk = createSDK<{}, SelectionType, ResultType, SelectionType, ResultType, SelectionType, ResultType>(); const operation = sdk.subscription({ selection: { __typename: true, @@ -170,7 +170,7 @@ describe('SDKLogic', () => { it('nested operation', () => { const sdk = createSDK< - string, + {}, SDKSelectionSet<{ __typename?: true; foo?: { @@ -238,21 +238,31 @@ describe('SDKLogic', () => { }); }); - it('query with variables', () => { + it('query with primitive variables', () => { + type InputTypes = { + String: string; + Int: number; + Boolean: number; + }; + type SelectionType = SDKSelectionSet<{ __typename?: true; user?: SDKSelectionSet<{ id?: boolean; + login?: boolean; }> & { - [SDKFieldArgumentSymbol]?: { + [SDKFieldArgumentSymbol]: { id?: 'String'; }; }; }>; type ResultType = { __typename?: 'Query'; + user?: { + id?: InputTypes['String']; + login?: InputTypes['String']; + }; }; - type InputTypes = 'String' | 'Int' | 'Boolean'; const sdk = createSDK(); diff --git a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts index 45af1d9f3d4..00f55a713a6 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts @@ -25,6 +25,13 @@ type AtLeastOnePropertyOf = { [K in keyof T]: { [L in K]-?: T[L] } & { [L in Exclude]?: T[L] }; }[keyof T]; +/** + * @source https://stackoverflow.com/a/56874389/4202031 + */ +type KeysMatching = { + [K in keyof T]-?: T[K] extends V ? K : never; +}[keyof T]; + type SDKSelection = { [key: string]: any }; type ResultType = { [key: string]: any }; @@ -37,87 +44,131 @@ type SDKOperationType : never; }; -type InternalSDKSelectionSet = Record; - export type SDKSelectionSet = any> = AtLeastOnePropertyOf>; -type SDKArgumentType = {}; // TODO IMPLEMENT +type SDKInputTypeMap = { [inputTypeName: string]: any }; -type SDKSelectionTypedDocumentNode = TypedDocumentNode< - SDKOperationType, - SDKArgumentType +type SDKArgumentType< + T_SDKInputTypeMap extends SDKInputTypeMap, + T_VariableDefinitions extends SDKVariableDefinitions +> = { [T_VariableName in keyof T_VariableDefinitions]: T_SDKInputTypeMap[T_VariableDefinitions[T_VariableName]] }; + +type SDKSelectionTypedDocumentNode< + T_Selection extends SDKSelection, + T_ResultType extends ResultType, + T_SDKInputTypeMap extends SDKInputTypeMap | void, + T_VariableDefinitions extends SDKVariableDefinitions< + T_SDKInputTypeMap extends void ? never : T_SDKInputTypeMap + > | void +> = TypedDocumentNode< + SDKOperationType, + T_SDKInputTypeMap extends SDKInputTypeMap + ? T_VariableDefinitions extends SDKVariableDefinitions + ? SDKArgumentType + : never + : never >; export const SDKFieldArgumentSymbol = Symbol('SDKFieldArguments'); -type SDKVariableDefinitions = { [key: string]: TInputTypes }; +type SDKVariableDefinitions = { [key: string]: keyof TSDKInputTypeMap }; + +type SDKSelectionWithVariables< + /** GraphQLTypeName -> TS type */ + T_SDKInputTypeMap extends SDKInputTypeMap, + T_Type extends SDKSelectionSet, + /** variableName -> GraphQLTypeName */ + T_VariableDefinitions extends SDKVariableDefinitions | void +> = { + [U_FieldName in keyof T_Type]: U_FieldName extends typeof SDKFieldArgumentSymbol + ? T_VariableDefinitions extends SDKVariableDefinitions + ? { + // From T_VariableDefinitions we want all keys whose value IS `T_Type[U_FieldName][V_ArgumentName]` + [V_ArgumentName in keyof T_Type[U_FieldName] /* ArgumentType */]: KeysMatching< + T_VariableDefinitions, + T_Type[U_FieldName][V_ArgumentName] + >; + } + : never + : T_Type[U_FieldName] extends SDKSelectionSet + ? SDKSelectionWithVariables + : T_Type[U_FieldName]; +}; type SDK< - SDKInputTypes extends string, - SDKQuerySelectionSet extends SDKSelectionSet, - QueryResultType extends ResultType, - SDKMutationSelectionSet extends SDKSelectionSet | void = void, - MutationResultType extends ResultType | void = void, - SDKSubscriptionSelectionSet extends SDKSelectionSet | void = void, - SubscriptionResultType extends ResultType | void = void + T_SDKInputTypeMap extends SDKInputTypeMap, + T_SDKQuerySelectionSet extends SDKSelectionSet, + T_QueryResultType extends ResultType, + T_SDKMutationSelectionSet extends SDKSelectionSet | void = void, + T_MutationResultType extends ResultType | void = void, + T_SDKSubscriptionSelectionSet extends SDKSelectionSet | void = void, + T_SubscriptionResultType extends ResultType | void = void > = { - query>( + query< + Q_VariableDefinitions extends SDKVariableDefinitions | void, + Q_Selection extends T_SDKQuerySelectionSet + >( args: ( | { name: string; - variables?: TVariableDefinitions; + variables?: Q_VariableDefinitions; } | { name?: never; variables?: never; } ) & { - selection: TSelection; + selection: SDKSelectionWithVariables; } - ): SDKSelectionTypedDocumentNode; -} & (SDKMutationSelectionSet extends SDKSelectionSet - ? MutationResultType extends ResultType + ): SDKSelectionTypedDocumentNode; +} & (T_SDKMutationSelectionSet extends SDKSelectionSet + ? T_MutationResultType extends ResultType ? { mutation< - TSelection extends SDKMutationSelectionSet, - TVariableDefinitions extends SDKVariableDefinitions + M_VariableDefinitions extends SDKVariableDefinitions, + M_Selection extends T_SDKMutationSelectionSet >( args: ( | { name: string; - variables?: TVariableDefinitions; + variables?: M_VariableDefinitions; } | { name?: never; variables?: never; } ) & { - selection: TSelection; + selection: SDKSelectionWithVariables; } - ): SDKSelectionTypedDocumentNode; + ): SDKSelectionTypedDocumentNode; } : {} : {}) & - (SDKSubscriptionSelectionSet extends SDKSelectionSet - ? SubscriptionResultType extends ResultType + (T_SDKSubscriptionSelectionSet extends SDKSelectionSet + ? T_SubscriptionResultType extends ResultType ? { subscription< - TSelection extends SDKSubscriptionSelectionSet, - TVariableDefinitions extends SDKVariableDefinitions + S_VariableDefinitions extends SDKVariableDefinitions, + S_Selection extends T_SDKSubscriptionSelectionSet >( args: ( | { name: string; - variables?: TVariableDefinitions; + variables?: S_VariableDefinitions; } | { name?: never; variables?: never; } ) & { - selection: TSelection; + selection: SDKSelectionWithVariables; } - ): SDKSelectionTypedDocumentNode; + ): SDKSelectionTypedDocumentNode< + S_Selection, + T_SubscriptionResultType, + T_SDKInputTypeMap, + S_VariableDefinitions + >; } : {} : {}); @@ -145,7 +196,7 @@ const getBaseDocument = ( ], }); -const buildSelectionSet = (sdkSelectionSet: InternalSDKSelectionSet): SelectionSetNode => { +const buildSelectionSet = (sdkSelectionSet: SDKSelectionSet): SelectionSetNode => { const selections: Array = []; for (const [fieldName, selectionValue] of Object.entries(sdkSelectionSet)) { @@ -235,21 +286,21 @@ const sdkHandler = }; export function createSDK< - SDKInputTypes extends string, - SDKQuerySelectionSet extends SDKSelectionSet, - QueryResultType extends ResultType, - SDKMutationSelectionSet extends SDKSelectionSet | void = void, - MutationResultType extends ResultType | void = void, - SDKSubscriptionSelectionSet extends SDKSelectionSet | void = void, - SubscriptionResultType extends ResultType | void = void + T_SDKInputTypeMap extends SDKInputTypeMap, + T_SDKQuerySelectionSet extends SDKSelectionSet, + T_QueryResultType extends ResultType, + T_SDKMutationSelectionSet extends SDKSelectionSet | void = void, + T_MutationResultType extends ResultType | void = void, + T_SDKSubscriptionSelectionSet extends SDKSelectionSet | void = void, + T_SubscriptionResultType extends ResultType | void = void >(): SDK< - SDKInputTypes, - SDKQuerySelectionSet, - QueryResultType, - SDKMutationSelectionSet, - MutationResultType, - SDKSubscriptionSelectionSet, - SubscriptionResultType + T_SDKInputTypeMap, + T_SDKQuerySelectionSet, + T_QueryResultType, + T_SDKMutationSelectionSet, + T_MutationResultType, + T_SDKSubscriptionSelectionSet, + T_SubscriptionResultType > { return { query: sdkHandler('query'), diff --git a/packages/plugins/typescript/typed-document-sdk/tests/typed-document-sdk.spec.ts b/packages/plugins/typescript/typed-document-sdk/tests/typed-document-sdk.spec.ts new file mode 100644 index 00000000000..d338feabec1 --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/tests/typed-document-sdk.spec.ts @@ -0,0 +1,9 @@ +// import { Types } from '@graphql-codegen/plugin-helpers'; +// import { buildSchema, parse } from 'graphql'; +// import { plugin } from '../src'; + +describe('TypedDocumentSDK', () => { + describe('buildObjectTypePartial', () => { + test('aaa', () => {}); + }); +}); From 501ece97df0e84b6dfaa6aba6070c70785ecfa3a Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Fri, 21 Jan 2022 16:24:51 +0100 Subject: [PATCH 03/17] nullable/list input types --- .../typed-document-sdk/src/sdk-base.spec.ts | 369 ++++++++++++++++++ .../typed-document-sdk/src/sdk-base.ts | 91 ++++- 2 files changed, 451 insertions(+), 9 deletions(-) diff --git a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts index a7f586cc77e..177b8f69561 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts @@ -356,4 +356,373 @@ describe('SDKLogic', () => { expect(document).toStrictEqual(expectedDocument); }); + + it('query with non nullable variables', () => { + type InputTypes = { + String: string; + Int: number; + Boolean: number; + }; + + type SelectionType = SDKSelectionSet<{ + __typename?: true; + user?: SDKSelectionSet<{ + id?: boolean; + login?: boolean; + }> & { + [SDKFieldArgumentSymbol]: { + id: 'String!'; + }; + }; + }>; + type ResultType = { + __typename?: 'Query'; + user?: { + id?: InputTypes['String']; + login?: InputTypes['String']; + }; + }; + + const sdk = createSDK(); + + const document = sdk.query({ + name: 'UserById', + variables: { + idVariableName: 'String!', + }, + selection: { + user: { + [SDKFieldArgumentSymbol]: { + id: 'idVariableName', + }, + id: true, + }, + }, + }); + + const expectedDocument: DocumentNode = { + kind: Kind.DOCUMENT, + definitions: [ + { + kind: Kind.OPERATION_DEFINITION, + name: { + kind: Kind.NAME, + value: 'UserById', + }, + operation: OperationTypeNode.QUERY, + variableDefinitions: [ + { + kind: Kind.VARIABLE_DEFINITION, + type: { + kind: Kind.NON_NULL_TYPE, + type: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'String', + }, + }, + }, + variable: { + kind: Kind.VARIABLE, + name: { + kind: Kind.NAME, + value: 'idVariableName', + }, + }, + }, + ], + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: 'user', + }, + arguments: [ + { + kind: Kind.ARGUMENT, + name: { + kind: Kind.NAME, + value: 'id', + }, + value: { + kind: Kind.VARIABLE, + name: { + kind: Kind.NAME, + value: 'idVariableName', + }, + }, + }, + ], + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: 'id', + }, + }, + ], + }, + }, + ], + }, + }, + ], + }; + + expect(document).toStrictEqual(expectedDocument); + }); + + it('query with list variables', () => { + type InputTypes = { + String: string; + Int: number; + Boolean: number; + }; + + type SelectionType = SDKSelectionSet<{ + __typename?: true; + user?: SDKSelectionSet<{ + id?: boolean; + login?: boolean; + }> & { + [SDKFieldArgumentSymbol]: { + id: '[String]'; + }; + }; + }>; + type ResultType = { + __typename?: 'Query'; + user?: { + id?: InputTypes['String']; + login?: InputTypes['String']; + }; + }; + + const sdk = createSDK(); + + const document = sdk.query({ + name: 'UserById', + variables: { + idVariableName: '[String]', + }, + selection: { + user: { + [SDKFieldArgumentSymbol]: { + id: 'idVariableName', + }, + id: true, + }, + }, + }); + + const expectedDocument: DocumentNode = { + kind: Kind.DOCUMENT, + definitions: [ + { + kind: Kind.OPERATION_DEFINITION, + name: { + kind: Kind.NAME, + value: 'UserById', + }, + operation: OperationTypeNode.QUERY, + variableDefinitions: [ + { + kind: Kind.VARIABLE_DEFINITION, + type: { + kind: Kind.LIST_TYPE, + type: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'String', + }, + }, + }, + variable: { + kind: Kind.VARIABLE, + name: { + kind: Kind.NAME, + value: 'idVariableName', + }, + }, + }, + ], + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: 'user', + }, + arguments: [ + { + kind: Kind.ARGUMENT, + name: { + kind: Kind.NAME, + value: 'id', + }, + value: { + kind: Kind.VARIABLE, + name: { + kind: Kind.NAME, + value: 'idVariableName', + }, + }, + }, + ], + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: 'id', + }, + }, + ], + }, + }, + ], + }, + }, + ], + }; + + expect(document).toStrictEqual(expectedDocument); + }); + + it('query with list variables', () => { + type InputTypes = { + String: string; + Int: number; + Boolean: number; + }; + + type SelectionType = SDKSelectionSet<{ + __typename?: true; + user?: SDKSelectionSet<{ + id?: boolean; + login?: boolean; + }> & { + [SDKFieldArgumentSymbol]: { + id: '[String!]'; + }; + }; + }>; + type ResultType = { + __typename?: 'Query'; + user?: { + id?: InputTypes['String']; + login?: InputTypes['String']; + }; + }; + + const sdk = createSDK(); + + const document = sdk.query({ + name: 'UserById', + variables: { + idVariableName: '[String!]', + }, + selection: { + user: { + [SDKFieldArgumentSymbol]: { + id: 'idVariableName', + }, + id: true, + }, + }, + }); + + const expectedDocument: DocumentNode = { + kind: Kind.DOCUMENT, + definitions: [ + { + kind: Kind.OPERATION_DEFINITION, + name: { + kind: Kind.NAME, + value: 'UserById', + }, + operation: OperationTypeNode.QUERY, + variableDefinitions: [ + { + kind: Kind.VARIABLE_DEFINITION, + type: { + kind: Kind.LIST_TYPE, + type: { + kind: Kind.NON_NULL_TYPE, + type: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'String', + }, + }, + }, + }, + variable: { + kind: Kind.VARIABLE, + name: { + kind: Kind.NAME, + value: 'idVariableName', + }, + }, + }, + ], + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: 'user', + }, + arguments: [ + { + kind: Kind.ARGUMENT, + name: { + kind: Kind.NAME, + value: 'id', + }, + value: { + kind: Kind.VARIABLE, + name: { + kind: Kind.NAME, + value: 'idVariableName', + }, + }, + }, + ], + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: 'id', + }, + }, + ], + }, + }, + ], + }, + }, + ], + }; + + expect(document).toStrictEqual(expectedDocument); + }); }); diff --git a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts index 00f55a713a6..021551f2c03 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts @@ -4,9 +4,13 @@ import type { DocumentNode, FieldNode, Kind, + ListTypeNode, + NamedTypeNode, + NonNullTypeNode, OperationTypeNode, SelectionNode, SelectionSetNode, + TypeNode, VariableDefinitionNode, } from 'graphql'; // IMPORTS END @@ -71,7 +75,24 @@ type SDKSelectionTypedDocumentNode< export const SDKFieldArgumentSymbol = Symbol('SDKFieldArguments'); -type SDKVariableDefinitions = { [key: string]: keyof TSDKInputTypeMap }; +type SDKInputNonNullType = `${T}!`; +type SDKInputListType = `[${T}]`; + +/** + * Poor mans implementation, this should actually be recursive as you could potentially indefinitely nest non nullable and list types... + * Right now we only allow Type, [Type], [Type]!, [Type!] and [Type!]! + */ +type SDKInputContainerType = + | T + | SDKInputNonNullType + | SDKInputListType + | SDKInputNonNullType> + | SDKInputListType> + | SDKInputNonNullType>>; + +type SDKVariableDefinitions = { + [key: string]: SDKInputContainerType>; +}; type SDKSelectionWithVariables< /** GraphQLTypeName -> TS type */ @@ -247,6 +268,65 @@ const buildSelectionSet = (sdkSelectionSet: SDKSelectionSet): SelectionSetNode = return selectionSet; }; +/** + * Poor mans GraphQL `parseType` (https://github.com/graphql/graphql-js/blob/a91fdc600f2012a60e44356c373e51c5dd20ba81/src/language/parser.ts#L157-L166) + * But in a more compact way :) + */ +const buildTypeNode = (name: string): TypeNode => { + let entry: Mutable; + let previous: Mutable; + + // eslint-disable-next-line no-constant-condition + while (true) { + if (name.endsWith('!')) { + name = name.substring(0, name.length - 1); + const current: NonNullTypeNode = { + kind: 'NonNullType' as Kind.NON_NULL_TYPE, + // Yeah... this is illegal - but we assign it in the next loop run + type: null, + }; + if (previous) { + previous.type = current; + previous = current; + } else if (!entry) { + entry = previous = current; + } + continue; + } + if (name.endsWith(']')) { + name = name.substring(1, name.length - 1); + const current: ListTypeNode = { + kind: 'ListType' as Kind.LIST_TYPE, + // Yeah... this is illegal - but we assign it in the next loop run + type: null, + }; + if (previous) { + previous.type = current; + previous = current; + } else if (!entry) { + entry = previous = current; + } + continue; + } + break; + } + + const last: NamedTypeNode = { + kind: 'NamedType' as Kind.NAMED_TYPE, + name: { + kind: 'Name' as Kind.NAME, + value: name, + }, + }; + + if (entry === undefined) { + return last; + } + + previous.type = last; + return entry; +}; + const buildVariableDefinitions = (args: Record): Array => { const variableDefinitions: Array = []; for (const [variableName, inputType] of Object.entries(args)) { @@ -259,14 +339,7 @@ const buildVariableDefinitions = (args: Record): Array Date: Fri, 21 Jan 2022 16:47:55 +0100 Subject: [PATCH 04/17] update snapshots --- .../src/buildObjectTypeSelectionString.spec.ts | 10 +++++----- .../src/buildSDKObjectString.spec.ts | 15 +++++++++------ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.spec.ts b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.spec.ts index 7fcdab92a0f..6362b5527a9 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.spec.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.spec.ts @@ -13,7 +13,7 @@ describe('buildObjectTypeSelectionString', () => { }); expect(buildObjectTypeSelectionString(graphQLObjectType)).toMatchInlineSnapshot(` - "type SDKHelloSelectionSet = SDKSelectionSet<{ + "type GeneratedSDKSelectionSetHello = SDKSelectionSet<{ __typename?: true; a?: true; }>;" @@ -30,9 +30,9 @@ describe('buildObjectTypeSelectionString', () => { }); expect(buildObjectTypeSelectionString(graphQLObjectType)).toMatchInlineSnapshot(` - "type SDKHelloSelectionSet = SDKSelectionSet<{ + "type GeneratedSDKSelectionSetHello = SDKSelectionSet<{ __typename?: true; - a?: SDKHelloSelectionSet; + a?: GeneratedSDKSelectionSetHello; }>;" `); }); @@ -52,7 +52,7 @@ describe('buildObjectTypeSelectionString', () => { }); expect(buildObjectTypeSelectionString(graphQLObjectType)).toMatchInlineSnapshot(` - "type SDKHelloSelectionSet = SDKSelectionSet<{ + "type GeneratedSDKSelectionSetHello = SDKSelectionSet<{ __typename?: true; a?: true | { [SDKFieldArgumentSymbol]?: { @@ -78,7 +78,7 @@ describe('buildObjectTypeSelectionString', () => { }); expect(buildObjectTypeSelectionString(graphQLObjectType)).toMatchInlineSnapshot(` - "type SDKHelloSelectionSet = SDKSelectionSet<{ + "type GeneratedSDKSelectionSetHello = SDKSelectionSet<{ __typename?: true; a?: { [SDKFieldArgumentSymbol]: { diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.spec.ts b/packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.spec.ts index ce58a10e4a9..6a36f8a1296 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.spec.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.spec.ts @@ -18,7 +18,8 @@ describe('buildSDKObjectString', () => { }); expect(buildSDKObjectString(objectType, null, null)).toMatchInlineSnapshot(` "export const sdk = createSDK< - SDKQueryRootSelectionSet, + GeneratedSDKInputTypes, + GeneratedSDKSelectionSetQueryRoot, QueryRoot, void, void, @@ -46,9 +47,10 @@ describe('buildSDKObjectString', () => { }); expect(buildSDKObjectString(queryType, mutationType, null)).toMatchInlineSnapshot(` "export const sdk = createSDK< - SDKQueryRootSelectionSet, + GeneratedSDKInputTypes, + GeneratedSDKSelectionSetQueryRoot, QueryRoot, - SDKMutationRootSelectionSet, + GeneratedSDKSelectionSetMutationRoot, MutationRoot, void, void, @@ -82,11 +84,12 @@ describe('buildSDKObjectString', () => { }); expect(buildSDKObjectString(queryType, mutationType, subscriptionType)).toMatchInlineSnapshot(` "export const sdk = createSDK< - SDKQueryRootSelectionSet, + GeneratedSDKInputTypes, + GeneratedSDKSelectionSetQueryRoot, QueryRoot, - SDKMutationRootSelectionSet, + GeneratedSDKSelectionSetMutationRoot, MutationRoot, - SDKSubscriptionSelectionSet, + GeneratedSDKSelectionSetSubscription, Subscription, >()" `); From a262d4a3971d244c08887a51e0acda064387d17f Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Fri, 21 Jan 2022 17:10:30 +0100 Subject: [PATCH 05/17] correct variable types --- .../typed-document-sdk/src/sdk-base.ts | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts index 021551f2c03..ef6e0fed76d 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts @@ -25,9 +25,12 @@ type Impossible = { type NoExtraProperties = U & Impossible>; /** Require at least one property in the object defined. */ -type AtLeastOnePropertyOf = { - [K in keyof T]: { [L in K]-?: T[L] } & { [L in Exclude]?: T[L] }; -}[keyof T]; +type AtLeastOnePropertyOf = Exclude< + { + [K in keyof T]: { [L in K]-?: T[L] } & { [L in Exclude]?: T[L] }; + }[keyof T], + void +>; /** * @source https://stackoverflow.com/a/56874389/4202031 @@ -55,7 +58,12 @@ type SDKInputTypeMap = { [inputTypeName: string]: any }; type SDKArgumentType< T_SDKInputTypeMap extends SDKInputTypeMap, T_VariableDefinitions extends SDKVariableDefinitions -> = { [T_VariableName in keyof T_VariableDefinitions]: T_SDKInputTypeMap[T_VariableDefinitions[T_VariableName]] }; +> = { + [T_VariableName in keyof T_VariableDefinitions]: SDKInputContainerUnwrap< + T_SDKInputTypeMap, + T_VariableDefinitions[T_VariableName] + >; +}; type SDKSelectionTypedDocumentNode< T_Selection extends SDKSelection, @@ -90,6 +98,20 @@ type SDKInputContainerType = | SDKInputListType> | SDKInputNonNullType>>; +/** + * Unwrap something like [Type!] to the actual runtime type. + */ +type SDKInputContainerUnwrap< + T_SDKInputTypeMap extends SDKInputTypeMap, + T_Typename extends keyof SDKVariableDefinitions +> = T_Typename extends keyof T_SDKInputTypeMap + ? T_SDKInputTypeMap[T_Typename] | null | undefined + : T_Typename extends SDKInputNonNullType + ? Exclude, null | undefined> + : T_Typename extends SDKInputListType + ? Array> + : never; + type SDKVariableDefinitions = { [key: string]: SDKInputContainerType>; }; @@ -97,23 +119,23 @@ type SDKVariableDefinitions = { type SDKSelectionWithVariables< /** GraphQLTypeName -> TS type */ T_SDKInputTypeMap extends SDKInputTypeMap, - T_Type extends SDKSelectionSet, + T_SDKSelectionSet extends SDKSelectionSet, /** variableName -> GraphQLTypeName */ T_VariableDefinitions extends SDKVariableDefinitions | void > = { - [U_FieldName in keyof T_Type]: U_FieldName extends typeof SDKFieldArgumentSymbol + [U_FieldName in keyof T_SDKSelectionSet]: U_FieldName extends typeof SDKFieldArgumentSymbol ? T_VariableDefinitions extends SDKVariableDefinitions ? { - // From T_VariableDefinitions we want all keys whose value IS `T_Type[U_FieldName][V_ArgumentName]` - [V_ArgumentName in keyof T_Type[U_FieldName] /* ArgumentType */]: KeysMatching< + // From T_VariableDefinitions we want all keys whose value IS `T_SDKSelectionSet[U_FieldName][V_ArgumentName]` + [V_ArgumentName in keyof T_SDKSelectionSet[U_FieldName] /* ArgumentType */]: KeysMatching< T_VariableDefinitions, - T_Type[U_FieldName][V_ArgumentName] + T_SDKSelectionSet[U_FieldName][V_ArgumentName] >; } : never - : T_Type[U_FieldName] extends SDKSelectionSet - ? SDKSelectionWithVariables - : T_Type[U_FieldName]; + : T_SDKSelectionSet[U_FieldName] extends SDKSelectionSet + ? SDKSelectionWithVariables + : T_SDKSelectionSet[U_FieldName]; }; type SDK< From df202c8c8d260f1e0d2fc9986790a457edace7fa Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Mon, 7 Feb 2022 09:25:55 +0100 Subject: [PATCH 06/17] more test cases --- .../typed-document-sdk/src/sdk-base.spec.ts | 40 +++++++++++++++++-- .../typed-document-sdk/src/sdk-base.ts | 16 ++++---- 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts index 177b8f69561..8da1120f564 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts @@ -514,7 +514,7 @@ describe('SDKLogic', () => { }, selection: { user: { - [SDKFieldArgumentSymbol]: { + [sdk.arguments]: { id: 'idVariableName', }, id: true, @@ -601,7 +601,7 @@ describe('SDKLogic', () => { expect(document).toStrictEqual(expectedDocument); }); - it('query with list variables', () => { + it('query with list variables (variance)', () => { type InputTypes = { String: string; Int: number; @@ -616,6 +616,7 @@ describe('SDKLogic', () => { }> & { [SDKFieldArgumentSymbol]: { id: '[String!]'; + number?: 'Int'; }; }; }>; @@ -633,11 +634,13 @@ describe('SDKLogic', () => { name: 'UserById', variables: { idVariableName: '[String!]', + a: 'Int', }, selection: { user: { - [SDKFieldArgumentSymbol]: { + [sdk.arguments]: { id: 'idVariableName', + number: 'a', }, id: true, }, @@ -678,6 +681,23 @@ describe('SDKLogic', () => { }, }, }, + { + kind: Kind.VARIABLE_DEFINITION, + type: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'Int', + }, + }, + variable: { + kind: Kind.VARIABLE, + name: { + kind: Kind.NAME, + value: 'a', + }, + }, + }, ], selectionSet: { kind: Kind.SELECTION_SET, @@ -703,6 +723,20 @@ describe('SDKLogic', () => { }, }, }, + { + kind: Kind.ARGUMENT, + name: { + kind: Kind.NAME, + value: 'number', + }, + value: { + kind: Kind.VARIABLE, + name: { + kind: Kind.NAME, + value: 'a', + }, + }, + }, ], selectionSet: { kind: Kind.SELECTION_SET, diff --git a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts index ef6e0fed76d..e104843c42b 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts @@ -90,13 +90,13 @@ type SDKInputListType = `[${T}]`; * Poor mans implementation, this should actually be recursive as you could potentially indefinitely nest non nullable and list types... * Right now we only allow Type, [Type], [Type]!, [Type!] and [Type!]! */ -type SDKInputContainerType = - | T - | SDKInputNonNullType - | SDKInputListType - | SDKInputNonNullType> - | SDKInputListType> - | SDKInputNonNullType>>; +type SDKInputContainerType = + | TTaxonomy + | SDKInputNonNullType + | SDKInputListType + | SDKInputNonNullType> + | SDKInputListType> + | SDKInputNonNullType>>; /** * Unwrap something like [Type!] to the actual runtime type. @@ -164,6 +164,7 @@ type SDK< selection: SDKSelectionWithVariables; } ): SDKSelectionTypedDocumentNode; + arguments: typeof SDKFieldArgumentSymbol; } & (T_SDKMutationSelectionSet extends SDKSelectionSet ? T_MutationResultType extends ResultType ? { @@ -401,5 +402,6 @@ export function createSDK< query: sdkHandler('query'), mutation: sdkHandler('mutation'), subscription: sdkHandler('subscription'), + arguments: SDKFieldArgumentSymbol, } as any; } From a8e4148fd9d9cf220786de026c8ea68f8c858434 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Tue, 8 Feb 2022 11:48:36 +0100 Subject: [PATCH 07/17] wip union support --- .../typed-document-sdk/src/sdk-base.ts | 60 ++++++++++++++++--- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts index e104843c42b..be9b0ad9922 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts @@ -43,16 +43,58 @@ type SDKSelection = { [key: string]: any }; type ResultType = { [key: string]: any }; -type SDKOperationType = { - [TKey in keyof TSelection]: TKey extends keyof TType - ? TType[TKey] extends Record - ? SDKOperationType - : TType[TKey] +export const SDKFieldArgumentSymbol = Symbol('SDKFieldArguments'); +export const SDKUnionResultSymbol = Symbol('UnionResultSymbol'); + +type SDKExtractUnionTargets< + TTargetField extends Record, + TUnionMember extends string | symbol | number +> = Exclude; + +type SDKOperationTypeInner = + // is record + ResultType extends Record + ? // union narrowing + typeof SDKUnionResultSymbol extends keyof TResultType + ? { + [TUnionMember in Exclude< + keyof TResultType, + typeof SDKUnionResultSymbol + >]: TUnionMember extends keyof SDKExtractUnionTargets // check whether all union members are covered + ? // all union members are covered + SDKOperationType< + SDKExtractUnionTargets[TUnionMember], + TResultType[TUnionMember] + > + : // not all union members are covered + {}; + // transform result into TypeScript union + }[Exclude] + : // object with selection set + SDKOperationType + : // primitive field value + TResultType; + +type SDKNullable = T | null; + +type SDKSelectionKeysWithoutArguments = Exclude< + keyof TSelection, + typeof SDKFieldArgumentSymbol +>; + +type SDKOperationType = { + // check whether field in in result type + [TSelectionField in SDKSelectionKeysWithoutArguments]: TSelectionField extends keyof TResultType + ? TResultType[TSelectionField] extends SDKNullable + ? SDKOperationTypeInner | null + : SDKOperationTypeInner : never; }; export type SDKSelectionSet = any> = AtLeastOnePropertyOf>; +export type SDKUnionSelectionSet = any> = AtLeastOnePropertyOf; + type SDKInputTypeMap = { [inputTypeName: string]: any }; type SDKArgumentType< @@ -66,7 +108,7 @@ type SDKArgumentType< }; type SDKSelectionTypedDocumentNode< - T_Selection extends SDKSelection, + T_Selection extends SDKSelectionSet, T_ResultType extends ResultType, T_SDKInputTypeMap extends SDKInputTypeMap | void, T_VariableDefinitions extends SDKVariableDefinitions< @@ -81,8 +123,6 @@ type SDKSelectionTypedDocumentNode< : never >; -export const SDKFieldArgumentSymbol = Symbol('SDKFieldArguments'); - type SDKInputNonNullType = `${T}!`; type SDKInputListType = `[${T}]`; @@ -149,7 +189,8 @@ type SDK< > = { query< Q_VariableDefinitions extends SDKVariableDefinitions | void, - Q_Selection extends T_SDKQuerySelectionSet + Q_Selection extends T_SDKQuerySelectionSet, + T >( args: ( | { @@ -164,6 +205,7 @@ type SDK< selection: SDKSelectionWithVariables; } ): SDKSelectionTypedDocumentNode; + arguments: typeof SDKFieldArgumentSymbol; } & (T_SDKMutationSelectionSet extends SDKSelectionSet ? T_MutationResultType extends ResultType From d6b7709cf5457d66c0e59e3681ce38fb0f92196c Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Tue, 8 Feb 2022 13:37:21 +0100 Subject: [PATCH 08/17] flip condition --- .../plugins/typescript/typed-document-sdk/src/sdk-base.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts index be9b0ad9922..2c8441281a6 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts @@ -75,7 +75,7 @@ type SDKOperationTypeInner = : // primitive field value TResultType; -type SDKNullable = T | null; +type SDKNonNullable = Exclude; type SDKSelectionKeysWithoutArguments = Exclude< keyof TSelection, @@ -85,8 +85,8 @@ type SDKSelectionKeysWithoutArguments = Exclude type SDKOperationType = { // check whether field in in result type [TSelectionField in SDKSelectionKeysWithoutArguments]: TSelectionField extends keyof TResultType - ? TResultType[TSelectionField] extends SDKNullable - ? SDKOperationTypeInner | null + ? null extends TResultType[TSelectionField] + ? SDKOperationTypeInner> | null : SDKOperationTypeInner : never; }; From 88c52918e91d23f8432ce0a061358ca831ebe821 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Wed, 9 Feb 2022 13:23:01 +0100 Subject: [PATCH 09/17] fix variable/argument usages --- .../typed-document-sdk/src/sdk-base.spec.ts | 202 +++++++++++++++--- .../typed-document-sdk/src/sdk-base.ts | 128 ++++++----- 2 files changed, 253 insertions(+), 77 deletions(-) diff --git a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts index 8da1120f564..dd637ca2a4f 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts @@ -1,9 +1,16 @@ import { DocumentNode, Kind, OperationTypeNode } from 'graphql'; -import { createSDK, SDKFieldArgumentSymbol, SDKSelectionSet } from './sdk-base'; +import { + createSDK, + SDKFieldArgumentSymbol, + SDKSelectionSet, + SDKUnionResultSymbol, + SDKUnionSelectionSet, +} from './sdk-base'; describe('SDKLogic', () => { it('anonymous query operation', () => { const sdk = createSDK< + {}, {}, SDKSelectionSet<{ __typename?: true; @@ -45,6 +52,7 @@ describe('SDKLogic', () => { it('named query operation', () => { const sdk = createSDK< + {}, {}, SDKSelectionSet<{ __typename?: true; @@ -92,11 +100,12 @@ describe('SDKLogic', () => { type SelectionType = SDKSelectionSet<{ __typename?: true; }>; + type ArgumentType = {}; type ResultType = { __typename?: 'Query'; }; - const sdk = createSDK<{}, SelectionType, ResultType, SelectionType, ResultType>(); + const sdk = createSDK<{}, SelectionType, ArgumentType, ResultType, SelectionType, ArgumentType, ResultType>(); const operation = sdk.mutation({ selection: { __typename: true, @@ -132,11 +141,23 @@ describe('SDKLogic', () => { type SelectionType = SDKSelectionSet<{ __typename?: true; }>; + type ArgumentType = {}; type ResultType = { __typename?: 'Query'; }; - const sdk = createSDK<{}, SelectionType, ResultType, SelectionType, ResultType, SelectionType, ResultType>(); + const sdk = createSDK< + {}, + SelectionType, + ArgumentType, + ResultType, + SelectionType, + ArgumentType, + ResultType, + SelectionType, + ArgumentType, + ResultType + >(); const operation = sdk.subscription({ selection: { __typename: true, @@ -170,6 +191,7 @@ describe('SDKLogic', () => { it('nested operation', () => { const sdk = createSDK< + {}, {}, SDKSelectionSet<{ __typename?: true; @@ -246,16 +268,26 @@ describe('SDKLogic', () => { }; type SelectionType = SDKSelectionSet<{ - __typename?: true; + __typename?: boolean; user?: SDKSelectionSet<{ id?: boolean; login?: boolean; }> & { - [SDKFieldArgumentSymbol]: { - id?: 'String'; + [SDKFieldArgumentSymbol]?: { + id?: string | never; }; }; + string?: true; }>; + + type ArgumentType = { + user: { + [SDKFieldArgumentSymbol]: { + id: 'String'; + }; + }; + }; + type ResultType = { __typename?: 'Query'; user?: { @@ -264,10 +296,10 @@ describe('SDKLogic', () => { }; }; - const sdk = createSDK(); + const sdk = createSDK(); const document = sdk.query({ - name: 'UserById', + name: 'AJSDGAJKSDHG', variables: { idVariableName: 'String', }, @@ -371,10 +403,19 @@ describe('SDKLogic', () => { login?: boolean; }> & { [SDKFieldArgumentSymbol]: { - id: 'String!'; + id: string | never; }; }; }>; + + type ArgumentType = { + user: { + [SDKFieldArgumentSymbol]: { + id: 'String!'; + }; + }; + }; + type ResultType = { __typename?: 'Query'; user?: { @@ -383,7 +424,7 @@ describe('SDKLogic', () => { }; }; - const sdk = createSDK(); + const sdk = createSDK(); const document = sdk.query({ name: 'UserById', @@ -493,10 +534,17 @@ describe('SDKLogic', () => { login?: boolean; }> & { [SDKFieldArgumentSymbol]: { - id: '[String]'; + id: string | never; }; }; }>; + type ArgumentType = { + user: { + [SDKFieldArgumentSymbol]: { + id: '[String]'; + }; + }; + }; type ResultType = { __typename?: 'Query'; user?: { @@ -505,7 +553,7 @@ describe('SDKLogic', () => { }; }; - const sdk = createSDK(); + const sdk = createSDK(); const document = sdk.query({ name: 'UserById', @@ -608,41 +656,58 @@ describe('SDKLogic', () => { Boolean: number; }; - type SelectionType = SDKSelectionSet<{ - __typename?: true; + // this holds the selection set + type QuerySelectionType = SDKSelectionSet<{ + __typename?: boolean; user?: SDKSelectionSet<{ + __typename?: boolean; id?: boolean; login?: boolean; }> & { [SDKFieldArgumentSymbol]: { - id: '[String!]'; - number?: 'Int'; + id: string | never; + number?: string | never; }; }; }>; - type ResultType = { - __typename?: 'Query'; - user?: { - id?: InputTypes['String']; - login?: InputTypes['String']; + + // this holds the actual argument types + type QueryArgumentType = { + user: { + [SDKFieldArgumentSymbol]: { + id: '[String!]!'; + number?: 'Int'; + }; }; }; - const sdk = createSDK(); + // this holds the result + type QueryResultType = { + __typename: 'Query'; + user: { + __typename: 'User'; + id: InputTypes['String']; + login: InputTypes['String']; + } | null; + }; + + const sdk = createSDK(); const document = sdk.query({ name: 'UserById', variables: { - idVariableName: '[String!]', + idVariableName: '[String!]!', a: 'Int', }, selection: { + __typename: true, user: { [sdk.arguments]: { id: 'idVariableName', - number: 'a', }, + __typename: true, id: true, + login: true, }, }, }); @@ -759,4 +824,93 @@ describe('SDKLogic', () => { expect(document).toStrictEqual(expectedDocument); }); + + it('union types', () => { + type InputTypes = { + String: string; + ID: string; + Boolean: number; + Int: number; + }; + + type SelectionType = SDKSelectionSet<{ + __typename?: true; + user?: SDKUnionSelectionSet<{ + User: SDKSelectionSet<{ + __typename?: true; + id?: boolean; + login?: boolean; + }>; + Error: SDKSelectionSet<{ + __typename?: true; + reason?: boolean; + }>; + }> & { + [SDKFieldArgumentSymbol]: { + id: string | never; + number?: string | never; + }; + }; + }>; + + type ArgumentType = { + user: { + [SDKUnionResultSymbol]: true; + [SDKFieldArgumentSymbol]: { + id: 'ID!'; + number?: 'Int'; + }; + }; + }; + + type ResultType = { + __typename?: 'Query'; + user: { + [SDKUnionResultSymbol]: true; + User: { + __typename: 'User'; + id?: InputTypes['String']; + login?: InputTypes['String']; + }; + Error: { + __typename: 'Error'; + reason: InputTypes['String']; + }; + }; + }; + + const sdk = createSDK(); + + const document = sdk.query({ + name: 'Foo', + variables: { + id: 'ID!', + someNumber: 'Int', + }, + selection: { + user: { + [sdk.arguments]: { + id: 'id', + number: 'someNumber', + }, + User: { + __typename: true, + id: true, + login: true, + }, + Error: { + __typename: true, + reason: true, + }, + }, + }, + }); + + const expectedDocument: DocumentNode = { + kind: Kind.DOCUMENT, + definitions: [], + }; + + expect(document).toStrictEqual(expectedDocument); + }); }); diff --git a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts index 2c8441281a6..ebb2135dc31 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts @@ -43,37 +43,30 @@ type SDKSelection = { [key: string]: any }; type ResultType = { [key: string]: any }; -export const SDKFieldArgumentSymbol = Symbol('SDKFieldArguments'); -export const SDKUnionResultSymbol = Symbol('UnionResultSymbol'); +export const SDKFieldArgumentSymbol: unique symbol = Symbol('SDKFieldArguments'); +export const SDKUnionResultSymbol: unique symbol = Symbol('UnionResultSymbol'); type SDKExtractUnionTargets< TTargetField extends Record, TUnionMember extends string | symbol | number > = Exclude; -type SDKOperationTypeInner = - // is record - ResultType extends Record - ? // union narrowing - typeof SDKUnionResultSymbol extends keyof TResultType - ? { - [TUnionMember in Exclude< - keyof TResultType, - typeof SDKUnionResultSymbol - >]: TUnionMember extends keyof SDKExtractUnionTargets // check whether all union members are covered - ? // all union members are covered - SDKOperationType< - SDKExtractUnionTargets[TUnionMember], - TResultType[TUnionMember] - > - : // not all union members are covered - {}; - // transform result into TypeScript union - }[Exclude] - : // object with selection set - SDKOperationType - : // primitive field value - TResultType; +type SDKOperationTypeInner> = + // union narrowing + typeof SDKUnionResultSymbol extends keyof TResultType + ? { + [TUnionMember in Exclude< + keyof TResultType, + typeof SDKUnionResultSymbol + >]: TUnionMember extends keyof SDKExtractUnionTargets // check whether all union members are covered + ? // all union members are covered + SDKOperationType[TUnionMember], TResultType[TUnionMember]> + : // not all union members are covered + {}; + // transform result into TypeScript union + }[Exclude] + : // object with selection set + SDKOperationType; type SDKNonNullable = Exclude; @@ -85,15 +78,17 @@ type SDKSelectionKeysWithoutArguments = Exclude type SDKOperationType = { // check whether field in in result type [TSelectionField in SDKSelectionKeysWithoutArguments]: TSelectionField extends keyof TResultType - ? null extends TResultType[TSelectionField] + ? TSelection[TSelectionField] extends boolean + ? TResultType[TSelectionField] + : null extends TResultType[TSelectionField] ? SDKOperationTypeInner> | null : SDKOperationTypeInner : never; }; -export type SDKSelectionSet = any> = AtLeastOnePropertyOf>; +export type SDKSelectionSet = AtLeastOnePropertyOf>; -export type SDKUnionSelectionSet = any> = AtLeastOnePropertyOf; +export type SDKUnionSelectionSet = any> = TType; // AtLeastOnePropertyOf; type SDKInputTypeMap = { [inputTypeName: string]: any }; @@ -108,7 +103,7 @@ type SDKArgumentType< }; type SDKSelectionTypedDocumentNode< - T_Selection extends SDKSelectionSet, + T_Selection, T_ResultType extends ResultType, T_SDKInputTypeMap extends SDKInputTypeMap | void, T_VariableDefinitions extends SDKVariableDefinitions< @@ -159,38 +154,49 @@ type SDKVariableDefinitions = { type SDKSelectionWithVariables< /** GraphQLTypeName -> TS type */ T_SDKInputTypeMap extends SDKInputTypeMap, - T_SDKSelectionSet extends SDKSelectionSet, + T_SDKSelectionSet, + T_ArgumentType, /** variableName -> GraphQLTypeName */ T_VariableDefinitions extends SDKVariableDefinitions | void > = { [U_FieldName in keyof T_SDKSelectionSet]: U_FieldName extends typeof SDKFieldArgumentSymbol ? T_VariableDefinitions extends SDKVariableDefinitions - ? { - // From T_VariableDefinitions we want all keys whose value IS `T_SDKSelectionSet[U_FieldName][V_ArgumentName]` - [V_ArgumentName in keyof T_SDKSelectionSet[U_FieldName] /* ArgumentType */]: KeysMatching< - T_VariableDefinitions, - T_SDKSelectionSet[U_FieldName][V_ArgumentName] - >; - } + ? T_ArgumentType extends { [SDKFieldArgumentSymbol]: infer U_Arguments } + ? { + // From T_VariableDefinitions we want all keys whose value matches `U_Arguments[V_ArgumentName]` + [V_ArgumentName in keyof T_SDKSelectionSet[U_FieldName] /* ArgumentType */]: KeysMatching< + T_VariableDefinitions, + // all legit argument values + V_ArgumentName extends keyof U_Arguments ? U_Arguments[V_ArgumentName] : never + >; + } + : never : never - : T_SDKSelectionSet[U_FieldName] extends SDKSelectionSet - ? SDKSelectionWithVariables + : T_SDKSelectionSet[U_FieldName] extends SDKSelectionSet + ? SDKSelectionWithVariables< + T_SDKInputTypeMap, + T_SDKSelectionSet[U_FieldName], + U_FieldName extends keyof T_ArgumentType ? T_ArgumentType[U_FieldName] : never, + T_VariableDefinitions + > : T_SDKSelectionSet[U_FieldName]; }; type SDK< T_SDKInputTypeMap extends SDKInputTypeMap, - T_SDKQuerySelectionSet extends SDKSelectionSet, + T_SDKQuerySelectionSet, + T_QueryArgumentType, T_QueryResultType extends ResultType, - T_SDKMutationSelectionSet extends SDKSelectionSet | void = void, + T_SDKMutationSelectionSet = void, + T_SDKMutationArgumentType = void, T_MutationResultType extends ResultType | void = void, - T_SDKSubscriptionSelectionSet extends SDKSelectionSet | void = void, + T_SDKSubscriptionSelectionSet = void, + T_SDKSubscriptionArgumentType = void, T_SubscriptionResultType extends ResultType | void = void > = { query< Q_VariableDefinitions extends SDKVariableDefinitions | void, - Q_Selection extends T_SDKQuerySelectionSet, - T + Q_Selection extends T_SDKQuerySelectionSet >( args: ( | { @@ -202,12 +208,12 @@ type SDK< variables?: never; } ) & { - selection: SDKSelectionWithVariables; + selection: SDKSelectionWithVariables; } ): SDKSelectionTypedDocumentNode; arguments: typeof SDKFieldArgumentSymbol; -} & (T_SDKMutationSelectionSet extends SDKSelectionSet +} & (T_SDKMutationSelectionSet extends SDKSelectionSet ? T_MutationResultType extends ResultType ? { mutation< @@ -224,13 +230,18 @@ type SDK< variables?: never; } ) & { - selection: SDKSelectionWithVariables; + selection: SDKSelectionWithVariables< + T_SDKInputTypeMap, + M_Selection, + T_SDKMutationArgumentType, + M_VariableDefinitions + >; } ): SDKSelectionTypedDocumentNode; } : {} : {}) & - (T_SDKSubscriptionSelectionSet extends SDKSelectionSet + (T_SDKSubscriptionSelectionSet extends SDKSelectionSet ? T_SubscriptionResultType extends ResultType ? { subscription< @@ -247,7 +258,12 @@ type SDK< variables?: never; } ) & { - selection: SDKSelectionWithVariables; + selection: SDKSelectionWithVariables< + T_SDKInputTypeMap, + S_Selection, + T_SDKSubscriptionArgumentType, + S_VariableDefinitions + >; } ): SDKSelectionTypedDocumentNode< S_Selection, @@ -282,7 +298,7 @@ const getBaseDocument = ( ], }); -const buildSelectionSet = (sdkSelectionSet: SDKSelectionSet): SelectionSetNode => { +const buildSelectionSet = (sdkSelectionSet: SDKSelectionSet>): SelectionSetNode => { const selections: Array = []; for (const [fieldName, selectionValue] of Object.entries(sdkSelectionSet)) { @@ -425,19 +441,25 @@ const sdkHandler = export function createSDK< T_SDKInputTypeMap extends SDKInputTypeMap, - T_SDKQuerySelectionSet extends SDKSelectionSet, + T_SDKQuerySelectionSet, + T_SDKQueryArguments, T_QueryResultType extends ResultType, - T_SDKMutationSelectionSet extends SDKSelectionSet | void = void, + T_SDKMutationSelectionSet = void, + T_SDKMutationArguments = void, T_MutationResultType extends ResultType | void = void, - T_SDKSubscriptionSelectionSet extends SDKSelectionSet | void = void, + T_SDKSubscriptionSelectionSet = void, + T_SDKSubscriptionArguments = void, T_SubscriptionResultType extends ResultType | void = void >(): SDK< T_SDKInputTypeMap, T_SDKQuerySelectionSet, + T_SDKQueryArguments, T_QueryResultType, T_SDKMutationSelectionSet, + T_SDKMutationArguments, T_MutationResultType, T_SDKSubscriptionSelectionSet, + T_SDKSubscriptionArguments, T_SubscriptionResultType > { return { From e7d467710fd7e9cd773ecf9b1226d7333ea6a615 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Wed, 9 Feb 2022 14:22:15 +0100 Subject: [PATCH 10/17] feat: build union selection sets --- .../typed-document-sdk/src/sdk-base.spec.ts | 267 +++++++++++++++++- .../typed-document-sdk/src/sdk-base.ts | 59 ++-- 2 files changed, 292 insertions(+), 34 deletions(-) diff --git a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts index dd637ca2a4f..8d5c1a53833 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts @@ -1,4 +1,4 @@ -import { DocumentNode, Kind, OperationTypeNode } from 'graphql'; +import { DocumentNode, Kind, OperationTypeNode, print } from 'graphql'; import { createSDK, SDKFieldArgumentSymbol, @@ -25,6 +25,12 @@ describe('SDKLogic', () => { }, }); + expect(print(operation)).toMatchInlineSnapshot(` + "{ + __typename + }" + `); + expect(operation).toStrictEqual({ kind: Kind.DOCUMENT, definitions: [ @@ -68,6 +74,12 @@ describe('SDKLogic', () => { }, }); + expect(print(operation)).toMatchInlineSnapshot(` + "query Brrt { + __typename + }" + `); + expect(operation).toStrictEqual({ kind: Kind.DOCUMENT, definitions: [ @@ -112,6 +124,12 @@ describe('SDKLogic', () => { }, }); + expect(print(operation)).toMatchInlineSnapshot(` + "mutation { + __typename + }" + `); + expect(operation).toStrictEqual({ kind: Kind.DOCUMENT, definitions: [ @@ -164,6 +182,12 @@ describe('SDKLogic', () => { }, }); + expect(print(operation)).toMatchInlineSnapshot(` + "subscription { + __typename + }" + `); + expect(operation).toStrictEqual({ kind: Kind.DOCUMENT, definitions: [ @@ -216,6 +240,15 @@ describe('SDKLogic', () => { }, }); + expect(print(operation)).toMatchInlineSnapshot(` + "{ + __typename + foo { + a + } + }" + `); + expect(operation).toStrictEqual({ kind: Kind.DOCUMENT, definitions: [ @@ -299,7 +332,7 @@ describe('SDKLogic', () => { const sdk = createSDK(); const document = sdk.query({ - name: 'AJSDGAJKSDHG', + name: 'UserById', variables: { idVariableName: 'String', }, @@ -313,6 +346,14 @@ describe('SDKLogic', () => { }, }); + expect(print(document)).toMatchInlineSnapshot(` + "query UserById($idVariableName: String) { + user(id: $idVariableName) { + id + } + }" + `); + const expectedDocument: DocumentNode = { kind: Kind.DOCUMENT, definitions: [ @@ -441,6 +482,14 @@ describe('SDKLogic', () => { }, }); + expect(print(document)).toMatchInlineSnapshot(` + "query UserById($idVariableName: String!) { + user(id: $idVariableName) { + id + } + }" + `); + const expectedDocument: DocumentNode = { kind: Kind.DOCUMENT, definitions: [ @@ -675,7 +724,7 @@ describe('SDKLogic', () => { type QueryArgumentType = { user: { [SDKFieldArgumentSymbol]: { - id: '[String!]!'; + id: '[String!]'; number?: 'Int'; }; }; @@ -696,22 +745,28 @@ describe('SDKLogic', () => { const document = sdk.query({ name: 'UserById', variables: { - idVariableName: '[String!]!', + idVariableName: '[String!]', a: 'Int', }, selection: { - __typename: true, user: { [sdk.arguments]: { id: 'idVariableName', + number: 'a', }, - __typename: true, id: true, - login: true, }, }, }); + expect(print(document)).toMatchInlineSnapshot(` + "query UserById($idVariableName: [String!], $a: Int) { + user(id: $idVariableName, number: $a) { + id + } + }" + `); + const expectedDocument: DocumentNode = { kind: Kind.DOCUMENT, definitions: [ @@ -836,12 +891,12 @@ describe('SDKLogic', () => { type SelectionType = SDKSelectionSet<{ __typename?: true; user?: SDKUnionSelectionSet<{ - User: SDKSelectionSet<{ + '...User': SDKSelectionSet<{ __typename?: true; id?: boolean; login?: boolean; }>; - Error: SDKSelectionSet<{ + '...Error': SDKSelectionSet<{ __typename?: true; reason?: boolean; }>; @@ -893,12 +948,11 @@ describe('SDKLogic', () => { id: 'id', number: 'someNumber', }, - User: { + '...User': { __typename: true, id: true, - login: true, }, - Error: { + '...Error': { __typename: true, reason: true, }, @@ -906,11 +960,196 @@ describe('SDKLogic', () => { }, }); + expect(print(document)).toMatchInlineSnapshot(` + "query Foo($id: ID!, $someNumber: Int) { + user(id: $id, number: $someNumber) { + ... on User { + __typename + id + } + ... on Error { + __typename + reason + } + } + }" + `); + const expectedDocument: DocumentNode = { kind: Kind.DOCUMENT, - definitions: [], + definitions: [ + { + kind: Kind.OPERATION_DEFINITION, + operation: OperationTypeNode.QUERY, + name: { + kind: Kind.NAME, + value: 'Foo', + }, + variableDefinitions: [ + { + kind: Kind.VARIABLE_DEFINITION, + type: { + kind: Kind.NON_NULL_TYPE, + type: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'ID', + }, + }, + }, + variable: { + kind: Kind.VARIABLE, + name: { + kind: Kind.NAME, + value: 'id', + }, + }, + }, + { + kind: Kind.VARIABLE_DEFINITION, + type: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'Int', + }, + }, + variable: { + kind: Kind.VARIABLE, + name: { + kind: Kind.NAME, + value: 'someNumber', + }, + }, + }, + ], + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: 'user', + }, + arguments: [ + { + kind: Kind.ARGUMENT, + name: { + kind: Kind.NAME, + value: 'id', + }, + value: { + kind: Kind.VARIABLE, + name: { + kind: Kind.NAME, + value: 'id', + }, + }, + }, + { + kind: Kind.ARGUMENT, + name: { + kind: Kind.NAME, + value: 'number', + }, + value: { + kind: Kind.VARIABLE, + name: { + kind: Kind.NAME, + value: 'someNumber', + }, + }, + }, + ], + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + kind: Kind.INLINE_FRAGMENT, + typeCondition: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'User', + }, + }, + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: '__typename', + }, + }, + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: 'id', + }, + }, + ], + }, + }, + { + kind: Kind.INLINE_FRAGMENT, + typeCondition: { + kind: Kind.NAMED_TYPE, + name: { + kind: Kind.NAME, + value: 'Error', + }, + }, + selectionSet: { + kind: Kind.SELECTION_SET, + selections: [ + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: '__typename', + }, + }, + { + kind: Kind.FIELD, + name: { + kind: Kind.NAME, + value: 'reason', + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + ], }; - expect(document).toStrictEqual(expectedDocument); + + type OperationType = ReturnType>; + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + function api_test(value: OperationType) { + if (value.user.__typename === 'User') { + value.user.id; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + value.user.login; + } + + if (value.user.__typename === 'Error') { + value.user.__typename; + value.user.reason; + } + } }); }); diff --git a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts index ebb2135dc31..041a5c70969 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts @@ -3,6 +3,7 @@ import type { ArgumentNode, DocumentNode, FieldNode, + InlineFragmentNode, Kind, ListTypeNode, NamedTypeNode, @@ -43,26 +44,30 @@ type SDKSelection = { [key: string]: any }; type ResultType = { [key: string]: any }; +type SDKInlineFragmentKey = `...${T}`; + export const SDKFieldArgumentSymbol: unique symbol = Symbol('SDKFieldArguments'); export const SDKUnionResultSymbol: unique symbol = Symbol('UnionResultSymbol'); -type SDKExtractUnionTargets< - TTargetField extends Record, - TUnionMember extends string | symbol | number -> = Exclude; +type SDKExtractUnionTargets, TUnionMember extends string> = Exclude< + TSelection, + null | undefined | never | { [key in `...${TUnionMember}`]?: never | undefined | void } +>; type SDKOperationTypeInner> = // union narrowing typeof SDKUnionResultSymbol extends keyof TResultType ? { - [TUnionMember in Exclude< - keyof TResultType, - typeof SDKUnionResultSymbol - >]: TUnionMember extends keyof SDKExtractUnionTargets // check whether all union members are covered - ? // all union members are covered - SDKOperationType[TUnionMember], TResultType[TUnionMember]> - : // not all union members are covered - {}; + [TUnionMember in keyof TResultType]: TUnionMember extends string + ? SDKInlineFragmentKey extends keyof SDKExtractUnionTargets // check whether all union members are covered + ? // all union members are covered + SDKOperationType< + SDKExtractUnionTargets[SDKInlineFragmentKey], + TResultType[TUnionMember] + > + : // not all union members are covered + {} + : never; // transform result into TypeScript union }[Exclude] : // object with selection set @@ -302,13 +307,27 @@ const buildSelectionSet = (sdkSelectionSet: SDKSelectionSet> const selections: Array = []; for (const [fieldName, selectionValue] of Object.entries(sdkSelectionSet)) { - const fieldNode: Mutable = { - kind: 'Field' as Kind.FIELD, - name: { - kind: 'Name' as Kind.NAME, - value: fieldName, - }, - }; + const fieldNode: Mutable | Mutable = fieldName.startsWith('...') + ? { + kind: 'InlineFragment' as Kind.INLINE_FRAGMENT, + typeCondition: { + kind: 'NamedType' as Kind.NAMED_TYPE, + name: { + kind: 'Name' as Kind.NAME, + value: fieldName.replace('...', ''), + }, + }, + // we lazily add this no need for adding a noop one here ok? + selectionSet: null as any, + } + : { + kind: 'Field' as Kind.FIELD, + name: { + kind: 'Name' as Kind.NAME, + value: fieldName, + }, + }; + if (typeof selectionValue === 'object') { fieldNode.selectionSet = buildSelectionSet(selectionValue); @@ -334,7 +353,7 @@ const buildSelectionSet = (sdkSelectionSet: SDKSelectionSet> }); } if (args.length) { - fieldNode.arguments = args; + (fieldNode as Mutable).arguments = args; } } } From ef7a01fab7c3d595946aa381b71de01bebe95be7 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Wed, 9 Feb 2022 14:45:59 +0100 Subject: [PATCH 11/17] fix selection set --- packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts index 041a5c70969..c896cb9b3d6 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts @@ -93,7 +93,7 @@ type SDKOperationType = AtLeastOnePropertyOf>; -export type SDKUnionSelectionSet = any> = TType; // AtLeastOnePropertyOf; +export type SDKUnionSelectionSet = any> = SDKSelectionSet; type SDKInputTypeMap = { [inputTypeName: string]: any }; From 639515c10c472147ce09efb085440d4a2894d69c Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Wed, 9 Feb 2022 22:11:03 +0100 Subject: [PATCH 12/17] generation code for unions, interfaces and arguments --- .../src/buildInterfaceSelectionString.spec.ts | 65 +++++++++++++++++++ .../src/buildInterfaceSelectionString.ts | 15 +++++ .../src/buildObjectTypeArgumentString.spec.ts | 62 ++++++++++++++++++ .../src/buildObjectTypeArgumentString.ts | 47 ++++++++++++++ .../buildObjectTypeSelectionString.spec.ts | 4 +- .../src/buildObjectTypeSelectionString.ts | 14 ++-- .../src/buildSDKObjectString.spec.ts | 9 +++ .../src/buildSDKObjectString.ts | 14 ++-- .../src/buildUnionSelectionString.spec.ts | 31 +++++++++ .../src/buildUnionSelectionString.ts | 13 ++++ .../typed-document-sdk/src/index.ts | 24 ++++++- 11 files changed, 280 insertions(+), 18 deletions(-) create mode 100644 packages/plugins/typescript/typed-document-sdk/src/buildInterfaceSelectionString.spec.ts create mode 100644 packages/plugins/typescript/typed-document-sdk/src/buildInterfaceSelectionString.ts create mode 100644 packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeArgumentString.spec.ts create mode 100644 packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeArgumentString.ts create mode 100644 packages/plugins/typescript/typed-document-sdk/src/buildUnionSelectionString.spec.ts create mode 100644 packages/plugins/typescript/typed-document-sdk/src/buildUnionSelectionString.ts diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildInterfaceSelectionString.spec.ts b/packages/plugins/typescript/typed-document-sdk/src/buildInterfaceSelectionString.spec.ts new file mode 100644 index 00000000000..c35da7ff291 --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/src/buildInterfaceSelectionString.spec.ts @@ -0,0 +1,65 @@ +import { GraphQLBoolean, GraphQLInterfaceType, GraphQLObjectType, GraphQLSchema } from 'graphql'; +import { buildInterfaceSelectionString } from './buildInterfaceSelectionString'; + +describe('buildInterfaceSelectionString', () => { + it('correct selection set for single interface member', () => { + const interfaceType = new GraphQLInterfaceType({ + name: 'Foo', + fields: { + a: { + type: GraphQLBoolean, + }, + }, + }); + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + a: { + type: interfaceType, + }, + }, + }), + types: [ + new GraphQLObjectType({ name: 'Hee', fields: { a: { type: GraphQLBoolean } }, interfaces: [interfaceType] }), + ], + }); + + expect(buildInterfaceSelectionString(schema, interfaceType)).toMatchInlineSnapshot(` + "type GeneratedSDKSelectionSetFoo = SDKSelectionSet<{ + \\"...Hee\\": GeneratedSDKSelectionSetHee; + }>;" + `); + }); + it('correct selection set for multi interface member', () => { + const interfaceType = new GraphQLInterfaceType({ + name: 'Foo', + fields: { + a: { + type: GraphQLBoolean, + }, + }, + }); + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + a: { + type: interfaceType, + }, + }, + }), + types: [ + new GraphQLObjectType({ name: 'Hee', fields: { a: { type: GraphQLBoolean } }, interfaces: [interfaceType] }), + new GraphQLObjectType({ name: 'Hoo', fields: { a: { type: GraphQLBoolean } }, interfaces: [interfaceType] }), + ], + }); + + expect(buildInterfaceSelectionString(schema, interfaceType)).toMatchInlineSnapshot(` + "type GeneratedSDKSelectionSetFoo = SDKSelectionSet<{ + \\"...Hee\\": GeneratedSDKSelectionSetHee; + \\"...Hoo\\": GeneratedSDKSelectionSetHoo; + }>;" + `); + }); +}); diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildInterfaceSelectionString.ts b/packages/plugins/typescript/typed-document-sdk/src/buildInterfaceSelectionString.ts new file mode 100644 index 00000000000..c570ca34adb --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/src/buildInterfaceSelectionString.ts @@ -0,0 +1,15 @@ +import { stripIndent } from 'common-tags'; +import type { GraphQLInterfaceType, GraphQLSchema } from 'graphql'; +import { buildSelectionSetName } from './buildObjectTypeSelectionString'; + +export const buildInterfaceSelectionString = (schema: GraphQLSchema, ttype: GraphQLInterfaceType) => { + const implementedTypes = schema + .getImplementations(ttype) + .objects.map(ttype => `"...${ttype.name}": ${buildSelectionSetName(ttype.name)};`); + + return stripIndent` + type ${buildSelectionSetName(ttype.name)} = SDKSelectionSet<{ + ${implementedTypes.join(`\n `)} + }>; + `; +}; diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeArgumentString.spec.ts b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeArgumentString.spec.ts new file mode 100644 index 00000000000..5e7c0d73ae2 --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeArgumentString.spec.ts @@ -0,0 +1,62 @@ +import { GraphQLInt, GraphQLObjectType } from 'graphql'; +import { buildObjectTypeArgumentString } from './buildObjectTypeArgumentString'; + +describe('buildObjectTypeArgumentString', () => { + it('primitive field', () => { + const graphQLObjectType = new GraphQLObjectType({ + name: 'Hello', + fields: { + a: { + type: GraphQLInt, + }, + }, + }); + + expect(buildObjectTypeArgumentString(graphQLObjectType)).toMatchInlineSnapshot(` + "type GeneratedSDKArgumentsHello = { + a: {}; + };" + `); + }); + it('object field', () => { + const graphQLObjectType = new GraphQLObjectType({ + name: 'Hello', + fields: () => ({ + a: { + type: graphQLObjectType, + }, + }), + }); + + expect(buildObjectTypeArgumentString(graphQLObjectType)).toMatchInlineSnapshot(` + "type GeneratedSDKArgumentsHello = { + a: GeneratedSDKArgumentsHello; + };" + `); + }); + it('primitive field with args.', () => { + const graphQLObjectType = new GraphQLObjectType({ + name: 'Hello', + fields: () => ({ + a: { + type: GraphQLInt, + args: { + arg: { + type: GraphQLInt, + }, + }, + }, + }), + }); + + expect(buildObjectTypeArgumentString(graphQLObjectType)).toMatchInlineSnapshot(` + "type GeneratedSDKArgumentsHello = { + a: {} & { + [SDKFieldArgumentSymbol]: { + arg: \\"Int\\"; + } + }; + };" + `); + }); +}); diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeArgumentString.ts b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeArgumentString.ts new file mode 100644 index 00000000000..c9d7977413d --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeArgumentString.ts @@ -0,0 +1,47 @@ +import { stripIndent } from 'common-tags'; +import { GraphQLObjectType, GraphQLField, getNamedType, isScalarType, isEnumType } from 'graphql'; + +export const buildObjectArgumentsName = (name: string) => `GeneratedSDKArguments${name}`; + +const buildFieldArgumentsString = (field: GraphQLField): string => { + const resultType = getNamedType(field.type); + + let fieldPartial = + isScalarType(resultType) || isEnumType(resultType) ? '{}' : buildObjectArgumentsName(resultType.name); + + if (field.args.length) { + const argumentPartials: Array = []; + + for (const arg of field.args) { + argumentPartials.push(`${arg.name}: "${arg.type.toString()}"`); + } + + fieldPartial = + fieldPartial + + ' & ' + + stripIndent` + { + [SDKFieldArgumentSymbol]: { + ${argumentPartials.join(`;\n `)}; + } + } + ` + .split(`\n`) + .map((line, i) => (i === 0 ? line : ` ${line}`)) + .join(`\n`); + } + + return `${field.name}: ${fieldPartial};`; +}; + +export const buildObjectTypeArgumentString = (objectType: GraphQLObjectType) => { + const fields: Array = []; + for (const field of Object.values(objectType.getFields())) { + fields.push(buildFieldArgumentsString(field)); + } + return stripIndent` + type ${buildObjectArgumentsName(objectType.name)} = { + ${fields.join(`;\n `)} + }; + `; +}; diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.spec.ts b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.spec.ts index 6362b5527a9..a7784590c12 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.spec.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.spec.ts @@ -56,7 +56,7 @@ describe('buildObjectTypeSelectionString', () => { __typename?: true; a?: true | { [SDKFieldArgumentSymbol]?: { - arg?: true; + arg?: string | never; } }; }>;" @@ -82,7 +82,7 @@ describe('buildObjectTypeSelectionString', () => { __typename?: true; a?: { [SDKFieldArgumentSymbol]: { - arg: true; + arg: string | never; } }; }>;" diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.ts b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.ts index 93fb5b82595..303cb4c9d8a 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.ts @@ -9,19 +9,15 @@ import { isUnionType, } from 'graphql'; -export const buildObjectSelectionSetName = (name: string) => `GeneratedSDKSelectionSet${name}`; +export const buildSelectionSetName = (name: string) => `GeneratedSDKSelectionSet${name}`; const buildFieldSelectionSetString = (field: GraphQLField): string => { const resultType = getNamedType(field.type); let value = `true`; - if (isInterfaceType(resultType) || isUnionType(resultType)) { - throw new Error('Not yet supported. Interfaces and Union.'); - } - - if (isObjectType(resultType)) { - value = buildObjectSelectionSetName(resultType.name); + if (isObjectType(resultType) || isInterfaceType(resultType) || isUnionType(resultType)) { + value = buildSelectionSetName(resultType.name); } if (field.args.length) { @@ -31,7 +27,7 @@ const buildFieldSelectionSetString = (field: GraphQLField): string => for (const arg of field.args) { const isNonNull = isNonNullType(arg.type); requireArguments = isNonNull === true || requireArguments; - argumentPartials.push(`${arg.name}${isNonNull ? `` : `?`}: true`); + argumentPartials.push(`${arg.name}${isNonNull ? `` : `?`}: string | never`); } const args = stripIndent` @@ -66,7 +62,7 @@ export const buildObjectTypeSelectionString = (objectType: GraphQLObjectType): s } return stripIndent` - type ${buildObjectSelectionSetName(objectType.name)} = SDKSelectionSet<{ + type ${buildSelectionSetName(objectType.name)} = SDKSelectionSet<{ __typename?: true; ${fields.join(`;\n `)} }>; diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.spec.ts b/packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.spec.ts index 6a36f8a1296..41d17321dab 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.spec.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.spec.ts @@ -20,11 +20,14 @@ describe('buildSDKObjectString', () => { "export const sdk = createSDK< GeneratedSDKInputTypes, GeneratedSDKSelectionSetQueryRoot, + GeneratedSDKArgumentsQueryRoot, QueryRoot, void, void, void, void, + void, + void, >()" `); }); @@ -49,11 +52,14 @@ describe('buildSDKObjectString', () => { "export const sdk = createSDK< GeneratedSDKInputTypes, GeneratedSDKSelectionSetQueryRoot, + GeneratedSDKArgumentsQueryRoot, QueryRoot, GeneratedSDKSelectionSetMutationRoot, + GeneratedSDKArgumentsMutationRoot, MutationRoot, void, void, + void, >()" `); }); @@ -86,10 +92,13 @@ describe('buildSDKObjectString', () => { "export const sdk = createSDK< GeneratedSDKInputTypes, GeneratedSDKSelectionSetQueryRoot, + GeneratedSDKArgumentsQueryRoot, QueryRoot, GeneratedSDKSelectionSetMutationRoot, + GeneratedSDKArgumentsMutationRoot, MutationRoot, GeneratedSDKSelectionSetSubscription, + GeneratedSDKArgumentsSubscription, Subscription, >()" `); diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.ts b/packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.ts index 7df8e2a60b1..ed7ffbffb95 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/buildSDKObjectString.ts @@ -1,6 +1,7 @@ import { stripIndent } from 'common-tags'; -import { GraphQLObjectType } from 'graphql'; -import { buildObjectSelectionSetName } from './buildObjectTypeSelectionString'; +import type { GraphQLObjectType } from 'graphql'; +import { buildSelectionSetName } from './buildObjectTypeSelectionString'; +import { buildObjectArgumentsName } from './buildObjectTypeArgumentString'; type Maybe = T | null | undefined; @@ -16,11 +17,14 @@ export const buildSDKObjectString = ( return stripIndent` export const sdk = createSDK< GeneratedSDKInputTypes, - ${buildObjectSelectionSetName(queryType.name)}, + ${buildSelectionSetName(queryType.name)}, + ${buildObjectArgumentsName(queryType.name)}, ${queryType.name}, - ${mutationType ? buildObjectSelectionSetName(mutationType.name) : 'void'}, + ${mutationType ? buildSelectionSetName(mutationType.name) : 'void'}, + ${mutationType ? buildObjectArgumentsName(mutationType.name) : 'void'}, ${mutationType ? mutationType : 'void'}, - ${subscriptionType ? buildObjectSelectionSetName(subscriptionType.name) : 'void'}, + ${subscriptionType ? buildSelectionSetName(subscriptionType.name) : 'void'}, + ${subscriptionType ? buildObjectArgumentsName(subscriptionType.name) : 'void'}, ${subscriptionType ? subscriptionType : 'void'}, >() `; diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildUnionSelectionString.spec.ts b/packages/plugins/typescript/typed-document-sdk/src/buildUnionSelectionString.spec.ts new file mode 100644 index 00000000000..efac0fb8ddc --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/src/buildUnionSelectionString.spec.ts @@ -0,0 +1,31 @@ +import { GraphQLBoolean, GraphQLObjectType, GraphQLUnionType } from 'graphql'; +import { buildUnionSelectionString } from './buildUnionSelectionString'; + +describe('buildUnionSelectionString', () => { + it('correct selection set for single union member', () => { + const union = new GraphQLUnionType({ + name: 'Foo', + types: [new GraphQLObjectType({ name: 'Hee', fields: { a: { type: GraphQLBoolean } } })], + }); + expect(buildUnionSelectionString(union)).toMatchInlineSnapshot(` + "type GeneratedSDKSelectionSetFoo = SDKSelectionSet<{ + \\"...Hee\\": GeneratedSDKSelectionSetHee; + }>;" + `); + }); + it('correct selection set for multi union member', () => { + const union = new GraphQLUnionType({ + name: 'Foo', + types: [ + new GraphQLObjectType({ name: 'Hee', fields: { a: { type: GraphQLBoolean } } }), + new GraphQLObjectType({ name: 'Hoo', fields: { a: { type: GraphQLBoolean } } }), + ], + }); + expect(buildUnionSelectionString(union)).toMatchInlineSnapshot(` + "type GeneratedSDKSelectionSetFoo = SDKSelectionSet<{ + \\"...Hee\\": GeneratedSDKSelectionSetHee; + \\"...Hoo\\": GeneratedSDKSelectionSetHoo; + }>;" + `); + }); +}); diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildUnionSelectionString.ts b/packages/plugins/typescript/typed-document-sdk/src/buildUnionSelectionString.ts new file mode 100644 index 00000000000..e0f6ef58f8c --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/src/buildUnionSelectionString.ts @@ -0,0 +1,13 @@ +import { stripIndent } from 'common-tags'; +import type { GraphQLUnionType } from 'graphql'; +import { buildSelectionSetName } from './buildObjectTypeSelectionString'; + +export const buildUnionSelectionString = (ttype: GraphQLUnionType) => { + const implementedTypes = ttype.getTypes().map(ttype => `"...${ttype.name}": ${buildSelectionSetName(ttype.name)};`); + + return stripIndent` + type ${buildSelectionSetName(ttype.name)} = SDKSelectionSet<{ + ${implementedTypes.join(`\n `)} + }>; + `; +}; diff --git a/packages/plugins/typescript/typed-document-sdk/src/index.ts b/packages/plugins/typescript/typed-document-sdk/src/index.ts index 55cbc5137b7..7ea1226068e 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/index.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/index.ts @@ -1,7 +1,18 @@ import { Types, PluginValidateFn, PluginFunction } from '@graphql-codegen/plugin-helpers'; -import { GraphQLSchema, isEnumType, isInputObjectType, isObjectType, isScalarType } from 'graphql'; +import { + GraphQLSchema, + isEnumType, + isInputObjectType, + isInterfaceType, + isObjectType, + isScalarType, + isUnionType, +} from 'graphql'; +import { buildInterfaceSelectionString } from './buildInterfaceSelectionString'; +import { buildObjectTypeArgumentString } from './buildObjectTypeArgumentString'; import { buildObjectTypeSelectionString } from './buildObjectTypeSelectionString'; import { buildSDKObjectString } from './buildSDKObjectString'; +import { buildUnionSelectionString } from './buildUnionSelectionString'; import { TypedDocumentSDKConfig } from './config'; import { importsString, contentsString } from './sdk-static'; @@ -16,8 +27,9 @@ export const plugin: PluginFunction = ( for (const graphQLType of Object.values(schema.getTypeMap())) { // selection set objects if (isObjectType(graphQLType)) { - contents.push(buildObjectTypeSelectionString(graphQLType)); + contents.push(buildObjectTypeSelectionString(graphQLType), buildObjectTypeArgumentString(graphQLType)); } + // input types if (isScalarType(graphQLType)) { inputTypeMap.push(` ${graphQLType.name}: Scalars['${graphQLType.name}'];`); @@ -30,6 +42,14 @@ export const plugin: PluginFunction = ( if (isInputObjectType(graphQLType)) { inputTypeMap.push(` ${graphQLType.name}: ${graphQLType.name};`); } + + if (isInterfaceType(graphQLType)) { + contents.push(buildInterfaceSelectionString(schema, graphQLType)); + } + + if (isUnionType(graphQLType)) { + contents.push(buildUnionSelectionString(graphQLType)); + } } contents.push(`type GeneratedSDKInputTypes = {\n${inputTypeMap.join('')}`); From d0fe21c6547574e9b2818c850c2ded58b29b5132 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Thu, 10 Feb 2022 10:24:04 +0100 Subject: [PATCH 13/17] fix: disallow empty selection sets --- .../typed-document-sdk/src/sdk-base.spec.ts | 59 ++++++++++++++++++- .../typed-document-sdk/src/sdk-base.ts | 8 +-- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts index 8d5c1a53833..67ea360ab91 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts @@ -880,6 +880,59 @@ describe('SDKLogic', () => { expect(document).toStrictEqual(expectedDocument); }); + it('query primitive field with variables', () => { + type InputTypes = { + String: string; + }; + type GeneratedSDKSelectionSetHello = SDKSelectionSet<{ + __typename?: true; + a?: + | true + | { + [SDKFieldArgumentSymbol]?: { + arg?: string | never; + }; + }; + }>; + type GeneratedSDKArgumentsHello = { + a: GeneratedSDKArgumentsHello & { + [SDKFieldArgumentSymbol]: { + arg: 'String'; + }; + }; + }; + type GeneratedSDKResultHello = { + a: InputTypes['String']; + }; + + const sdk = createSDK< + InputTypes, + GeneratedSDKSelectionSetHello, + GeneratedSDKArgumentsHello, + GeneratedSDKResultHello + >(); + + const document = sdk.query({ + name: 'Foo', + variables: { + myString: 'String', + }, + selection: { + a: { + [sdk.arguments]: { + arg: 'myString', + }, + }, + }, + }); + + expect(print(document)).toMatchInlineSnapshot(` + "query Foo($myString: String) { + a(arg: $myString) + }" + `); + }); + it('union types', () => { type InputTypes = { String: string; @@ -892,7 +945,7 @@ describe('SDKLogic', () => { __typename?: true; user?: SDKUnionSelectionSet<{ '...User': SDKSelectionSet<{ - __typename?: true; + __typename?: boolean; id?: boolean; login?: boolean; }>; @@ -924,8 +977,8 @@ describe('SDKLogic', () => { [SDKUnionResultSymbol]: true; User: { __typename: 'User'; - id?: InputTypes['String']; - login?: InputTypes['String']; + id: InputTypes['String']; + login: InputTypes['String']; }; Error: { __typename: 'Error'; diff --git a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts index c896cb9b3d6..81a1600ea6d 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts @@ -159,7 +159,7 @@ type SDKVariableDefinitions = { type SDKSelectionWithVariables< /** GraphQLTypeName -> TS type */ T_SDKInputTypeMap extends SDKInputTypeMap, - T_SDKSelectionSet, + T_SDKSelectionSet extends SDKSelectionSet, T_ArgumentType, /** variableName -> GraphQLTypeName */ T_VariableDefinitions extends SDKVariableDefinitions | void @@ -189,13 +189,13 @@ type SDKSelectionWithVariables< type SDK< T_SDKInputTypeMap extends SDKInputTypeMap, - T_SDKQuerySelectionSet, + T_SDKQuerySelectionSet extends SDKSelectionSet, T_QueryArgumentType, T_QueryResultType extends ResultType, - T_SDKMutationSelectionSet = void, + T_SDKMutationSelectionSet extends SDKSelectionSet | void = void, T_SDKMutationArgumentType = void, T_MutationResultType extends ResultType | void = void, - T_SDKSubscriptionSelectionSet = void, + T_SDKSubscriptionSelectionSet extends SDKSelectionSet | void = void, T_SDKSubscriptionArgumentType = void, T_SubscriptionResultType extends ResultType | void = void > = { From af7e82fc049769e708d3fb8c61979c59131460dc Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Thu, 10 Feb 2022 10:28:25 +0100 Subject: [PATCH 14/17] fix: no additional arguments --- .../src/buildObjectTypeSelectionString.spec.ts | 8 ++++---- .../src/buildObjectTypeSelectionString.ts | 4 ++-- .../typescript/typed-document-sdk/src/sdk-base.spec.ts | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.spec.ts b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.spec.ts index a7784590c12..143c32791f1 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.spec.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.spec.ts @@ -54,11 +54,11 @@ describe('buildObjectTypeSelectionString', () => { expect(buildObjectTypeSelectionString(graphQLObjectType)).toMatchInlineSnapshot(` "type GeneratedSDKSelectionSetHello = SDKSelectionSet<{ __typename?: true; - a?: true | { + a?: true | SDKSelectionSet<{ [SDKFieldArgumentSymbol]?: { arg?: string | never; } - }; + }>; }>;" `); }); @@ -80,11 +80,11 @@ describe('buildObjectTypeSelectionString', () => { expect(buildObjectTypeSelectionString(graphQLObjectType)).toMatchInlineSnapshot(` "type GeneratedSDKSelectionSetHello = SDKSelectionSet<{ __typename?: true; - a?: { + a?: SDKSelectionSet<{ [SDKFieldArgumentSymbol]: { arg: string | never; } - }; + }>; }>;" `); }); diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.ts b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.ts index 303cb4c9d8a..d16e8abd3fb 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.ts @@ -31,11 +31,11 @@ const buildFieldSelectionSetString = (field: GraphQLField): string => } const args = stripIndent` - { + SDKSelectionSet<{ [SDKFieldArgumentSymbol]${requireArguments ? `` : `?`}: { ${argumentPartials.join(`;\n`)}; } - } + }> ` .split(`\n`) .map((line, i) => (i === 0 ? line : ` ${line}`)) diff --git a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts index 67ea360ab91..a4f91e6e641 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts @@ -888,11 +888,11 @@ describe('SDKLogic', () => { __typename?: true; a?: | true - | { + | SDKSelectionSet<{ [SDKFieldArgumentSymbol]?: { arg?: string | never; }; - }; + }>; }>; type GeneratedSDKArgumentsHello = { a: GeneratedSDKArgumentsHello & { From 18099b43d7e4f25be70ddc3581d929a149f09f06 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Thu, 10 Feb 2022 12:32:51 +0100 Subject: [PATCH 15/17] generate union and interface argument types --- package.json | 2 +- .../typed-document-sdk/package.json | 3 +- .../typed-document-sdk/scripts/write-sdk.js | 6 +- .../src/buildInterfaceArgumentString.spec.ts | 65 +++++++++++++++++++ .../src/buildInterfaceArgumentString.ts | 15 +++++ .../src/buildObjectTypeArgumentString.ts | 2 +- .../src/buildObjectTypeSelectionString.ts | 2 +- .../src/buildUnionArgumentString.spec.ts | 31 +++++++++ .../src/buildUnionArgumentString.ts | 15 +++++ .../typed-document-sdk/src/index.ts | 11 +++- 10 files changed, 143 insertions(+), 9 deletions(-) create mode 100644 packages/plugins/typescript/typed-document-sdk/src/buildInterfaceArgumentString.spec.ts create mode 100644 packages/plugins/typescript/typed-document-sdk/src/buildInterfaceArgumentString.ts create mode 100644 packages/plugins/typescript/typed-document-sdk/src/buildUnionArgumentString.spec.ts create mode 100644 packages/plugins/typescript/typed-document-sdk/src/buildUnionArgumentString.ts diff --git a/package.json b/package.json index c638deb0c41..8ffce95a994 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "postinstall": "patch-package && husky install", "clean": "rimraf node_modules packages/{*,plugins/*/*,presets/*,utils/*}/node_modules", "prebuild": "rimraf packages/{*,plugins/*/*,presets/*,utils/*}/dist", - "build": "tsc --project tsconfig.json && bob build", + "build": "yarn workspace @graphql-codegen/typed-document-sdk run build && tsc --project tsconfig.json && bob build", "watch-build": "npx tsc-watch --project tsconfig.json --onSuccess \"bob build\"", "test": "jest --forceExit --no-watchman", "lint": "eslint --ext .ts .", diff --git a/packages/plugins/typescript/typed-document-sdk/package.json b/packages/plugins/typescript/typed-document-sdk/package.json index 9e16ce20866..0ced785a18a 100644 --- a/packages/plugins/typescript/typed-document-sdk/package.json +++ b/packages/plugins/typescript/typed-document-sdk/package.json @@ -11,7 +11,8 @@ "scripts": { "lint": "eslint **/*.ts", "test": "jest --no-watchman --config ../../../../jest.config.js", - "prepack": "bob prepack" + "prepack": "bob prepack", + "build": "node scripts/write-sdk.js" }, "peerDependencies": { "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0" diff --git a/packages/plugins/typescript/typed-document-sdk/scripts/write-sdk.js b/packages/plugins/typescript/typed-document-sdk/scripts/write-sdk.js index d4c1c547e48..5dc94294734 100644 --- a/packages/plugins/typescript/typed-document-sdk/scripts/write-sdk.js +++ b/packages/plugins/typescript/typed-document-sdk/scripts/write-sdk.js @@ -8,6 +8,8 @@ const sdkOutPath = path.resolve(__dirname, '..', 'src', 'sdk-static.ts'); const sdkBaseContents = fs.readFileSync(sdkBasePath, 'utf-8'); +const escape = str => str.replace(/`/g, `\``); + const [imports, body] = sdkBaseContents.split('// IMPORTS END'); if (body === undefined) { @@ -17,7 +19,7 @@ if (body === undefined) { fs.writeFileSync( sdkOutPath, ` -export const importsString = \`${imports}\` -export const contentsString = \`${body}\` +export const importsString = ${JSON.stringify(imports)}; +export const contentsString = ${JSON.stringify(body)}; ` ); diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildInterfaceArgumentString.spec.ts b/packages/plugins/typescript/typed-document-sdk/src/buildInterfaceArgumentString.spec.ts new file mode 100644 index 00000000000..8ab8c440bc6 --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/src/buildInterfaceArgumentString.spec.ts @@ -0,0 +1,65 @@ +import { GraphQLBoolean, GraphQLInterfaceType, GraphQLObjectType, GraphQLSchema } from 'graphql'; +import { buildInterfaceArgumentString } from './buildInterfaceArgumentString'; + +describe('buildInterfaceArgumentString', () => { + it('single interface member', () => { + const interfaceType = new GraphQLInterfaceType({ + name: 'Foo', + fields: { + a: { + type: GraphQLBoolean, + }, + }, + }); + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + a: { + type: interfaceType, + }, + }, + }), + types: [ + new GraphQLObjectType({ name: 'Hee', fields: { a: { type: GraphQLBoolean } }, interfaces: [interfaceType] }), + ], + }); + + expect(buildInterfaceArgumentString(schema, interfaceType)).toMatchInlineSnapshot(` + "type GeneratedSDKArgumentsFoo = SDKSelectionSet<{ + \\"...Hee\\": GeneratedSDKArgumentsHee; + }>;" + `); + }); + it('multiple interface member', () => { + const interfaceType = new GraphQLInterfaceType({ + name: 'Foo', + fields: { + a: { + type: GraphQLBoolean, + }, + }, + }); + const schema = new GraphQLSchema({ + query: new GraphQLObjectType({ + name: 'Query', + fields: { + a: { + type: interfaceType, + }, + }, + }), + types: [ + new GraphQLObjectType({ name: 'Hee', fields: { a: { type: GraphQLBoolean } }, interfaces: [interfaceType] }), + new GraphQLObjectType({ name: 'Hoo', fields: { a: { type: GraphQLBoolean } }, interfaces: [interfaceType] }), + ], + }); + + expect(buildInterfaceArgumentString(schema, interfaceType)).toMatchInlineSnapshot(` + "type GeneratedSDKArgumentsFoo = SDKSelectionSet<{ + \\"...Hee\\": GeneratedSDKArgumentsHee; + \\"...Hoo\\": GeneratedSDKArgumentsHoo; + }>;" + `); + }); +}); diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildInterfaceArgumentString.ts b/packages/plugins/typescript/typed-document-sdk/src/buildInterfaceArgumentString.ts new file mode 100644 index 00000000000..d036b12aebd --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/src/buildInterfaceArgumentString.ts @@ -0,0 +1,15 @@ +import { stripIndent } from 'common-tags'; +import type { GraphQLInterfaceType, GraphQLSchema } from 'graphql'; +import { buildObjectArgumentsName } from './buildObjectTypeArgumentString'; + +export const buildInterfaceArgumentString = (schema: GraphQLSchema, ttype: GraphQLInterfaceType) => { + const implementedTypes = schema + .getImplementations(ttype) + .objects.map(ttype => `"...${ttype.name}": ${buildObjectArgumentsName(ttype.name)};`); + + return stripIndent` + type ${buildObjectArgumentsName(ttype.name)} = SDKSelectionSet<{ + ${implementedTypes.join(`\n `)} + }>; + `; +}; diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeArgumentString.ts b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeArgumentString.ts index c9d7977413d..7dac46fedd4 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeArgumentString.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeArgumentString.ts @@ -41,7 +41,7 @@ export const buildObjectTypeArgumentString = (objectType: GraphQLObjectType) => } return stripIndent` type ${buildObjectArgumentsName(objectType.name)} = { - ${fields.join(`;\n `)} + ${fields.join(`\n `)} }; `; }; diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.ts b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.ts index d16e8abd3fb..79b14418cbf 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/buildObjectTypeSelectionString.ts @@ -64,7 +64,7 @@ export const buildObjectTypeSelectionString = (objectType: GraphQLObjectType): s return stripIndent` type ${buildSelectionSetName(objectType.name)} = SDKSelectionSet<{ __typename?: true; - ${fields.join(`;\n `)} + ${fields.join(`\n `)} }>; `; }; diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildUnionArgumentString.spec.ts b/packages/plugins/typescript/typed-document-sdk/src/buildUnionArgumentString.spec.ts new file mode 100644 index 00000000000..f3b3362be0e --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/src/buildUnionArgumentString.spec.ts @@ -0,0 +1,31 @@ +import { GraphQLBoolean, GraphQLObjectType, GraphQLUnionType } from 'graphql'; +import { buildUnionArgumentString } from './buildUnionArgumentString'; + +describe('buildUnionArgumentString', () => { + it('single union member', () => { + const unionType = new GraphQLUnionType({ + name: 'Foo', + types: [new GraphQLObjectType({ name: 'Hee', fields: { a: { type: GraphQLBoolean } } })], + }); + expect(buildUnionArgumentString(unionType)).toMatchInlineSnapshot(` + "type GeneratedSDKArgumentsFoo = { + \\"...Hee\\": GeneratedSDKArgumentsHee; + };" + `); + }); + it('multiple union member', () => { + const unionType = new GraphQLUnionType({ + name: 'Foo', + types: [ + new GraphQLObjectType({ name: 'Hee', fields: { a: { type: GraphQLBoolean } } }), + new GraphQLObjectType({ name: 'Hoo', fields: { a: { type: GraphQLBoolean } } }), + ], + }); + expect(buildUnionArgumentString(unionType)).toMatchInlineSnapshot(` + "type GeneratedSDKArgumentsFoo = { + \\"...Hee\\": GeneratedSDKArgumentsHee; + \\"...Hoo\\": GeneratedSDKArgumentsHoo; + };" + `); + }); +}); diff --git a/packages/plugins/typescript/typed-document-sdk/src/buildUnionArgumentString.ts b/packages/plugins/typescript/typed-document-sdk/src/buildUnionArgumentString.ts new file mode 100644 index 00000000000..a6a9e31c136 --- /dev/null +++ b/packages/plugins/typescript/typed-document-sdk/src/buildUnionArgumentString.ts @@ -0,0 +1,15 @@ +import { stripIndent } from 'common-tags'; +import type { GraphQLUnionType } from 'graphql'; +import { buildObjectArgumentsName } from './buildObjectTypeArgumentString'; + +export const buildUnionArgumentString = (ttype: GraphQLUnionType) => { + const implementedTypes = ttype + .getTypes() + .map(ttype => `"...${ttype.name}": ${buildObjectArgumentsName(ttype.name)};`); + + return stripIndent` + type ${buildObjectArgumentsName(ttype.name)} = { + ${implementedTypes.join(`\n `)} + }; + `; +}; diff --git a/packages/plugins/typescript/typed-document-sdk/src/index.ts b/packages/plugins/typescript/typed-document-sdk/src/index.ts index 7ea1226068e..b952c5ae5b0 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/index.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/index.ts @@ -8,10 +8,12 @@ import { isScalarType, isUnionType, } from 'graphql'; +import { buildInterfaceArgumentString } from './buildInterfaceArgumentString'; import { buildInterfaceSelectionString } from './buildInterfaceSelectionString'; import { buildObjectTypeArgumentString } from './buildObjectTypeArgumentString'; import { buildObjectTypeSelectionString } from './buildObjectTypeSelectionString'; import { buildSDKObjectString } from './buildSDKObjectString'; +import { buildUnionArgumentString } from './buildUnionArgumentString'; import { buildUnionSelectionString } from './buildUnionSelectionString'; import { TypedDocumentSDKConfig } from './config'; import { importsString, contentsString } from './sdk-static'; @@ -44,15 +46,18 @@ export const plugin: PluginFunction = ( } if (isInterfaceType(graphQLType)) { - contents.push(buildInterfaceSelectionString(schema, graphQLType)); + contents.push( + buildInterfaceSelectionString(schema, graphQLType), + buildInterfaceArgumentString(schema, graphQLType) + ); } if (isUnionType(graphQLType)) { - contents.push(buildUnionSelectionString(graphQLType)); + contents.push(buildUnionSelectionString(graphQLType), buildUnionArgumentString(graphQLType)); } } - contents.push(`type GeneratedSDKInputTypes = {\n${inputTypeMap.join('')}`); + contents.push(`type GeneratedSDKInputTypes = {\n${inputTypeMap.join('')} }`); // sdk object contents.push(buildSDKObjectString(schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType())); From 3c2e7932b969c45f4be7663ef16c86db8fe4b801 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Thu, 10 Feb 2022 18:35:39 +0100 Subject: [PATCH 16/17] fix: only allow specified fields on the root selection set --- .../typed-document-sdk/src/sdk-base.ts | 89 ++++++++++++------- 1 file changed, 56 insertions(+), 33 deletions(-) diff --git a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts index 81a1600ea6d..849ae577018 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.ts @@ -46,8 +46,8 @@ type ResultType = { [key: string]: any }; type SDKInlineFragmentKey = `...${T}`; -export const SDKFieldArgumentSymbol: unique symbol = Symbol('SDKFieldArguments'); -export const SDKUnionResultSymbol: unique symbol = Symbol('UnionResultSymbol'); +export const SDKFieldArgumentSymbol: unique symbol = Symbol('sdk.arguments'); +export const SDKUnionResultSymbol: unique symbol = Symbol('sdk.union'); type SDKExtractUnionTargets, TUnionMember extends string> = Exclude< TSelection, @@ -93,7 +93,9 @@ type SDKOperationType = AtLeastOnePropertyOf>; -export type SDKUnionSelectionSet = any> = SDKSelectionSet; +export type SDKUnionSelectionSet = any> = AtLeastOnePropertyOf< + NoExtraProperties +>; type SDKInputTypeMap = { [inputTypeName: string]: any }; @@ -159,33 +161,38 @@ type SDKVariableDefinitions = { type SDKSelectionWithVariables< /** GraphQLTypeName -> TS type */ T_SDKInputTypeMap extends SDKInputTypeMap, - T_SDKSelectionSet extends SDKSelectionSet, + T_SDKPossibleSelectionSet extends SDKSelectionSet, + T_SDKUserSelectionSet extends SDKSelectionSet, T_ArgumentType, /** variableName -> GraphQLTypeName */ T_VariableDefinitions extends SDKVariableDefinitions | void -> = { - [U_FieldName in keyof T_SDKSelectionSet]: U_FieldName extends typeof SDKFieldArgumentSymbol - ? T_VariableDefinitions extends SDKVariableDefinitions - ? T_ArgumentType extends { [SDKFieldArgumentSymbol]: infer U_Arguments } - ? { - // From T_VariableDefinitions we want all keys whose value matches `U_Arguments[V_ArgumentName]` - [V_ArgumentName in keyof T_SDKSelectionSet[U_FieldName] /* ArgumentType */]: KeysMatching< - T_VariableDefinitions, - // all legit argument values - V_ArgumentName extends keyof U_Arguments ? U_Arguments[V_ArgumentName] : never - >; - } - : never - : never - : T_SDKSelectionSet[U_FieldName] extends SDKSelectionSet - ? SDKSelectionWithVariables< - T_SDKInputTypeMap, - T_SDKSelectionSet[U_FieldName], - U_FieldName extends keyof T_ArgumentType ? T_ArgumentType[U_FieldName] : never, - T_VariableDefinitions - > - : T_SDKSelectionSet[U_FieldName]; -}; +> = + | { + [U_FieldName in keyof T_SDKUserSelectionSet]: U_FieldName extends typeof SDKFieldArgumentSymbol + ? T_VariableDefinitions extends SDKVariableDefinitions + ? T_ArgumentType extends { [SDKFieldArgumentSymbol]: infer U_Arguments } + ? { + // From T_VariableDefinitions we want all keys whose value matches `U_Arguments[V_ArgumentName]` + [V_ArgumentName in keyof T_SDKUserSelectionSet[U_FieldName] /* ArgumentType */]: KeysMatching< + T_VariableDefinitions, + // all legit argument values + V_ArgumentName extends keyof U_Arguments ? U_Arguments[V_ArgumentName] : never + >; + } + : never + : never + : U_FieldName extends keyof T_SDKPossibleSelectionSet + ? T_SDKUserSelectionSet[U_FieldName] extends SDKSelectionSet + ? SDKSelectionWithVariables< + T_SDKInputTypeMap, + T_SDKPossibleSelectionSet[U_FieldName], + T_SDKUserSelectionSet[U_FieldName], + U_FieldName extends keyof T_ArgumentType ? T_ArgumentType[U_FieldName] : never, + T_VariableDefinitions + > + : T_SDKUserSelectionSet[U_FieldName] + : never; + }; type SDK< T_SDKInputTypeMap extends SDKInputTypeMap, @@ -199,6 +206,10 @@ type SDK< T_SDKSubscriptionArgumentType = void, T_SubscriptionResultType extends ResultType | void = void > = { + arguments: typeof SDKFieldArgumentSymbol; + /** + * Build a query operation document node. + */ query< Q_VariableDefinitions extends SDKVariableDefinitions | void, Q_Selection extends T_SDKQuerySelectionSet @@ -213,14 +224,21 @@ type SDK< variables?: never; } ) & { - selection: SDKSelectionWithVariables; + selection: SDKSelectionWithVariables< + T_SDKInputTypeMap, + T_SDKQuerySelectionSet, + Q_Selection, + T_QueryArgumentType, + Q_VariableDefinitions + >; } ): SDKSelectionTypedDocumentNode; - - arguments: typeof SDKFieldArgumentSymbol; } & (T_SDKMutationSelectionSet extends SDKSelectionSet ? T_MutationResultType extends ResultType ? { + /** + * Build a mutation operation document node. + */ mutation< M_VariableDefinitions extends SDKVariableDefinitions, M_Selection extends T_SDKMutationSelectionSet @@ -237,6 +255,7 @@ type SDK< ) & { selection: SDKSelectionWithVariables< T_SDKInputTypeMap, + T_SDKMutationSelectionSet, M_Selection, T_SDKMutationArgumentType, M_VariableDefinitions @@ -249,6 +268,9 @@ type SDK< (T_SDKSubscriptionSelectionSet extends SDKSelectionSet ? T_SubscriptionResultType extends ResultType ? { + /** + * Build a subscription operation document node. + */ subscription< S_VariableDefinitions extends SDKVariableDefinitions, S_Selection extends T_SDKSubscriptionSelectionSet @@ -265,6 +287,7 @@ type SDK< ) & { selection: SDKSelectionWithVariables< T_SDKInputTypeMap, + T_SDKSubscriptionSelectionSet, S_Selection, T_SDKSubscriptionArgumentType, S_VariableDefinitions @@ -460,13 +483,13 @@ const sdkHandler = export function createSDK< T_SDKInputTypeMap extends SDKInputTypeMap, - T_SDKQuerySelectionSet, + T_SDKQuerySelectionSet extends SDKSelectionSet, T_SDKQueryArguments, T_QueryResultType extends ResultType, - T_SDKMutationSelectionSet = void, + T_SDKMutationSelectionSet extends SDKSelectionSet | void = void, T_SDKMutationArguments = void, T_MutationResultType extends ResultType | void = void, - T_SDKSubscriptionSelectionSet = void, + T_SDKSubscriptionSelectionSet extends SDKSelectionSet | void = void, T_SDKSubscriptionArguments = void, T_SubscriptionResultType extends ResultType | void = void >(): SDK< From 336189d95bbb20cf8bdacaee20da44fe43854e21 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Mon, 14 Feb 2022 08:15:44 +0100 Subject: [PATCH 17/17] fix: test fixtures --- .../typed-document-sdk/src/sdk-base.spec.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts index a4f91e6e641..61336142ce6 100644 --- a/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts +++ b/packages/plugins/typescript/typed-document-sdk/src/sdk-base.spec.ts @@ -10,13 +10,13 @@ import { describe('SDKLogic', () => { it('anonymous query operation', () => { const sdk = createSDK< - {}, {}, SDKSelectionSet<{ __typename?: true; }>, + {}, { - __typename?: 'Query'; + __typename: 'Query'; } >(); const operation = sdk.query({ @@ -58,11 +58,11 @@ describe('SDKLogic', () => { it('named query operation', () => { const sdk = createSDK< - {}, {}, SDKSelectionSet<{ __typename?: true; }>, + {}, { __typename?: 'Query'; } @@ -215,7 +215,6 @@ describe('SDKLogic', () => { it('nested operation', () => { const sdk = createSDK< - {}, {}, SDKSelectionSet<{ __typename?: true; @@ -223,10 +222,11 @@ describe('SDKLogic', () => { a?: true; }; }>, + {}, { - __typename?: 'Query'; - foo?: { - a?: boolean; + __typename: 'Query'; + foo: { + a: boolean; }; } >(); @@ -1192,6 +1192,7 @@ describe('SDKLogic', () => { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore function api_test(value: OperationType) { + value; if (value.user.__typename === 'User') { value.user.id; // eslint-disable-next-line @typescript-eslint/ban-ts-comment