From eb2f3cf3906c7203f2a8cf6c4cb00278695aaf0e Mon Sep 17 00:00:00 2001 From: Koen Punt Date: Sun, 20 Jan 2019 21:42:29 +0100 Subject: [PATCH] feat(gg): generate resolver types for GQL interfaces, unions (#149) closes #347, #346 --- packages/graphqlgen/benchmarks/micro/index.ts | 10 + packages/graphqlgen/package.json | 1 + .../graphqlgen/src/generators/common.spec.ts | 8 +- packages/graphqlgen/src/generators/common.ts | 76 ++- .../src/generators/flow-generator.ts | 135 ++++- .../src/generators/flow-scaffolder.ts | 32 +- .../graphqlgen/src/generators/ts-generator.ts | 286 ++++++++++- .../src/generators/ts-scaffolder.ts | 28 +- packages/graphqlgen/src/source-helper.ts | 62 ++- .../tests/fixtures/interface/schema.graphql | 29 ++ .../src/tests/fixtures/interface/types.ts | 17 + .../src/tests/fixtures/union/schema.graphql | 5 +- .../flow/__snapshots__/basic.test.ts.snap | 162 +++--- .../__snapshots__/basic.test.ts.snap | 482 +++++++++++++++--- .../src/tests/typescript/basic.test.ts | 15 + packages/graphqlgen/src/types.ts | 2 + 16 files changed, 1168 insertions(+), 182 deletions(-) create mode 100644 packages/graphqlgen/src/tests/fixtures/interface/schema.graphql create mode 100644 packages/graphqlgen/src/tests/fixtures/interface/types.ts diff --git a/packages/graphqlgen/benchmarks/micro/index.ts b/packages/graphqlgen/benchmarks/micro/index.ts index cae5647f..ec7253e2 100644 --- a/packages/graphqlgen/benchmarks/micro/index.ts +++ b/packages/graphqlgen/benchmarks/micro/index.ts @@ -3,6 +3,7 @@ import * as Benchmark from '../lib/benchmark' const type = { name: 'Z', + implements: null, type: { name: 'Z', isInput: false, @@ -18,6 +19,7 @@ const type = { const typeMap: Core.InputTypesMap = { A: { name: 'A', + implements: null, type: { name: 'A', isInput: true, @@ -41,6 +43,7 @@ const typeMap: Core.InputTypesMap = { isUnion: false, isRequired: false, isArray: false, + isArrayRequired: false, }, }, { @@ -56,12 +59,14 @@ const typeMap: Core.InputTypesMap = { isUnion: false, isRequired: false, isArray: false, + isArrayRequired: false, }, }, ], }, B: { name: 'B', + implements: null, type: { name: 'B', isInput: true, @@ -85,12 +90,14 @@ const typeMap: Core.InputTypesMap = { isUnion: false, isRequired: false, isArray: false, + isArrayRequired: false, }, }, ], }, C: { name: 'C', + implements: null, type: { name: 'C', isInput: true, @@ -114,12 +121,14 @@ const typeMap: Core.InputTypesMap = { isUnion: false, isRequired: false, isArray: false, + isArrayRequired: false, }, }, ], }, D: { name: 'D', + implements: null, type: { name: 'D', isInput: true, @@ -143,6 +152,7 @@ const typeMap: Core.InputTypesMap = { isUnion: false, isRequired: false, isArray: false, + isArrayRequired: false, }, }, ], diff --git a/packages/graphqlgen/package.json b/packages/graphqlgen/package.json index e089077f..1201f83e 100644 --- a/packages/graphqlgen/package.json +++ b/packages/graphqlgen/package.json @@ -20,6 +20,7 @@ "watch": "tsc -w", "lint": "tslint --project tsconfig.json {src,test}/**/*.ts", "test": "jest", + "check:types": "yarn tsc --noEmit", "test:watch": "jest --watch", "test:ci": "npm run lint && jest --maxWorkers 4", "gen": "ts-node --files src/index.ts" diff --git a/packages/graphqlgen/src/generators/common.spec.ts b/packages/graphqlgen/src/generators/common.spec.ts index 7935b95e..3c95a7a0 100644 --- a/packages/graphqlgen/src/generators/common.spec.ts +++ b/packages/graphqlgen/src/generators/common.spec.ts @@ -1,7 +1,8 @@ +import * as Source from '../source-helper' import * as Common from './common' it('getDistinctInputTypes', () => { - const Z = { + const Z: Source.GraphQLTypeObject = { name: 'Z', type: { name: 'Z', @@ -13,11 +14,13 @@ it('getDistinctInputTypes', () => { isUnion: false, }, fields: [], + implements: null, } const typeMap: Common.InputTypesMap = { A: { name: 'A', + implements: null, type: { name: 'A', isInput: true, @@ -64,6 +67,7 @@ it('getDistinctInputTypes', () => { }, B: { name: 'B', + implements: null, type: { name: 'B', isInput: true, @@ -94,6 +98,7 @@ it('getDistinctInputTypes', () => { }, C: { name: 'C', + implements: null, type: { name: 'C', isInput: true, @@ -124,6 +129,7 @@ it('getDistinctInputTypes', () => { }, D: { name: 'D', + implements: null, type: { name: 'D', isInput: true, diff --git a/packages/graphqlgen/src/generators/common.ts b/packages/graphqlgen/src/generators/common.ts index 4f133a1c..cd73e6ba 100644 --- a/packages/graphqlgen/src/generators/common.ts +++ b/packages/graphqlgen/src/generators/common.ts @@ -2,9 +2,11 @@ import * as os from 'os' import { GraphQLTypeObject, - GraphQLType, + GraphQLTypeDefinition, GraphQLTypeField, getGraphQLEnumValues, + GraphQLInterfaceObject, + GraphQLUnionObject, } from '../source-helper' import { ModelMap, ContextDefinition, GenerateArgs, Model } from '../types' import { @@ -28,6 +30,24 @@ export interface TypeToInputTypeAssociation { [objectTypeName: string]: string[] } +export type InterfacesMap = Record + +export const createInterfacesMap = ( + interfaces: GraphQLInterfaceObject[], +): InterfacesMap => + interfaces.reduce((interfacesMap, inter) => { + interfacesMap[inter.name] = inter.implementors + return interfacesMap + }, {}) + +export type UnionsMap = Record + +export const createUnionsMap = (unions: GraphQLUnionObject[]): UnionsMap => + unions.reduce((unionsMap, union) => { + unionsMap[union.name] = union.types + return unionsMap + }, {}) + export function fieldsFromModelDefinition( modelDef: TypeDefinition, ): FieldDefinition[] { @@ -114,7 +134,7 @@ export function getContextName(context?: ContextDefinition) { } export function getModelName( - type: GraphQLType, + type: GraphQLTypeDefinition, modelMap: ModelMap, emptyType: string = '{}', ): string { @@ -199,10 +219,17 @@ const kv = ( return `${key}${isOptional ? '?' : ''}: ${value}` } -const array = (innerType: string, config: { innerUnion?: boolean } = {}) => { +const array = ( + innerType: string, + config: { innerUnion?: boolean } = {}, +): string => { return config.innerUnion ? `${innerType}[]` : `Array<${innerType}>` } +const union = (types: string[]): string => { + return types.join(' | ') +} + type FieldPrintOptions = { isReturn?: boolean } @@ -210,10 +237,45 @@ type FieldPrintOptions = { export const printFieldLikeType = ( field: GraphQLTypeField, modelMap: ModelMap, + interfacesMap: InterfacesMap, + unionsMap: UnionsMap, options: FieldPrintOptions = { isReturn: false, }, ): string => { + if (field.type.isInterface || field.type.isUnion) { + const typesMap = field.type.isInterface ? interfacesMap : unionsMap + + const modelNames = typesMap[field.type.name].map(type => + getModelName(type, modelMap), + ) + + let rendering = union(modelNames) + + if (!field.type.isRequired) { + rendering = nullable(rendering) + } + + if (field.type.isArray) { + rendering = array(rendering, { innerUnion: false }) + } + + if (!field.type.isArrayRequired) { + rendering = nullable(rendering) + } + + // We do not have to handle defaults becuase graphql only + // supports defaults on field params but conversely + // interfaces and unions are only supported on output. Therefore + // these two features will never cross. + + // No check for isReturn option because unions and interfaces + // cannot be used to type graphql field parameters which implies + // this branch will always be for a return case. + + return rendering + } + const name = field.type.isScalar ? getTypeFromGraphQLType(field.type.name) : field.type.isInput || field.type.isEnum @@ -335,11 +397,13 @@ export function isParentType(name: string) { export function groupModelsNameByImportPath(models: Model[]) { return models.reduce<{ [importPath: string]: string[] }>((acc, model) => { - if (acc[model.importPathRelativeToOutput] === undefined) { - acc[model.importPathRelativeToOutput] = [] + const fileModels = acc[model.importPathRelativeToOutput] || [] + + if (!fileModels.includes(model.definition.name)) { + fileModels.push(model.definition.name) } - acc[model.importPathRelativeToOutput].push(model.definition.name) + acc[model.importPathRelativeToOutput] = fileModels return acc }, {}) diff --git a/packages/graphqlgen/src/generators/flow-generator.ts b/packages/graphqlgen/src/generators/flow-generator.ts index fed9c3bf..6ce99dec 100644 --- a/packages/graphqlgen/src/generators/flow-generator.ts +++ b/packages/graphqlgen/src/generators/flow-generator.ts @@ -18,6 +18,10 @@ import { renderDefaultResolvers, renderEnums, TypeToInputTypeAssociation, + InterfacesMap, + UnionsMap, + createInterfacesMap, + createUnionsMap, } from './common' export function format(code: string, options: prettier.Options = {}) { @@ -69,12 +73,21 @@ export function generate(args: GenerateArgs): string { } }, {}) + const interfacesMap = createInterfacesMap(args.interfaces) + const unionsMap = createUnionsMap(args.unions) + return `\ ${renderHeader(args)} ${renderEnums(args)} - ${renderNamespaces(args, typeToInputTypeAssociation, inputTypesMap)} + ${renderNamespaces( + args, + interfacesMap, + unionsMap, + typeToInputTypeAssociation, + inputTypesMap, + )} ${renderResolvers(args)} @@ -115,19 +128,30 @@ function renderContext(context?: ContextDefinition) { function renderNamespaces( args: GenerateArgs, + interfacesMap: InterfacesMap, + unionsMap: UnionsMap, typeToInputTypeAssociation: TypeToInputTypeAssociation, inputTypesMap: InputTypesMap, ): string { return args.types .filter(type => type.type.isObject) .map(type => - renderNamespace(type, typeToInputTypeAssociation, inputTypesMap, args), + renderNamespace( + type, + interfacesMap, + unionsMap, + typeToInputTypeAssociation, + inputTypesMap, + args, + ), ) .join(os.EOL) } function renderNamespace( type: GraphQLTypeObject, + interfacesMap: InterfacesMap, + unionsMap: UnionsMap, typeToInputTypeAssociation: TypeToInputTypeAssociation, inputTypesMap: InputTypesMap, args: GenerateArgs, @@ -141,15 +165,29 @@ function renderNamespace( ${renderInputTypeInterfaces( type, args.modelMap, + interfacesMap, + unionsMap, typeToInputTypeAssociation, inputTypesMap, )} - ${renderInputArgInterfaces(type, args.modelMap)} + ${renderInputArgInterfaces(type, args.modelMap, interfacesMap, unionsMap)} - ${renderResolverFunctionInterfaces(type, args.modelMap, args.context)} + ${renderResolverFunctionInterfaces( + type, + args.modelMap, + interfacesMap, + unionsMap, + args.context, + )} - ${renderResolverTypeInterface(type, args.modelMap, args.context)} + ${renderResolverTypeInterface( + type, + args.modelMap, + interfacesMap, + unionsMap, + args.context, + )} ${/* TODO renderResolverClass(type, modelMap) */ ''} ` @@ -158,6 +196,8 @@ function renderNamespace( function renderInputTypeInterfaces( type: GraphQLTypeObject, modelMap: ModelMap, + interfacesMap: InterfacesMap, + unionsMap: UnionsMap, typeToInputTypeAssociation: TypeToInputTypeAssociation, inputTypesMap: InputTypesMap, ) { @@ -171,7 +211,7 @@ function renderInputTypeInterfaces( inputTypesMap[typeAssociation].name, )} { ${inputTypesMap[typeAssociation].fields.map(field => - printFieldLikeType(field, modelMap), + printFieldLikeType(field, modelMap, interfacesMap, unionsMap), )} }` }) @@ -181,9 +221,13 @@ function renderInputTypeInterfaces( function renderInputArgInterfaces( type: GraphQLTypeObject, modelMap: ModelMap, + interfacesMap: InterfacesMap, + unionsMap: UnionsMap, ): string { return type.fields - .map(field => renderInputArgInterface(type, field, modelMap)) + .map(field => + renderInputArgInterface(type, field, modelMap, interfacesMap, unionsMap), + ) .join(os.EOL) } @@ -191,6 +235,8 @@ function renderInputArgInterface( type: GraphQLTypeObject, field: GraphQLTypeField, modelMap: ModelMap, + interfacesMap: InterfacesMap, + unionsMap: UnionsMap, ): string { if (field.arguments.length === 0) { return '' @@ -200,10 +246,12 @@ function renderInputArgInterface( export interface ${getInputArgName(type, field)} { ${field.arguments .map(arg => - printFieldLikeType(arg as GraphQLTypeField, modelMap).replace( - ': ', - `: ${getArgTypePrefix(type, arg)}`, - ), + printFieldLikeType( + arg as GraphQLTypeField, + modelMap, + interfacesMap, + unionsMap, + ).replace(': ', `: ${getArgTypePrefix(type, arg)}`), ) .join(',' + os.EOL)} } @@ -227,11 +275,20 @@ const getArgTypePrefix = ( function renderResolverFunctionInterfaces( type: GraphQLTypeObject, modelMap: ModelMap, + interfacesMap: InterfacesMap, + unionsMap: UnionsMap, context?: ContextDefinition, ): string { return type.fields .map(field => - renderResolverFunctionInterface(field, type, modelMap, context), + renderResolverFunctionInterface( + field, + type, + modelMap, + interfacesMap, + unionsMap, + context, + ), ) .join(os.EOL) } @@ -240,6 +297,8 @@ function renderResolverFunctionInterface( field: GraphQLTypeField, type: GraphQLTypeObject, modelMap: ModelMap, + interfacesMap: InterfacesMap, + unionsMap: UnionsMap, context?: ContextDefinition, ): string { const resolverName = `${upperFirst(type.name)}_${upperFirst( @@ -254,9 +313,15 @@ function renderResolverFunctionInterface( ) ` - const returnType = printFieldLikeType(field, modelMap, { - isReturn: true, - }) + const returnType = printFieldLikeType( + field, + modelMap, + interfacesMap, + unionsMap, + { + isReturn: true, + }, + ) if (type.name === 'Subscription') { return ` @@ -275,13 +340,22 @@ function renderResolverFunctionInterface( function renderResolverTypeInterface( type: GraphQLTypeObject, modelMap: ModelMap, + interfacesMap: InterfacesMap, + unionsMap: UnionsMap, context?: ContextDefinition, ): string { return ` export interface ${upperFirst(type.name)}_Resolvers { ${type.fields .map(field => - renderResolverTypeInterfaceFunction(field, type, modelMap, context), + renderResolverTypeInterfaceFunction( + field, + type, + modelMap, + interfacesMap, + unionsMap, + context, + ), ) .join(os.EOL)} } @@ -292,6 +366,8 @@ function renderResolverTypeInterfaceFunction( field: GraphQLTypeField, type: GraphQLTypeObject, modelMap: ModelMap, + interfacesMap: InterfacesMap, + unionsMap: UnionsMap, context?: ContextDefinition, ): string { const resolverDefinition = ` @@ -301,9 +377,15 @@ function renderResolverTypeInterfaceFunction( ctx: ${getContextName(context)}, info: GraphQLResolveInfo, )` - const returnType = printFieldLikeType(field, modelMap, { - isReturn: true, - }) + const returnType = printFieldLikeType( + field, + modelMap, + interfacesMap, + unionsMap, + { + isReturn: true, + }, + ) if (type.name === 'Subscription') { return ` @@ -323,10 +405,17 @@ function renderResolverTypeInterfaceFunction( function renderResolvers(args: GenerateArgs): string { return ` export interface Resolvers { - ${args.types - .filter(type => type.type.isObject) - .map(type => `${type.name}: ${upperFirst(type.name)}_Resolvers`) - .join(',' + os.EOL)} + ${[ + ...args.types + .filter(type => type.type.isObject) + .map(type => `${type.name}: ${upperFirst(type.name)}_Resolvers`), + ...args.interfaces.map( + type => `${type.name}?: ${upperFirst(type.name)}_Resolvers`, + ), + ...args.unions.map( + type => `${type.name}?: ${upperFirst(type.name)}_Resolvers`, + ), + ].join(`,${os.EOL}`)} } ` } diff --git a/packages/graphqlgen/src/generators/flow-scaffolder.ts b/packages/graphqlgen/src/generators/flow-scaffolder.ts index 9b075567..46a833eb 100644 --- a/packages/graphqlgen/src/generators/flow-scaffolder.ts +++ b/packages/graphqlgen/src/generators/flow-scaffolder.ts @@ -1,6 +1,10 @@ import { GenerateArgs, CodeFileLike } from '../types' import { upperFirst } from '../utils' -import { GraphQLTypeObject } from '../source-helper' +import { + GraphQLTypeObject, + GraphQLInterfaceObject, + GraphQLUnionObject, +} from '../source-helper' import { fieldsFromModelDefinition, shouldScaffoldFieldResolver, @@ -59,6 +63,27 @@ function renderExports(types: GraphQLTypeObject[]): string { .join(',')} }` } + +function renderPolyResolvers( + type: GraphQLInterfaceObject | GraphQLUnionObject, +): CodeFileLike { + const upperTypeName = upperFirst(type.name) + + const code = `\ + // @flow + // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. + // Please do not import this file directly but copy & paste to your application code. + + import { ${upperTypeName}_Resolvers } from '[TEMPLATE-INTERFACES-PATH]' + + export const ${type.name}: ${upperTypeName}_Resolvers = { + __resolveType: (parent, ctx, info) => { + throw new Error('Resolver not implemented') + } + }` + return { path: `${type.name}.ts`, force: false, code } +} + function renderResolvers( type: GraphQLTypeObject, args: GenerateArgs, @@ -98,6 +123,11 @@ export function generate(args: GenerateArgs): CodeFileLike[] { .filter(type => !isParentType(type.name)) .map(type => renderResolvers(type, args)) + files = files.concat( + args.interfaces.map(type => renderPolyResolvers(type)), + args.unions.map(type => renderPolyResolvers(type)), + ) + files = files.concat( args.types .filter(type => isParentType(type.name)) diff --git a/packages/graphqlgen/src/generators/ts-generator.ts b/packages/graphqlgen/src/generators/ts-generator.ts index ff234d94..8c5a0f42 100644 --- a/packages/graphqlgen/src/generators/ts-generator.ts +++ b/packages/graphqlgen/src/generators/ts-generator.ts @@ -2,7 +2,13 @@ import * as os from 'os' import * as prettier from 'prettier' import { GenerateArgs, ModelMap, ContextDefinition } from '../types' -import { GraphQLTypeField, GraphQLTypeObject } from '../source-helper' +import { + GraphQLTypeField, + GraphQLTypeObject, + GraphQLInterfaceObject, + GraphQLTypeDefinition, + GraphQLUnionObject, +} from '../source-helper' import { renderDefaultResolvers, getContextName, @@ -13,6 +19,10 @@ import { getDistinctInputTypes, renderEnums, groupModelsNameByImportPath, + InterfacesMap, + UnionsMap, + createInterfacesMap, + createUnionsMap, } from './common' import { TypeAliasDefinition } from '../introspection/types' import { upperFirst } from '../utils' @@ -66,23 +76,45 @@ export function generate(args: GenerateArgs): string { } }, {}) + const interfacesMap = createInterfacesMap(args.interfaces) + const unionsMap = createUnionsMap(args.unions) + const hasPolymorphicObjects = + Object.keys(interfacesMap).length > 0 || Object.keys(unionsMap).length > 0 + return `\ - ${renderHeader(args)} + ${renderHeader(args, { hasPolymorphicObjects })} ${renderEnums(args)} - ${renderNamespaces(args, typeToInputTypeAssociation, inputTypesMap)} + ${renderNamespaces( + args, + interfacesMap, + unionsMap, + typeToInputTypeAssociation, + inputTypesMap, + )} ${renderResolvers(args)} ` } -function renderHeader(args: GenerateArgs): string { +type HeaderOptions = { + hasPolymorphicObjects?: boolean +} + +function renderHeader( + args: GenerateArgs, + { hasPolymorphicObjects = false }: HeaderOptions = {}, +): string { + const imports = hasPolymorphicObjects + ? ['GraphQLResolveInfo', 'GraphQLTypeResolver', 'GraphQLIsTypeOfFn'] + : ['GraphQLResolveInfo'] + return ` // Code generated by github.com/prisma/graphqlgen, DO NOT EDIT. -import { GraphQLResolveInfo } from 'graphql' +import { ${imports.join(', ')} } from 'graphql' ${renderImports(args)} ` } @@ -133,19 +165,105 @@ function importsToString( function renderNamespaces( args: GenerateArgs, + interfacesMap: InterfacesMap, + unionsMap: UnionsMap, + typeToInputTypeAssociation: TypeToInputTypeAssociation, + inputTypesMap: InputTypesMap, +): string { + return `\ + ${renderObjectNamespaces( + args, + interfacesMap, + unionsMap, + typeToInputTypeAssociation, + inputTypesMap, + )} + + ${renderInterfaceNamespaces(args, interfacesMap, unionsMap)} + + ${renderUnionNamespaces(args)} + ` +} + +function renderObjectNamespaces( + args: GenerateArgs, + interfacesMap: InterfacesMap, + unionsMap: UnionsMap, typeToInputTypeAssociation: TypeToInputTypeAssociation, inputTypesMap: InputTypesMap, ): string { return args.types .filter(type => type.type.isObject) .map(type => - renderNamespace(type, typeToInputTypeAssociation, inputTypesMap, args), + renderNamespace( + type, + interfacesMap, + unionsMap, + typeToInputTypeAssociation, + inputTypesMap, + args, + ), ) .join(os.EOL) } +function renderInterfaceNamespaces( + args: GenerateArgs, + interfacesMap: InterfacesMap, + unionsMap: UnionsMap, +): string { + return args.interfaces + .map(type => renderInterfaceNamespace(type, interfacesMap, unionsMap, args)) + .join(os.EOL) +} + +function renderUnionNamespaces(args: GenerateArgs): string { + return args.unions.map(type => renderUnionNamespace(type, args)).join(os.EOL) +} + +function renderInterfaceNamespace( + graphQLTypeObject: GraphQLInterfaceObject, + interfacesMap: InterfacesMap, + unionsMap: UnionsMap, + args: GenerateArgs, +): string { + return `\ + export namespace ${graphQLTypeObject.name}Resolvers { + ${renderInputArgInterfaces( + graphQLTypeObject, + args.modelMap, + interfacesMap, + unionsMap, + )} + + export interface Type { + __resolveType: GraphQLTypeResolver<${graphQLTypeObject.implementors + .map(interfaceType => getModelName(interfaceType, args.modelMap)) + .join(' | ')}, ${getContextName(args.context)}>; + } + } + ` +} + +function renderUnionNamespace( + graphQLTypeObject: GraphQLUnionObject, + args: GenerateArgs, +): string { + return `\ + export namespace ${graphQLTypeObject.name}Resolvers { + export interface Type { + __resolveType?: GraphQLTypeResolver<${graphQLTypeObject.types + .map(interfaceType => getModelName(interfaceType, args.modelMap)) + .join(' | ')}, ${getContextName(args.context)}>; + } + } + ` +} + function renderNamespace( graphQLTypeObject: GraphQLTypeObject, + interfacesMap: InterfacesMap, + unionsMap: UnionsMap, typeToInputTypeAssociation: TypeToInputTypeAssociation, inputTypesMap: InputTypesMap, args: GenerateArgs, @@ -158,21 +276,32 @@ function renderNamespace( ${renderInputTypeInterfaces( graphQLTypeObject, args.modelMap, + interfacesMap, + unionsMap, typeToInputTypeAssociation, inputTypesMap, )} - ${renderInputArgInterfaces(graphQLTypeObject, args.modelMap)} + ${renderInputArgInterfaces( + graphQLTypeObject, + args.modelMap, + interfacesMap, + unionsMap, + )} ${renderResolverFunctionInterfaces( graphQLTypeObject, args.modelMap, + interfacesMap, + unionsMap, args.context, )} ${renderResolverTypeInterface( graphQLTypeObject, args.modelMap, + interfacesMap, + unionsMap, args.context, )} @@ -181,9 +310,48 @@ function renderNamespace( ` } +function renderIsTypeOfFunctionInterface( + type: GraphQLTypeObject | GraphQLInterfaceObject, + modelMap: ModelMap, + interfacesMap: InterfacesMap, + unionsMap: UnionsMap, + context?: ContextDefinition, +) { + let possibleTypes: GraphQLTypeDefinition[] = [] + + // TODO Refactor once type is a proper discriminated union + if (!type.type.isInterface) { + type = type as GraphQLTypeObject + if (type.implements) { + possibleTypes = type.implements.reduce( + (obj: GraphQLTypeDefinition[], interfaceName) => { + return [...obj, ...interfacesMap[interfaceName]] + }, + [], + ) + } + } + + for (let unionName in unionsMap) { + if (unionsMap[unionName].find(unionType => unionType.name === type.name)) { + possibleTypes = unionsMap[unionName] + } + } + + if (possibleTypes.length === 0) { + return '' + } + return `\ + __isTypeOf?: GraphQLIsTypeOfFn<${possibleTypes + .map(possibleType => getModelName(possibleType, modelMap)) + .join(' | ')}, ${getContextName(context)}>;` +} + function renderInputTypeInterfaces( type: GraphQLTypeObject, modelMap: ModelMap, + interfacesMap: InterfacesMap, + unionsMap: UnionsMap, typeToInputTypeAssociation: TypeToInputTypeAssociation, inputTypesMap: InputTypesMap, ) { @@ -195,7 +363,7 @@ function renderInputTypeInterfaces( .map(typeAssociation => { return `export interface ${inputTypesMap[typeAssociation].name} { ${inputTypesMap[typeAssociation].fields.map(field => - printFieldLikeType(field, modelMap), + printFieldLikeType(field, modelMap, interfacesMap, unionsMap), )} }` }) @@ -203,17 +371,23 @@ function renderInputTypeInterfaces( } function renderInputArgInterfaces( - type: GraphQLTypeObject, + type: GraphQLTypeObject | GraphQLInterfaceObject, modelMap: ModelMap, + interfacesMap: InterfacesMap, + unionsMap: UnionsMap, ): string { return type.fields - .map(field => renderInputArgInterface(field, modelMap)) + .map(field => + renderInputArgInterface(field, modelMap, interfacesMap, unionsMap), + ) .join(os.EOL) } function renderInputArgInterface( field: GraphQLTypeField, modelMap: ModelMap, + interfacesMap: InterfacesMap, + unionsMap: UnionsMap, ): string { if (field.arguments.length === 0) { return '' @@ -222,7 +396,14 @@ function renderInputArgInterface( return ` export interface Args${upperFirst(field.name)} { ${field.arguments - .map(arg => printFieldLikeType(arg as GraphQLTypeField, modelMap)) + .map(arg => + printFieldLikeType( + arg as GraphQLTypeField, + modelMap, + interfacesMap, + unionsMap, + ), + ) .join(os.EOL)} } ` @@ -231,11 +412,20 @@ function renderInputArgInterface( function renderResolverFunctionInterfaces( type: GraphQLTypeObject, modelMap: ModelMap, + interfacesMap: InterfacesMap, + unionsMap: UnionsMap, context?: ContextDefinition, ): string { return type.fields .map(field => - renderResolverFunctionInterface(field, type, modelMap, context), + renderResolverFunctionInterface( + field, + type, + modelMap, + interfacesMap, + unionsMap, + context, + ), ) .join(os.EOL) } @@ -244,6 +434,8 @@ function renderResolverFunctionInterface( field: GraphQLTypeField, type: GraphQLTypeObject, modelMap: ModelMap, + interfacesMap: InterfacesMap, + unionsMap: UnionsMap, context?: ContextDefinition, ): string { const resolverName = `${upperFirst(field.name)}Resolver` @@ -258,7 +450,13 @@ function renderResolverFunctionInterface( ) ` - const returnType = printFieldLikeType(field, modelMap, { isReturn: true }) + const returnType = printFieldLikeType( + field, + modelMap, + interfacesMap, + unionsMap, + { isReturn: true }, + ) if (type.name === 'Subscription') { return ` @@ -275,30 +473,61 @@ function renderResolverFunctionInterface( } function renderResolverTypeInterface( - type: GraphQLTypeObject, + type: GraphQLTypeObject | GraphQLInterfaceObject, modelMap: ModelMap, + interfacesMap: InterfacesMap, + unionsMap: UnionsMap, context?: ContextDefinition, + interfaceName: string = 'Type', ): string { return ` - export interface Type { + export interface ${interfaceName} { ${type.fields .map(field => - renderResolverTypeInterfaceFunction(field, type, modelMap, context), + renderResolverTypeInterfaceFunction( + field, + type, + modelMap, + interfacesMap, + unionsMap, + context, + ), ) .join(os.EOL)} + ${renderIsTypeOfFunctionInterface( + type, + modelMap, + interfacesMap, + unionsMap, + context, + )} } ` } function renderResolverTypeInterfaceFunction( field: GraphQLTypeField, - type: GraphQLTypeObject, + type: GraphQLTypeObject | GraphQLInterfaceObject, modelMap: ModelMap, + interfacesMap: InterfacesMap, + unionsMap: UnionsMap, context?: ContextDefinition, ): string { + let parent: string + + if (type.type.isInterface) { + const implementingTypes = interfacesMap[type.name] + + parent = implementingTypes + .map(implType => getModelName(implType, modelMap, 'undefined')) + .join(' | ') + } else { + parent = getModelName(type.type as any, modelMap, 'undefined') + } + const resolverDefinition = ` ( - parent: ${getModelName(type.type as any, modelMap, 'undefined')}, + parent: ${parent}, args: ${ field.arguments.length > 0 ? `Args${upperFirst(field.name)}` : '{}' }, @@ -306,7 +535,13 @@ function renderResolverTypeInterfaceFunction( info: GraphQLResolveInfo, ) ` - const returnType = printFieldLikeType(field, modelMap, { isReturn: true }) + const returnType = printFieldLikeType( + field, + modelMap, + interfacesMap, + unionsMap, + { isReturn: true }, + ) if (type.name === 'Subscription') { return ` @@ -325,12 +560,15 @@ function renderResolverTypeInterfaceFunction( } function renderResolvers(args: GenerateArgs): string { - return ` + return `\ export interface Resolvers { - ${args.types - .filter(type => type.type.isObject) - .map(type => `${type.name}: ${type.name}Resolvers.Type`) - .join(os.EOL)} + ${[ + ...args.types + .filter(obj => obj.type.isObject) + .map(type => `${type.name}: ${type.name}Resolvers.Type`), + ...args.interfaces.map(type => `${type.name}?: ${type.name}Resolvers.Type`), + ...args.unions.map(type => `${type.name}?: ${type.name}Resolvers.Type`), + ].join(os.EOL)} } ` } diff --git a/packages/graphqlgen/src/generators/ts-scaffolder.ts b/packages/graphqlgen/src/generators/ts-scaffolder.ts index f7b6621d..c11c325a 100644 --- a/packages/graphqlgen/src/generators/ts-scaffolder.ts +++ b/packages/graphqlgen/src/generators/ts-scaffolder.ts @@ -1,5 +1,9 @@ import { GenerateArgs, CodeFileLike } from '../types' -import { GraphQLTypeObject } from '../source-helper' +import { + GraphQLTypeObject, + GraphQLInterfaceObject, + GraphQLUnionObject, +} from '../source-helper' import { fieldsFromModelDefinition, shouldScaffoldFieldResolver, @@ -36,6 +40,23 @@ function renderResolvers( return { path: `${type.name}.ts`, force: false, code } } +function renderPolyResolvers( + type: GraphQLInterfaceObject | GraphQLUnionObject, +): CodeFileLike { + const code = `\ + // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. + // Please do not import this file directly but copy & paste to your application code. + + import { ${type.name}Resolvers } from '[TEMPLATE-INTERFACES-PATH]' + + export const ${type.name}: ${type.name}Resolvers.Type = { + __resolveType: (parent, ctx) => { + throw new Error('Resolver not implemented') + } + }` + return { path: `${type.name}.ts`, force: false, code } +} + function renderParentResolvers(type: GraphQLTypeObject): CodeFileLike { const code = `\ // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. @@ -96,6 +117,11 @@ export function generate(args: GenerateArgs): CodeFileLike[] { .filter(type => !isParentType(type.name)) .map(type => renderResolvers(type, args)) + files = files.concat( + args.interfaces.map(type => renderPolyResolvers(type)), + args.unions.map(type => renderPolyResolvers(type)), + ) + files = files.concat( args.types .filter(type => type.type.isObject) diff --git a/packages/graphqlgen/src/source-helper.ts b/packages/graphqlgen/src/source-helper.ts index e383f663..6efa0c11 100644 --- a/packages/graphqlgen/src/source-helper.ts +++ b/packages/graphqlgen/src/source-helper.ts @@ -23,6 +23,7 @@ export type GraphQLTypes = { types: GraphQLTypeObject[] unions: GraphQLUnionObject[] enums: GraphQLEnumObject[] + interfaces: GraphQLInterfaceObject[] } /** Converts typeDefs, e.g. the raw SDL string, into our `GraphQLTypes`. */ @@ -31,10 +32,11 @@ export function extractTypes(typeDefs: string): GraphQLTypes { const types = extractGraphQLTypes(schema) const unions = extractGraphQLUnions(schema) const enums = extractGraphQLEnums(schema) - return { types, enums, unions } + const interfaces = extractGraphQLInterfaces(schema, types) + return { types, enums, unions, interfaces } } -type GraphQLTypeDefinition = { +export type GraphQLTypeDefinition = { name: string isScalar: boolean isEnum: boolean @@ -67,6 +69,7 @@ export type GraphQLTypeObject = { name: string type: GraphQLTypeDefinition fields: GraphQLTypeField[] + implements: null | string[] } export type GraphQLEnumObject = { @@ -81,6 +84,13 @@ export type GraphQLUnionObject = { types: GraphQLTypeDefinition[] } +export type GraphQLInterfaceObject = { + name: string + type: GraphQLTypeDefinition + fields: GraphQLTypeField[] + implementors: GraphQLTypeDefinition[] +} + interface FinalType { isRequired: boolean isArray: boolean @@ -200,7 +210,7 @@ function extractTypeLike( function extractTypeFieldsFromObjectType( schema: GraphQLSchema, - node: GraphQLObjectType, + node: GraphQLObjectType | GraphQLInterfaceType, ) { const fields: GraphQLTypeField[] = [] Object.values(node.getFields()).forEach( @@ -240,7 +250,7 @@ function extractTypeFieldsFromInputType( return fields } -function extractGraphQLTypes(schema: GraphQLSchema) { +function extractGraphQLTypes(schema: GraphQLSchema): GraphQLTypeObject[] { const types: GraphQLTypeObject[] = [] Object.values(schema.getTypeMap()).forEach((node: GraphQLNamedType) => { // Ignore meta types like __Schema and __TypeKind @@ -260,6 +270,7 @@ function extractGraphQLTypes(schema: GraphQLSchema) { isInterface: false, }, fields: [], // extractTypeFields(schema, node), + implements: null, }) } else if (node instanceof GraphQLObjectType) { types.push({ @@ -274,6 +285,9 @@ function extractGraphQLTypes(schema: GraphQLSchema) { isInterface: false, }, fields: extractTypeFieldsFromObjectType(schema, node), + implements: node + .getInterfaces() + .map(interfaceType => interfaceType.name), }) } else if (node instanceof GraphQLInputObjectType) { types.push({ @@ -288,6 +302,7 @@ function extractGraphQLTypes(schema: GraphQLSchema) { isInterface: false, }, fields: extractTypeFieldsFromInputType(schema, node), + implements: null, }) } }) @@ -346,6 +361,45 @@ function extractGraphQLUnions(schema: GraphQLSchema) { return types } +function extractGraphQLInterfaces( + schema: GraphQLSchema, + types: GraphQLTypeObject[], +): GraphQLInterfaceObject[] { + const interfaceUsingTypes = types.filter(type => type.implements !== null) + + return Object.values(schema.getTypeMap()) + .filter(node => node instanceof GraphQLInterfaceType) + .reduce( + (interfaces, node) => { + node = node as GraphQLInterfaceType + + const implementorTypes = interfaceUsingTypes + .filter(type => type.implements!.includes(node.name)) + .map(type => type.type) + + if (implementorTypes.length) { + interfaces.push({ + name: node.name, + type: { + name: node.name, + isObject: false, + isInput: false, + isEnum: false, + isUnion: false, + isScalar: false, + isInterface: true, + }, + implementors: implementorTypes, + fields: extractTypeFieldsFromObjectType(schema, node), + }) + } + + return interfaces + }, + [] as GraphQLInterfaceObject[], + ) +} + const graphqlToTypescriptFlow: { [key: string]: string } = { String: 'string', Boolean: 'boolean', diff --git a/packages/graphqlgen/src/tests/fixtures/interface/schema.graphql b/packages/graphqlgen/src/tests/fixtures/interface/schema.graphql new file mode 100644 index 00000000..faf187e7 --- /dev/null +++ b/packages/graphqlgen/src/tests/fixtures/interface/schema.graphql @@ -0,0 +1,29 @@ +type Query { + media(first: Int): [Media] +} + +interface Unused { + foo: Int +} + +interface Media { + id: ID! + url: String! +} + +type Dimensions { + width: Int! + height: Int! +} + +type Image implements Media { + id: ID! + url: String! + dimensions: Dimensions! +} + +type Video implements Media { + id: ID! + url: String! + duration: Int! +} diff --git a/packages/graphqlgen/src/tests/fixtures/interface/types.ts b/packages/graphqlgen/src/tests/fixtures/interface/types.ts new file mode 100644 index 00000000..dfc6ce74 --- /dev/null +++ b/packages/graphqlgen/src/tests/fixtures/interface/types.ts @@ -0,0 +1,17 @@ +export interface Media { + id: string + url: string +} + +export interface Dimensions { + width: number + height: number +} + +export interface Image extends Media { + dimensions: Dimensions +} + +export interface Video extends Media { + dimensions: number +} diff --git a/packages/graphqlgen/src/tests/fixtures/union/schema.graphql b/packages/graphqlgen/src/tests/fixtures/union/schema.graphql index 5a675c5f..be7f67b4 100644 --- a/packages/graphqlgen/src/tests/fixtures/union/schema.graphql +++ b/packages/graphqlgen/src/tests/fixtures/union/schema.graphql @@ -1,7 +1,10 @@ +type Query { + users(first: Int): [UserType!]! +} + type User { id: ID! name: String! - type: UserType! } union UserType = Student | Professor diff --git a/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap b/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap index f9954db3..99fc57c4 100644 --- a/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap +++ b/packages/graphqlgen/src/tests/flow/__snapshots__/basic.test.ts.snap @@ -614,57 +614,30 @@ exports[`basic union 1`] = ` // Code generated by github.com/prisma/graphqlgen, DO NOT EDIT. import type { GraphQLResolveInfo } from \\"graphql\\"; -import type { User, Student, Professor } from \\"../../fixtures/union/flow-types\\"; +import type { Student, Professor, User } from \\"../../fixtures/union/flow-types\\"; type Context = any; -// Types for User -export const User_defaultResolvers = { - id: (parent: User) => parent.id, - name: (parent: User) => parent.name -}; - -export type User_Id_Resolver = ( - parent: User, - args: {}, - ctx: Context, - info: GraphQLResolveInfo -) => string | Promise; +// Types for Query +export const Query_defaultResolvers = {}; -export type User_Name_Resolver = ( - parent: User, - args: {}, - ctx: Context, - info: GraphQLResolveInfo -) => string | Promise; +export interface Query_Args_Users { + first?: number | null; +} -export type User_Type_Resolver = ( - parent: User, - args: {}, +export type Query_Users_Resolver = ( + parent: {}, + args: Query_Args_Users, ctx: Context, info: GraphQLResolveInfo -) => {} | Promise<{}>; +) => Array | Promise>; -export interface User_Resolvers { - id: ( - parent: User, - args: {}, - ctx: Context, - info: GraphQLResolveInfo - ) => string | Promise; - - name: ( - parent: User, - args: {}, - ctx: Context, - info: GraphQLResolveInfo - ) => string | Promise; - - type: ( - parent: User, - args: {}, +export interface Query_Resolvers { + users: ( + parent: {}, + args: Query_Args_Users, ctx: Context, info: GraphQLResolveInfo - ) => {} | Promise<{}>; + ) => Array | Promise>; } // Types for Student @@ -709,10 +682,48 @@ export interface Professor_Resolvers { ) => string | null | Promise; } +// Types for User +export const User_defaultResolvers = { + id: (parent: User) => parent.id, + name: (parent: User) => parent.name +}; + +export type User_Id_Resolver = ( + parent: User, + args: {}, + ctx: Context, + info: GraphQLResolveInfo +) => string | Promise; + +export type User_Name_Resolver = ( + parent: User, + args: {}, + ctx: Context, + info: GraphQLResolveInfo +) => string | Promise; + +export interface User_Resolvers { + id: ( + parent: User, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => string | Promise; + + name: ( + parent: User, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => string | Promise; +} + export interface Resolvers { - User: User_Resolvers; + Query: Query_Resolvers; Student: Student_Resolvers; Professor: Professor_Resolvers; + User: User_Resolvers; + UserType?: UserType_Resolvers; } " `; @@ -721,22 +732,6 @@ exports[`basic union 2`] = ` Array [ Object { "code": "/* @flow */ -import { User_defaultResolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; -import type { User_Resolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; - -export const User: User_Resolvers = { - ...User_defaultResolvers, - - type: (parent, args, ctx, info) => { - throw new Error(\\"Resolver not implemented\\"); - } -}; -", - "force": false, - "path": "User.js", - }, - Object { - "code": "/* @flow */ import { Student_defaultResolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; import type { Student_Resolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; @@ -759,6 +754,47 @@ export const Professor: Professor_Resolvers = { "force": false, "path": "Professor.js", }, + Object { + "code": "/* @flow */ +import { User_defaultResolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; +import type { User_Resolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; + +export const User: User_Resolvers = { + ...User_defaultResolvers +}; +", + "force": false, + "path": "User.js", + }, + Object { + "code": "// @flow +// This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. +// Please do not import this file directly but copy & paste to your application code. + +import { UserType_Resolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; + +export const UserType: UserType_Resolvers = { + __resolveType: (parent, ctx, info) => { + throw new Error(\\"Resolver not implemented\\"); + } +}; +", + "force": false, + "path": "UserType.ts", + }, + Object { + "code": "/* @flow */ +import type { Query_Resolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; + +export const Query: Query_Resolvers = { + users: (parent, args, ctx, info) => { + throw new Error(\\"Resolver not implemented\\"); + } +}; +", + "force": false, + "path": "Query.js", + }, Object { "code": "// @flow // This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. @@ -766,14 +802,16 @@ export const Professor: Professor_Resolvers = { import type { Resolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; -import { User } from \\"./User\\"; +import { Query } from \\"./Query\\"; import { Student } from \\"./Student\\"; import { Professor } from \\"./Professor\\"; +import { User } from \\"./User\\"; export const resolvers: Resolvers = { - User, + Query, Student, - Professor + Professor, + User }; ", "force": false, diff --git a/packages/graphqlgen/src/tests/typescript/__snapshots__/basic.test.ts.snap b/packages/graphqlgen/src/tests/typescript/__snapshots__/basic.test.ts.snap index 915e134c..94d078c1 100644 --- a/packages/graphqlgen/src/tests/typescript/__snapshots__/basic.test.ts.snap +++ b/packages/graphqlgen/src/tests/typescript/__snapshots__/basic.test.ts.snap @@ -336,6 +336,315 @@ export const resolvers: Resolvers = { ] `; +exports[`basic interface 1`] = ` +"// Code generated by github.com/prisma/graphqlgen, DO NOT EDIT. + +import { + GraphQLResolveInfo, + GraphQLTypeResolver, + GraphQLIsTypeOfFn +} from \\"graphql\\"; +import { Dimensions, Image, Video } from \\"../../../fixtures/interface/types\\"; +type Context = any; + +export namespace QueryResolvers { + export const defaultResolvers = {}; + + export interface ArgsMedia { + first?: number | null; + } + + export type MediaResolver = ( + parent: undefined, + args: ArgsMedia, + ctx: Context, + info: GraphQLResolveInfo + ) => + | Array + | null + | Promise | null>; + + export interface Type { + media: ( + parent: undefined, + args: ArgsMedia, + ctx: Context, + info: GraphQLResolveInfo + ) => + | Array + | null + | Promise | null>; + } +} + +export namespace DimensionsResolvers { + export const defaultResolvers = { + width: (parent: Dimensions) => parent.width, + height: (parent: Dimensions) => parent.height + }; + + export type WidthResolver = ( + parent: Dimensions, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => number | Promise; + + export type HeightResolver = ( + parent: Dimensions, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => number | Promise; + + export interface Type { + width: ( + parent: Dimensions, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => number | Promise; + + height: ( + parent: Dimensions, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => number | Promise; + } +} + +export namespace ImageResolvers { + export const defaultResolvers = { + dimensions: (parent: Image) => parent.dimensions + }; + + export type IdResolver = ( + parent: Image, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => string | Promise; + + export type UrlResolver = ( + parent: Image, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => string | Promise; + + export type DimensionsResolver = ( + parent: Image, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => Dimensions | Promise; + + export interface Type { + id: ( + parent: Image, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => string | Promise; + + url: ( + parent: Image, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => string | Promise; + + dimensions: ( + parent: Image, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => Dimensions | Promise; + + __isTypeOf?: GraphQLIsTypeOfFn; + } +} + +export namespace VideoResolvers { + export const defaultResolvers = {}; + + export type IdResolver = ( + parent: Video, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => string | Promise; + + export type UrlResolver = ( + parent: Video, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => string | Promise; + + export type DurationResolver = ( + parent: Video, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => number | Promise; + + export interface Type { + id: ( + parent: Video, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => string | Promise; + + url: ( + parent: Video, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => string | Promise; + + duration: ( + parent: Video, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => number | Promise; + + __isTypeOf?: GraphQLIsTypeOfFn; + } +} + +export namespace MediaResolvers { + export interface Type { + __resolveType: GraphQLTypeResolver; + } +} + +export interface Resolvers { + Query: QueryResolvers.Type; + Dimensions: DimensionsResolvers.Type; + Image: ImageResolvers.Type; + Video: VideoResolvers.Type; + Media?: MediaResolvers.Type; +} +" +`; + +exports[`basic interface 2`] = ` +Array [ + Object { + "code": "// This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. +// Please do not import this file directly but copy & paste to your application code. + +import { DimensionsResolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; + +export const Dimensions: DimensionsResolvers.Type = { + ...DimensionsResolvers.defaultResolvers +}; +", + "force": false, + "path": "Dimensions.ts", + }, + Object { + "code": "// This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. +// Please do not import this file directly but copy & paste to your application code. + +import { ImageResolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; + +export const Image: ImageResolvers.Type = { + ...ImageResolvers.defaultResolvers, + + id: (parent, args, ctx) => { + throw new Error(\\"Resolver not implemented\\"); + }, + url: (parent, args, ctx) => { + throw new Error(\\"Resolver not implemented\\"); + } +}; +", + "force": false, + "path": "Image.ts", + }, + Object { + "code": "// This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. +// Please do not import this file directly but copy & paste to your application code. + +import { VideoResolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; + +export const Video: VideoResolvers.Type = { + ...VideoResolvers.defaultResolvers, + + id: (parent, args, ctx) => { + throw new Error(\\"Resolver not implemented\\"); + }, + url: (parent, args, ctx) => { + throw new Error(\\"Resolver not implemented\\"); + }, + duration: (parent, args, ctx) => { + throw new Error(\\"Resolver not implemented\\"); + } +}; +", + "force": false, + "path": "Video.ts", + }, + Object { + "code": "// This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. +// Please do not import this file directly but copy & paste to your application code. + +import { MediaResolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; + +export const Media: MediaResolvers.Type = { + __resolveType: (parent, ctx) => { + throw new Error(\\"Resolver not implemented\\"); + } +}; +", + "force": false, + "path": "Media.ts", + }, + Object { + "code": "// This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. +// Please do not import this file directly but copy & paste to your application code. + +import { QueryResolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; + +export const Query: QueryResolvers.Type = { + ...QueryResolvers.defaultResolvers, + media: (parent, args, ctx) => { + throw new Error(\\"Resolver not implemented\\"); + } +}; +", + "force": false, + "path": "Query.ts", + }, + Object { + "code": "// This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. +// Please do not import this file directly but copy & paste to your application code. + +import { Resolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; + +import { Query } from \\"./Query\\"; +import { Dimensions } from \\"./Dimensions\\"; +import { Image } from \\"./Image\\"; +import { Video } from \\"./Video\\"; + +export const resolvers: Resolvers = { + Query, + Dimensions, + Image, + Video +}; +", + "force": false, + "path": "index.ts", + }, +] +`; + exports[`basic scalar 1`] = ` "// Code generated by github.com/prisma/graphqlgen, DO NOT EDIT. @@ -797,58 +1106,35 @@ export const resolvers: Resolvers = { exports[`basic union 1`] = ` "// Code generated by github.com/prisma/graphqlgen, DO NOT EDIT. -import { GraphQLResolveInfo } from \\"graphql\\"; -import { User, Student, Professor } from \\"../../fixtures/union/types\\"; +import { + GraphQLResolveInfo, + GraphQLTypeResolver, + GraphQLIsTypeOfFn +} from \\"graphql\\"; +import { Student, Professor, User } from \\"../../fixtures/union/types\\"; type Context = any; -export namespace UserResolvers { - export const defaultResolvers = { - id: (parent: User) => parent.id, - name: (parent: User) => parent.name - }; - - export type IdResolver = ( - parent: User, - args: {}, - ctx: Context, - info: GraphQLResolveInfo - ) => string | Promise; +export namespace QueryResolvers { + export const defaultResolvers = {}; - export type NameResolver = ( - parent: User, - args: {}, - ctx: Context, - info: GraphQLResolveInfo - ) => string | Promise; + export interface ArgsUsers { + first?: number | null; + } - export type TypeResolver = ( - parent: User, - args: {}, + export type UsersResolver = ( + parent: undefined, + args: ArgsUsers, ctx: Context, info: GraphQLResolveInfo - ) => {} | Promise<{}>; + ) => Array | Promise>; export interface Type { - id: ( - parent: User, - args: {}, - ctx: Context, - info: GraphQLResolveInfo - ) => string | Promise; - - name: ( - parent: User, - args: {}, - ctx: Context, - info: GraphQLResolveInfo - ) => string | Promise; - - type: ( - parent: User, - args: {}, + users: ( + parent: undefined, + args: ArgsUsers, ctx: Context, info: GraphQLResolveInfo - ) => {} | Promise<{}>; + ) => Array | Promise>; } } @@ -871,6 +1157,8 @@ export namespace StudentResolvers { ctx: Context, info: GraphQLResolveInfo ) => number | Promise; + + __isTypeOf?: GraphQLIsTypeOfFn; } } @@ -893,13 +1181,60 @@ export namespace ProfessorResolvers { ctx: Context, info: GraphQLResolveInfo ) => string | null | Promise; + + __isTypeOf?: GraphQLIsTypeOfFn; + } +} + +export namespace UserResolvers { + export const defaultResolvers = { + id: (parent: User) => parent.id, + name: (parent: User) => parent.name + }; + + export type IdResolver = ( + parent: User, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => string | Promise; + + export type NameResolver = ( + parent: User, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => string | Promise; + + export interface Type { + id: ( + parent: User, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => string | Promise; + + name: ( + parent: User, + args: {}, + ctx: Context, + info: GraphQLResolveInfo + ) => string | Promise; + } +} + +export namespace UserTypeResolvers { + export interface Type { + __resolveType?: GraphQLTypeResolver; } } export interface Resolvers { - User: UserResolvers.Type; + Query: QueryResolvers.Type; Student: StudentResolvers.Type; Professor: ProfessorResolvers.Type; + User: UserResolvers.Type; + UserType?: UserTypeResolvers.Type; } " `; @@ -910,14 +1245,36 @@ Array [ "code": "// This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. // Please do not import this file directly but copy & paste to your application code. +import { StudentResolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; + +export const Student: StudentResolvers.Type = { + ...StudentResolvers.defaultResolvers +}; +", + "force": false, + "path": "Student.ts", + }, + Object { + "code": "// This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. +// Please do not import this file directly but copy & paste to your application code. + +import { ProfessorResolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; + +export const Professor: ProfessorResolvers.Type = { + ...ProfessorResolvers.defaultResolvers +}; +", + "force": false, + "path": "Professor.ts", + }, + Object { + "code": "// This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. +// Please do not import this file directly but copy & paste to your application code. + import { UserResolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; export const User: UserResolvers.Type = { - ...UserResolvers.defaultResolvers, - - type: (parent, args, ctx) => { - throw new Error(\\"Resolver not implemented\\"); - } + ...UserResolvers.defaultResolvers }; ", "force": false, @@ -927,27 +1284,32 @@ export const User: UserResolvers.Type = { "code": "// This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. // Please do not import this file directly but copy & paste to your application code. -import { StudentResolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; +import { UserTypeResolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; -export const Student: StudentResolvers.Type = { - ...StudentResolvers.defaultResolvers +export const UserType: UserTypeResolvers.Type = { + __resolveType: (parent, ctx) => { + throw new Error(\\"Resolver not implemented\\"); + } }; ", "force": false, - "path": "Student.ts", + "path": "UserType.ts", }, Object { "code": "// This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. // Please do not import this file directly but copy & paste to your application code. -import { ProfessorResolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; +import { QueryResolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; -export const Professor: ProfessorResolvers.Type = { - ...ProfessorResolvers.defaultResolvers +export const Query: QueryResolvers.Type = { + ...QueryResolvers.defaultResolvers, + users: (parent, args, ctx) => { + throw new Error(\\"Resolver not implemented\\"); + } }; ", "force": false, - "path": "Professor.ts", + "path": "Query.ts", }, Object { "code": "// This resolver file was scaffolded by github.com/prisma/graphqlgen, DO NOT EDIT. @@ -955,14 +1317,16 @@ export const Professor: ProfessorResolvers.Type = { import { Resolvers } from \\"[TEMPLATE-INTERFACES-PATH]\\"; -import { User } from \\"./User\\"; +import { Query } from \\"./Query\\"; import { Student } from \\"./Student\\"; import { Professor } from \\"./Professor\\"; +import { User } from \\"./User\\"; export const resolvers: Resolvers = { - User, + Query, Student, - Professor + Professor, + User }; ", "force": false, diff --git a/packages/graphqlgen/src/tests/typescript/basic.test.ts b/packages/graphqlgen/src/tests/typescript/basic.test.ts index 22c7303a..18dc5f8b 100644 --- a/packages/graphqlgen/src/tests/typescript/basic.test.ts +++ b/packages/graphqlgen/src/tests/typescript/basic.test.ts @@ -149,3 +149,18 @@ test('override model', async () => { }, }) }) + +test('basic interface', async () => { + testGeneration({ + language, + schema: relative('../fixtures/interface/schema.graphql'), + models: { + files: [relative('../fixtures/interface/types.ts')], + }, + output: relative('./generated/interface/graphqlgen.ts'), + ['resolver-scaffolding']: { + output: relative('./tmp/interface/'), + layout: 'file-per-type', + }, + }) +}) diff --git a/packages/graphqlgen/src/types.ts b/packages/graphqlgen/src/types.ts index 2168a907..97863af2 100644 --- a/packages/graphqlgen/src/types.ts +++ b/packages/graphqlgen/src/types.ts @@ -3,10 +3,12 @@ import { GraphQLTypeObject, GraphQLEnumObject, GraphQLUnionObject, + GraphQLInterfaceObject, } from './source-helper' import { TypeDefinition } from './introspection/types' export interface GenerateArgs { + interfaces: GraphQLInterfaceObject[] types: GraphQLTypeObject[] enums: GraphQLEnumObject[] unions: GraphQLUnionObject[]