Skip to content

buildSchema should be able to create fully featured schema #1987

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/type/definition.js
Original file line number Diff line number Diff line change
Expand Up @@ -610,15 +610,19 @@ export type GraphQLScalarLiteralParser<TInternal> = (
variables: ?ObjMap<mixed>,
) => ?TInternal;

export type GraphQLScalarTypeConfig<TInternal, TExternal> = {|
name: string,
description?: ?string,
export type GraphQLScalarTypeConverters<TInternal, TExternal> = {|
// Serializes an internal value to include in a response.
serialize?: GraphQLScalarSerializer<TExternal>,
// Parses an externally provided value to use as an input.
parseValue?: GraphQLScalarValueParser<TInternal>,
// Parses an externally provided literal value to use as an input.
parseLiteral?: GraphQLScalarLiteralParser<TInternal>,
|};

export type GraphQLScalarTypeConfig<TInternal, TExternal> = {|
name: string,
...GraphQLScalarTypeConverters<TInternal, TExternal>,
description?: ?string,
astNode?: ?ScalarTypeDefinitionNode,
extensionASTNodes?: ?$ReadOnlyArray<ScalarTypeExtensionNode>,
|};
Expand Down
80 changes: 80 additions & 0 deletions src/utilities/__tests__/buildASTSchema-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,86 @@ 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('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
Expand Down
86 changes: 73 additions & 13 deletions src/utilities/buildASTSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ import {
type GraphQLArgumentConfig,
type GraphQLEnumValueConfig,
type GraphQLInputFieldConfig,
type GraphQLFieldResolver,
type GraphQLScalarTypeConverters,
GraphQLScalarType,
GraphQLObjectType,
GraphQLInterfaceType,
Expand All @@ -72,6 +74,16 @@ import {
GraphQLSchema,
} from '../type/schema';

export type TypeFieldResolver =
/* scalars */
| GraphQLScalarTypeConverters<any, any>
/* type and interface */
| ObjMap<GraphQLFieldResolver<mixed, mixed, mixed>>
/* enum */
| ObjMap<any>;

export type TypeFieldResolverMap = ObjMap<TypeFieldResolver>;

export type BuildSchemaOptions = {
...GraphQLSchemaValidationOptions,

Expand All @@ -92,24 +104,37 @@ export type BuildSchemaOptions = {
*/
assumeValidSDL?: boolean,

/**
* Object map of object maps to resolver funtions.
*
* Default: undefined
*/
resolvers?: TypeFieldResolverMap,

...
};

/**
* 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
* - Scalars — serialize, parseValue, parseLiteral
*
*/
export function buildASTSchema(
documentAST: DocumentNode,
Expand Down Expand Up @@ -243,14 +268,18 @@ export class ASTDefinitionBuilder {
});
}

buildField(field: FieldDefinitionNode): GraphQLFieldConfig<mixed, mixed> {
buildField(
field: FieldDefinitionNode,
typeName?: string,
): GraphQLFieldConfig<mixed, mixed> {
return {
// Note: While this could make assertions to get the correctly typed
// value, that would throw immediately while type system validation
// with validateSchema() will produce more actionable results.
type: (this.getWrappedType(field.type): any),
description: getDescription(field, this._options),
args: keyByNameNode(field.arguments || [], arg => this.buildArg(arg)),
resolve: this._lookupResolverField(typeName, field.name.value),
deprecationReason: getDeprecationReason(field),
astNode: field,
};
Expand Down Expand Up @@ -282,8 +311,12 @@ export class ASTDefinitionBuilder {
};
}

buildEnumValue(value: EnumValueDefinitionNode): GraphQLEnumValueConfig {
buildEnumValue(
value: EnumValueDefinitionNode,
typeName?: string,
): GraphQLEnumValueConfig {
return {
value: this._lookupResolverField(typeName, value.name.value),
description: getDescription(value, this._options),
deprecationReason: getDeprecationReason(value),
astNode: value,
Expand Down Expand Up @@ -330,13 +363,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,
Expand All @@ -346,14 +381,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,
Expand All @@ -362,11 +398,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,
});
}
Expand All @@ -391,9 +430,17 @@ export class ASTDefinitionBuilder {
}

_makeScalarDef(astNode: ScalarTypeDefinitionNode) {
const name = astNode.name.value;
const resolver = ((this._lookupResolver(
name,
): any): GraphQLScalarTypeConverters<any, any>);

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,
});
}
Expand All @@ -410,6 +457,19 @@ export class ASTDefinitionBuilder {
astNode: def,
});
}

_lookupResolver(typeName: ?string) {
const opts = this._options;
return (
(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<T: { +name: NameNode, ... }, V>(
Expand Down
18 changes: 14 additions & 4 deletions src/utilities/extendSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -70,6 +73,13 @@ type Options = {|
* Default: false
*/
assumeValidSDL?: boolean,

/**
* Object map of object maps to resolver funtions.
*
* Default: undefined
*/
resolvers?: TypeFieldResolverMap,
|};

/**
Expand Down Expand Up @@ -304,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),
Expand Down Expand Up @@ -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),
Expand All @@ -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),
Expand Down