From 9fb5c639765bc91ebd9c4e5011a2447435261e1e Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Fri, 21 Jun 2019 03:08:30 +0200 Subject: [PATCH 1/5] `buildSchema` can lookup for type field resolvers --- src/utilities/buildASTSchema.js | 37 ++++++++++++++++++++++++++++----- src/utilities/extendSchema.js | 16 +++++++++++--- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 3694e81cfe..c065c9a2f8 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -46,6 +46,7 @@ import { type GraphQLArgumentConfig, type GraphQLEnumValueConfig, type GraphQLInputFieldConfig, + type GraphQLFieldResolver, GraphQLScalarType, GraphQLObjectType, GraphQLInterfaceType, @@ -72,6 +73,10 @@ import { GraphQLSchema, } from '../type/schema'; +export type TypeFieldResolverMap = ObjMap< + ObjMap>, +>; + export type BuildSchemaOptions = { ...GraphQLSchemaValidationOptions, @@ -92,6 +97,13 @@ export type BuildSchemaOptions = { */ assumeValidSDL?: boolean, + /** + * Object map of object maps to resolver funtions. + * + * Default: undefined + */ + resolvers?: TypeFieldResolverMap, + ... }; @@ -243,7 +255,18 @@ export class ASTDefinitionBuilder { }); } - buildField(field: FieldDefinitionNode): GraphQLFieldConfig { + buildField( + field: FieldDefinitionNode, + typeName?: string, + ): GraphQLFieldConfig { + const resolve = + (typeName && + this._options && + this._options.resolvers && + this._options.resolvers[typeName] && + this._options.resolvers[typeName][field.name.value]) || + undefined; + return { // Note: While this could make assertions to get the correctly typed // value, that would throw immediately while type system validation @@ -251,6 +274,7 @@ export class ASTDefinitionBuilder { type: (this.getWrappedType(field.type): any), description: getDescription(field, this._options), args: keyByNameNode(field.arguments || [], arg => this.buildArg(arg)), + resolve, deprecationReason: getDeprecationReason(field), astNode: field, }; @@ -330,13 +354,15 @@ export class ASTDefinitionBuilder { ? () => interfaceNodes.map(ref => (this.getNamedType(ref): any)) : []; + const name = astNode.name.value; + const fields = fieldNodes && fieldNodes.length > 0 - ? () => keyByNameNode(fieldNodes, field => this.buildField(field)) + ? () => keyByNameNode(fieldNodes, field => this.buildField(field, name)) : Object.create(null); return new GraphQLObjectType({ - name: astNode.name.value, + name, description: getDescription(astNode, this._options), interfaces, fields, @@ -346,14 +372,15 @@ export class ASTDefinitionBuilder { _makeInterfaceDef(astNode: InterfaceTypeDefinitionNode) { const fieldNodes = astNode.fields; + const name = astNode.name.value; const fields = fieldNodes && fieldNodes.length > 0 - ? () => keyByNameNode(fieldNodes, field => this.buildField(field)) + ? () => keyByNameNode(fieldNodes, field => this.buildField(field, name)) : Object.create(null); return new GraphQLInterfaceType({ - name: astNode.name.value, + name, description: getDescription(astNode, this._options), fields, astNode, diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index 123a1c0ab0..d3a982c251 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -6,7 +6,10 @@ import inspect from '../jsutils/inspect'; import invariant from '../jsutils/invariant'; import mapValue from '../jsutils/mapValue'; import keyValMap from '../jsutils/keyValMap'; -import { ASTDefinitionBuilder } from './buildASTSchema'; +import { + type TypeFieldResolverMap, + ASTDefinitionBuilder, +} from './buildASTSchema'; import { assertValidSDLExtension } from '../validation/validate'; import { type GraphQLSchemaValidationOptions, @@ -70,6 +73,13 @@ type Options = {| * Default: false */ assumeValidSDL?: boolean, + + /** + * Object map of object maps to resolver funtions. + * + * Default: undefined + */ + resolvers?: TypeFieldResolverMap, |}; /** @@ -341,7 +351,7 @@ export function extendSchema( ...keyValMap( fieldNodes, node => node.name.value, - node => astBuilder.buildField(node), + node => astBuilder.buildField(node, config.name), ), }), extensionASTNodes: config.extensionASTNodes.concat(extensions), @@ -362,7 +372,7 @@ export function extendSchema( ...keyValMap( fieldNodes, node => node.name.value, - node => astBuilder.buildField(node), + node => astBuilder.buildField(node, config.name), ), }), extensionASTNodes: config.extensionASTNodes.concat(extensions), From a71b806d68ef9a174d6b8db551069614576d89de Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Fri, 21 Jun 2019 03:59:15 +0200 Subject: [PATCH 2/5] `buildSchema` lookup for enum value in resolvers --- src/utilities/buildASTSchema.js | 37 +++++++++++++++++++++------------ src/utilities/extendSchema.js | 2 +- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index c065c9a2f8..205756e0ce 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -74,7 +74,8 @@ import { } from '../type/schema'; export type TypeFieldResolverMap = ObjMap< - ObjMap>, + | ObjMap> /* type and interface */ + | ObjMap /* enum */, >; export type BuildSchemaOptions = { @@ -259,14 +260,6 @@ export class ASTDefinitionBuilder { field: FieldDefinitionNode, typeName?: string, ): GraphQLFieldConfig { - const resolve = - (typeName && - this._options && - this._options.resolvers && - this._options.resolvers[typeName] && - this._options.resolvers[typeName][field.name.value]) || - undefined; - return { // Note: While this could make assertions to get the correctly typed // value, that would throw immediately while type system validation @@ -274,7 +267,7 @@ export class ASTDefinitionBuilder { type: (this.getWrappedType(field.type): any), description: getDescription(field, this._options), args: keyByNameNode(field.arguments || [], arg => this.buildArg(arg)), - resolve, + resolve: this._lookupResolver(typeName, field.name.value), deprecationReason: getDeprecationReason(field), astNode: field, }; @@ -306,8 +299,12 @@ export class ASTDefinitionBuilder { }; } - buildEnumValue(value: EnumValueDefinitionNode): GraphQLEnumValueConfig { + buildEnumValue( + value: EnumValueDefinitionNode, + typeName?: string, + ): GraphQLEnumValueConfig { return { + value: this._lookupResolver(typeName, value.name.value), description: getDescription(value, this._options), deprecationReason: getDeprecationReason(value), astNode: value, @@ -389,11 +386,14 @@ export class ASTDefinitionBuilder { _makeEnumDef(astNode: EnumTypeDefinitionNode) { const valueNodes = astNode.values || []; + const name = astNode.name.value; return new GraphQLEnumType({ - name: astNode.name.value, + name, description: getDescription(astNode, this._options), - values: keyByNameNode(valueNodes, value => this.buildEnumValue(value)), + values: keyByNameNode(valueNodes, value => + this.buildEnumValue(value, name), + ), astNode, }); } @@ -437,6 +437,17 @@ export class ASTDefinitionBuilder { astNode: def, }); } + + _lookupResolver(typeName: ?string, key: string) { + return ( + (typeName && + this._options && + this._options.resolvers && + this._options.resolvers[typeName] && + this._options.resolvers[typeName][key]) || + undefined + ); + } } function keyByNameNode( diff --git a/src/utilities/extendSchema.js b/src/utilities/extendSchema.js index d3a982c251..9930bad124 100644 --- a/src/utilities/extendSchema.js +++ b/src/utilities/extendSchema.js @@ -314,7 +314,7 @@ export function extendSchema( ...keyValMap( valueNodes, value => value.name.value, - value => astBuilder.buildEnumValue(value), + value => astBuilder.buildEnumValue(value, config.name), ), }, extensionASTNodes: config.extensionASTNodes.concat(extensions), From 7a6e33086dbe7fdc915c13061f84c3124cb81ac7 Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Fri, 21 Jun 2019 05:44:40 +0200 Subject: [PATCH 3/5] Add tests for type and enum resolvers --- .../__tests__/buildASTSchema-test.js | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/utilities/__tests__/buildASTSchema-test.js b/src/utilities/__tests__/buildASTSchema-test.js index 65a0a014cb..1ea4f89605 100644 --- a/src/utilities/__tests__/buildASTSchema-test.js +++ b/src/utilities/__tests__/buildASTSchema-test.js @@ -74,6 +74,54 @@ describe('Schema Builder', () => { }); }); + it('can lookup type resolvers', () => { + const schema = buildSchema( + ` + type Query { + mult(a: Int!, b: Int!): Int! + } + `, + { + resolvers: { + Query: { + mult: (_: { ... }, { a, b }) => a * b, + }, + }, + }, + ); + + expect(graphqlSync(schema, '{ mult(a: 3, b: 4) }', null)).to.deep.equal({ + data: { mult: 12 }, + }); + }); + + it('can lookup enum values', () => { + const schema = buildSchema( + ` + enum Color { RED, GREEN, BLUE } + type Query { + colors: [Color!]! + } + `, + { + resolvers: { + Query: { + colors: () => [4, 2, 1], + }, + Color: { + RED: 1, + GREEN: 2, + BLUE: 4, + }, + }, + }, + ); + + expect(graphqlSync(schema, '{ colors }', null)).to.deep.equal({ + data: { colors: ['BLUE', 'GREEN', 'RED'] }, + }); + }); + it('Empty type', () => { const sdl = dedent` type EmptyType From 30125d4513f5697bf9eb4de64ef215908b910785 Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Fri, 21 Jun 2019 08:22:35 +0200 Subject: [PATCH 4/5] Update doccomments --- src/utilities/buildASTSchema.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index 205756e0ce..d0394420d8 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -112,17 +112,22 @@ export type BuildSchemaOptions = { * This takes the ast of a schema document produced by the parse function in * src/language/parser.js. * - * If no schema definition is provided, then it will look for types named Query - * and Mutation. + * If no schema definition is provided, then it will look for types named Query, + * Mutation and Subscription. * - * Given that AST it constructs a GraphQLSchema. The resulting schema - * has no resolve methods, so execution will use default resolvers. + * Given that AST it constructs a GraphQLSchema. The built schema will use + * resolve methods from `options.resolvers[typeName][fieldName]` if found. + * Otherwise it will use default resolvers. * * Accepts options as a second argument: * * - commentDescriptions: * Provide true to use preceding comments as the description. * + * - resolvers — map of named types + * - Object, Interface — field resolvers + * - Enum — External string → any internal value + * */ export function buildASTSchema( documentAST: DocumentNode, From a1b85202152b7c644abbfe19eed798638652b804 Mon Sep 17 00:00:00 2001 From: Pavel Lang Date: Fri, 21 Jun 2019 09:43:26 +0200 Subject: [PATCH 5/5] buildSchema custom scalars support --- src/type/definition.js | 10 +++-- .../__tests__/buildASTSchema-test.js | 32 ++++++++++++++ src/utilities/buildASTSchema.js | 43 +++++++++++++------ 3 files changed, 69 insertions(+), 16 deletions(-) diff --git a/src/type/definition.js b/src/type/definition.js index 5a899c4179..6b35c5fcb0 100644 --- a/src/type/definition.js +++ b/src/type/definition.js @@ -610,15 +610,19 @@ export type GraphQLScalarLiteralParser = ( variables: ?ObjMap, ) => ?TInternal; -export type GraphQLScalarTypeConfig = {| - name: string, - description?: ?string, +export type GraphQLScalarTypeConverters = {| // Serializes an internal value to include in a response. serialize?: GraphQLScalarSerializer, // Parses an externally provided value to use as an input. parseValue?: GraphQLScalarValueParser, // Parses an externally provided literal value to use as an input. parseLiteral?: GraphQLScalarLiteralParser, +|}; + +export type GraphQLScalarTypeConfig = {| + name: string, + ...GraphQLScalarTypeConverters, + description?: ?string, astNode?: ?ScalarTypeDefinitionNode, extensionASTNodes?: ?$ReadOnlyArray, |}; diff --git a/src/utilities/__tests__/buildASTSchema-test.js b/src/utilities/__tests__/buildASTSchema-test.js index 1ea4f89605..2b05da62b7 100644 --- a/src/utilities/__tests__/buildASTSchema-test.js +++ b/src/utilities/__tests__/buildASTSchema-test.js @@ -122,6 +122,38 @@ describe('Schema Builder', () => { }); }); + it('can define custom scalar converters', () => { + const schema = buildSchema( + ` + scalar Uppercase + scalar Lowercase + type Query { + hello: Uppercase + lower(str: Lowercase!): String + } + `, + { + resolvers: { + Uppercase: { + serialize: (value: string) => value.toUpperCase(), + }, + Lowercase: { + parseValue: (value: string) => value.toLowerCase(), + }, + Query: { + lower: (_, { str }: { str: string, ... }) => str, + }, + }, + }, + ); + + expect( + graphqlSync(schema, '{ hello lower(str: "World") }', { hello: 'hello' }), + ).to.deep.equal({ + data: { hello: 'HELLO', lower: 'world' }, + }); + }); + it('Empty type', () => { const sdl = dedent` type EmptyType diff --git a/src/utilities/buildASTSchema.js b/src/utilities/buildASTSchema.js index d0394420d8..ba5e08ce6d 100644 --- a/src/utilities/buildASTSchema.js +++ b/src/utilities/buildASTSchema.js @@ -47,6 +47,7 @@ import { type GraphQLEnumValueConfig, type GraphQLInputFieldConfig, type GraphQLFieldResolver, + type GraphQLScalarTypeConverters, GraphQLScalarType, GraphQLObjectType, GraphQLInterfaceType, @@ -73,10 +74,15 @@ import { GraphQLSchema, } from '../type/schema'; -export type TypeFieldResolverMap = ObjMap< - | ObjMap> /* type and interface */ - | ObjMap /* enum */, ->; +export type TypeFieldResolver = + /* scalars */ + | GraphQLScalarTypeConverters + /* type and interface */ + | ObjMap> + /* enum */ + | ObjMap; + +export type TypeFieldResolverMap = ObjMap; export type BuildSchemaOptions = { ...GraphQLSchemaValidationOptions, @@ -127,6 +133,7 @@ export type BuildSchemaOptions = { * - resolvers — map of named types * - Object, Interface — field resolvers * - Enum — External string → any internal value + * - Scalars — serialize, parseValue, parseLiteral * */ export function buildASTSchema( @@ -272,7 +279,7 @@ export class ASTDefinitionBuilder { type: (this.getWrappedType(field.type): any), description: getDescription(field, this._options), args: keyByNameNode(field.arguments || [], arg => this.buildArg(arg)), - resolve: this._lookupResolver(typeName, field.name.value), + resolve: this._lookupResolverField(typeName, field.name.value), deprecationReason: getDeprecationReason(field), astNode: field, }; @@ -309,7 +316,7 @@ export class ASTDefinitionBuilder { typeName?: string, ): GraphQLEnumValueConfig { return { - value: this._lookupResolver(typeName, value.name.value), + value: this._lookupResolverField(typeName, value.name.value), description: getDescription(value, this._options), deprecationReason: getDeprecationReason(value), astNode: value, @@ -423,9 +430,17 @@ export class ASTDefinitionBuilder { } _makeScalarDef(astNode: ScalarTypeDefinitionNode) { + const name = astNode.name.value; + const resolver = ((this._lookupResolver( + name, + ): any): GraphQLScalarTypeConverters); + return new GraphQLScalarType({ - name: astNode.name.value, + name, description: getDescription(astNode, this._options), + serialize: (resolver && resolver.serialize) || undefined, + parseValue: (resolver && resolver.parseValue) || undefined, + parseLiteral: (resolver && resolver.parseLiteral) || undefined, astNode, }); } @@ -443,16 +458,18 @@ export class ASTDefinitionBuilder { }); } - _lookupResolver(typeName: ?string, key: string) { + _lookupResolver(typeName: ?string) { + const opts = this._options; return ( - (typeName && - this._options && - this._options.resolvers && - this._options.resolvers[typeName] && - this._options.resolvers[typeName][key]) || + (typeName && opts && opts.resolvers && opts.resolvers[typeName]) || undefined ); } + + _lookupResolverField(typeName: ?string, key: string) { + const resolver = this._lookupResolver(typeName); + return (resolver && resolver[key]) || undefined; + } } function keyByNameNode(