diff --git a/.travis.yml b/.travis.yml index 65a50a948ea..06bc71e21e0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,5 +20,4 @@ script: sudo: false env: - - GRAPHQL_VERSION='^0.12' - GRAPHQL_VERSION='^0.13' diff --git a/CHANGELOG.md b/CHANGELOG.md index f3172284953..f9ee0b7a5bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Change log +* Use `getDescription` from `graphql-js` package [PR #672](https://github.com/apollographql/graphql-tools/pull/672) + ### v3.0.5 * Update apollo-link to 1.2.2 [#785](https://github.com/apollographql/graphql-tools/pull/785) @@ -18,7 +20,6 @@ [Issue #753](https://github.com/apollographql/graphql-tools/issues/753) [PR #806](https://github.com/apollographql/graphql-tools/pull/806) - ### v3.0.2 * Fixed duplicate fragments getting added during transform in `FilterToSchema` [#778](https://github.com/apollographql/graphql-tools/pull/778) diff --git a/package.json b/package.json index d18ede42ccb..3da6879eef9 100644 --- a/package.json +++ b/package.json @@ -55,11 +55,11 @@ "uuid": "^3.1.0" }, "peerDependencies": { - "graphql": "^0.12.0 || ^0.13.0" + "graphql": "^0.13.0" }, "devDependencies": { "@types/chai": "4.0.10", - "@types/graphql": "0.11.7", + "@types/graphql": "0.12.5", "@types/dateformat": "^1.0.1", "@types/mocha": "^2.2.44", "@types/node": "^8.0.47", diff --git a/src/generate/extractExtensionDefinitions.ts b/src/generate/extractExtensionDefinitions.ts index d3238e624ad..05fb394411c 100644 --- a/src/generate/extractExtensionDefinitions.ts +++ b/src/generate/extractExtensionDefinitions.ts @@ -1,16 +1,11 @@ import { DocumentNode, DefinitionNode } from 'graphql'; -// This was changed in graphql@0.12 -// See https://github.com/apollographql/graphql-tools/pull/541 -// TODO fix types https://github.com/apollographql/graphql-tools/issues/542 -const oldTypeExtensionDefinitionKind = 'TypeExtensionDefinition'; const newExtensionDefinitionKind = 'ObjectTypeExtension'; const interfaceExtensionDefinitionKind = 'InterfaceTypeExtension'; export default function extractExtensionDefinitions(ast: DocumentNode) { const extensionDefs = ast.definitions.filter( (def: DefinitionNode) => - def.kind === oldTypeExtensionDefinitionKind || (def.kind as any) === newExtensionDefinitionKind || (def.kind as any) === interfaceExtensionDefinitionKind, ); diff --git a/src/schemaGenerator.ts b/src/schemaGenerator.ts new file mode 100644 index 00000000000..029c1447497 --- /dev/null +++ b/src/schemaGenerator.ts @@ -0,0 +1,807 @@ +// Generates a schema for graphql-js given a shorthand schema + +// TODO: document each function clearly in the code: what arguments it accepts +// and what it outputs. + +// TODO: we should refactor this file, rename it to makeExecutableSchema, and move +// a bunch of utility functions into a separate utitlities folder, one file per function. + +import { + GraphQLEnumType, + DocumentNode, + parse, + print, + DefinitionNode, + defaultFieldResolver, + buildASTSchema, + extendSchema, + GraphQLScalarType, + getNamedType, + GraphQLObjectType, + GraphQLSchema, + GraphQLResolveInfo, + GraphQLField, + GraphQLFieldResolver, + GraphQLType, + GraphQLInterfaceType, + GraphQLFieldMap, + GraphQLUnionType, +} from 'graphql'; + +import { + IExecutableSchemaDefinition, + ILogger, + IResolvers, + ITypeDefinitions, + ITypedef, + IFieldIteratorFn, + IConnectors, + IConnector, + IConnectorCls, + IResolverValidationOptions, + IDirectiveResolvers, + UnitOrList, + GraphQLParseOptions, + IAddResolveFunctionsToSchemaOptions, +} from './Interfaces'; + +import { SchemaDirectiveVisitor } from './schemaVisitor'; +import { deprecated } from 'deprecated-decorator'; +import mergeDeep from './mergeDeep'; + +// @schemaDefinition: A GraphQL type schema in shorthand +// @resolvers: Definitions for resolvers to be merged with schema +class SchemaError extends Error { + public message: string; + + constructor(message: string) { + super(message); + this.message = message; + Error.captureStackTrace(this, this.constructor); + } +} + +// type definitions can be a string or an array of strings. +function _generateSchema( + typeDefinitions: ITypeDefinitions, + resolveFunctions: UnitOrList, + logger: ILogger, + // TODO: rename to allowUndefinedInResolve to be consistent + allowUndefinedInResolve: boolean, + resolverValidationOptions: IResolverValidationOptions, + parseOptions: GraphQLParseOptions, + inheritResolversFromInterfaces: boolean +) { + if (typeof resolverValidationOptions !== 'object') { + throw new SchemaError( + 'Expected `resolverValidationOptions` to be an object', + ); + } + if (!typeDefinitions) { + throw new SchemaError('Must provide typeDefs'); + } + if (!resolveFunctions) { + throw new SchemaError('Must provide resolvers'); + } + + const resolvers = Array.isArray(resolveFunctions) + ? resolveFunctions + .filter(resolverObj => typeof resolverObj === 'object') + .reduce(mergeDeep, {}) + : resolveFunctions; + + // TODO: check that typeDefinitions is either string or array of strings + + const schema = buildSchemaFromTypeDefinitions(typeDefinitions, parseOptions); + + addResolveFunctionsToSchema({ schema, resolvers, resolverValidationOptions, inheritResolversFromInterfaces }); + + assertResolveFunctionsPresent(schema, resolverValidationOptions); + + if (!allowUndefinedInResolve) { + addCatchUndefinedToSchema(schema); + } + + if (logger) { + addErrorLoggingToSchema(schema, logger); + } + + return schema; +} + +function makeExecutableSchema({ + typeDefs, + resolvers = {}, + connectors, + logger, + allowUndefinedInResolve = true, + resolverValidationOptions = {}, + directiveResolvers = null, + schemaDirectives = null, + parseOptions = {}, + inheritResolversFromInterfaces = false +}: IExecutableSchemaDefinition) { + const jsSchema = _generateSchema( + typeDefs, + resolvers, + logger, + allowUndefinedInResolve, + resolverValidationOptions, + parseOptions, + inheritResolversFromInterfaces + ); + if (typeof resolvers['__schema'] === 'function') { + // TODO a bit of a hack now, better rewrite generateSchema to attach it there. + // not doing that now, because I'd have to rewrite a lot of tests. + addSchemaLevelResolveFunction(jsSchema, resolvers[ + '__schema' + ] as GraphQLFieldResolver); + } + if (connectors) { + // connectors are optional, at least for now. That means you can just import them in the resolve + // function if you want. + attachConnectorsToContext(jsSchema, connectors); + } + + if (directiveResolvers) { + attachDirectiveResolvers(jsSchema, directiveResolvers); + } + + if (schemaDirectives) { + SchemaDirectiveVisitor.visitSchemaDirectives( + jsSchema, + schemaDirectives, + ); + } + + return jsSchema; +} + +function isDocumentNode( + typeDefinitions: ITypeDefinitions, +): typeDefinitions is DocumentNode { + return (typeDefinitions).kind !== undefined; +} + +function uniq(array: Array): Array { + return array.reduce((accumulator, currentValue) => { + return accumulator.indexOf(currentValue) === -1 + ? [...accumulator, currentValue] + : accumulator; + }, []); +} + +function concatenateTypeDefs( + typeDefinitionsAry: ITypedef[], + calledFunctionRefs = [] as any, +): string { + let resolvedTypeDefinitions: string[] = []; + typeDefinitionsAry.forEach((typeDef: ITypedef) => { + if (isDocumentNode(typeDef)) { + typeDef = print(typeDef); + } + + if (typeof typeDef === 'function') { + if (calledFunctionRefs.indexOf(typeDef) === -1) { + calledFunctionRefs.push(typeDef); + resolvedTypeDefinitions = resolvedTypeDefinitions.concat( + concatenateTypeDefs(typeDef(), calledFunctionRefs), + ); + } + } else if (typeof typeDef === 'string') { + resolvedTypeDefinitions.push(typeDef.trim()); + } else { + const type = typeof typeDef; + throw new SchemaError( + `typeDef array must contain only strings and functions, got ${type}`, + ); + } + }); + return uniq(resolvedTypeDefinitions.map(x => x.trim())).join('\n'); +} + +function buildSchemaFromTypeDefinitions( + typeDefinitions: ITypeDefinitions, + parseOptions?: GraphQLParseOptions, +): GraphQLSchema { + // TODO: accept only array here, otherwise interfaces get confusing. + let myDefinitions = typeDefinitions; + let astDocument: DocumentNode; + + if (isDocumentNode(typeDefinitions)) { + astDocument = typeDefinitions; + } else if (typeof myDefinitions !== 'string') { + if (!Array.isArray(myDefinitions)) { + const type = typeof myDefinitions; + throw new SchemaError( + `typeDefs must be a string, array or schema AST, got ${type}`, + ); + } + myDefinitions = concatenateTypeDefs(myDefinitions); + } + + if (typeof myDefinitions === 'string') { + astDocument = parse(myDefinitions, parseOptions); + } + + const backcompatOptions = { commentDescriptions: true }; + + // TODO fix types https://github.com/apollographql/graphql-tools/issues/542 + let schema: GraphQLSchema = (buildASTSchema as any)( + astDocument, + backcompatOptions, + ); + + const extensionsAst = extractExtensionDefinitions(astDocument); + if (extensionsAst.definitions.length > 0) { + // TODO fix types https://github.com/apollographql/graphql-tools/issues/542 + schema = (extendSchema as any)(schema, extensionsAst, backcompatOptions); + } + + return schema; +} + +const newExtensionDefinitionKind = 'ObjectTypeExtension'; +const interfaceExtensionDefinitionKind = 'InterfaceTypeExtension'; + +export function extractExtensionDefinitions(ast: DocumentNode) { + const extensionDefs = ast.definitions.filter( + (def: DefinitionNode) => + (def.kind as any) === newExtensionDefinitionKind || + (def.kind as any) === interfaceExtensionDefinitionKind, + ); + + return Object.assign({}, ast, { + definitions: extensionDefs, + }); +} + +function forEachField(schema: GraphQLSchema, fn: IFieldIteratorFn): void { + const typeMap = schema.getTypeMap(); + Object.keys(typeMap).forEach(typeName => { + const type = typeMap[typeName]; + + // TODO: maybe have an option to include these? + if ( + !getNamedType(type).name.startsWith('__') && + type instanceof GraphQLObjectType + ) { + const fields = type.getFields(); + Object.keys(fields).forEach(fieldName => { + const field = fields[fieldName]; + fn(field, typeName, fieldName); + }); + } + }); +} + +// takes a GraphQL-JS schema and an object of connectors, then attaches +// the connectors to the context by wrapping each query or mutation resolve +// function with a function that attaches connectors if they don't exist. +// attaches connectors only once to make sure they are singletons +const attachConnectorsToContext = deprecated( + { + version: '0.7.0', + url: 'https://github.com/apollostack/graphql-tools/issues/140', + }, + function(schema: GraphQLSchema, connectors: IConnectors): void { + if (!schema || !(schema instanceof GraphQLSchema)) { + throw new Error( + 'schema must be an instance of GraphQLSchema. ' + + 'This error could be caused by installing more than one version of GraphQL-JS', + ); + } + + if (typeof connectors !== 'object') { + const connectorType = typeof connectors; + throw new Error( + `Expected connectors to be of type object, got ${connectorType}`, + ); + } + if (Object.keys(connectors).length === 0) { + throw new Error('Expected connectors to not be an empty object'); + } + if (Array.isArray(connectors)) { + throw new Error('Expected connectors to be of type object, got Array'); + } + if (schema['_apolloConnectorsAttached']) { + throw new Error( + 'Connectors already attached to context, cannot attach more than once', + ); + } + schema['_apolloConnectorsAttached'] = true; + const attachconnectorFn: GraphQLFieldResolver = ( + root: any, + args: { [key: string]: any }, + ctx: any, + ) => { + if (typeof ctx !== 'object') { + // if in any way possible, we should throw an error when the attachconnectors + // function is called, not when a query is executed. + const contextType = typeof ctx; + throw new Error( + `Cannot attach connector because context is not an object: ${contextType}`, + ); + } + if (typeof ctx.connectors === 'undefined') { + ctx.connectors = {}; + } + Object.keys(connectors).forEach(connectorName => { + let connector: IConnector = connectors[connectorName]; + if (!!connector.prototype) { + ctx.connectors[connectorName] = new (connector)(ctx); + } else { + throw new Error(`Connector must be a function or an class`); + } + }); + return root; + }; + addSchemaLevelResolveFunction(schema, attachconnectorFn); + }, +); + +// wraps all resolve functions of query, mutation or subscription fields +// with the provided function to simulate a root schema level resolve funciton +function addSchemaLevelResolveFunction( + schema: GraphQLSchema, + fn: GraphQLFieldResolver, +): void { + // TODO test that schema is a schema, fn is a function + const rootTypes = [ + schema.getQueryType(), + schema.getMutationType(), + schema.getSubscriptionType(), + ].filter(x => !!x); + rootTypes.forEach(type => { + // XXX this should run at most once per request to simulate a true root resolver + // for graphql-js this is an approximation that works with queries but not mutations + const rootResolveFn = runAtMostOncePerRequest(fn); + const fields = type.getFields(); + Object.keys(fields).forEach(fieldName => { + // XXX if the type is a subscription, a same query AST will be ran multiple times so we + // deactivate here the runOnce if it's a subscription. This may not be optimal though... + if (type === schema.getSubscriptionType()) { + fields[fieldName].resolve = wrapResolver(fields[fieldName].resolve, fn); + } else { + fields[fieldName].resolve = wrapResolver( + fields[fieldName].resolve, + rootResolveFn, + ); + } + }); + }); +} + +function getFieldsForType(type: GraphQLType): GraphQLFieldMap { + if ( + type instanceof GraphQLObjectType || + type instanceof GraphQLInterfaceType + ) { + return type.getFields(); + } else { + return undefined; + } +} + +function addResolveFunctionsToSchema( + options: IAddResolveFunctionsToSchemaOptions|GraphQLSchema, + legacyInputResolvers?: IResolvers, + legacyInputValidationOptions?: IResolverValidationOptions) { + if (options instanceof GraphQLSchema) { + console.warn('The addResolveFunctionsToSchema function takes named options now; see IAddResolveFunctionsToSchemaOptions'); + options = { + schema: options, + resolvers: legacyInputResolvers, + resolverValidationOptions: legacyInputValidationOptions + }; + } + + const { + schema, + resolvers: inputResolvers, + resolverValidationOptions = {}, + inheritResolversFromInterfaces = false + } = options; + + const { + allowResolversNotInSchema = false, + requireResolversForResolveType, + } = resolverValidationOptions; + + const resolvers = inheritResolversFromInterfaces + ? extendResolversFromInterfaces(schema, inputResolvers) + : inputResolvers; + + Object.keys(resolvers).forEach(typeName => { + const type = schema.getType(typeName); + if (!type && typeName !== '__schema') { + if (allowResolversNotInSchema) { + return; + } + + throw new SchemaError( + `"${typeName}" defined in resolvers, but not in schema`, + ); + } + + Object.keys(resolvers[typeName]).forEach(fieldName => { + if (fieldName.startsWith('__')) { + // this is for isTypeOf and resolveType and all the other stuff. + type[fieldName.substring(2)] = resolvers[typeName][fieldName]; + return; + } + + if (type instanceof GraphQLScalarType) { + type[fieldName] = resolvers[typeName][fieldName]; + return; + } + + if (type instanceof GraphQLEnumType) { + if (!type.getValue(fieldName)) { + throw new SchemaError( + `${typeName}.${fieldName} was defined in resolvers, but enum is not in schema`, + ); + } + + type.getValue(fieldName)['value'] = + resolvers[typeName][fieldName]; + return; + } + + // object type + const fields = getFieldsForType(type); + if (!fields) { + if (allowResolversNotInSchema) { + return; + } + + throw new SchemaError( + `${typeName} was defined in resolvers, but it's not an object`, + ); + } + + if (!fields[fieldName]) { + if (allowResolversNotInSchema) { + return; + } + + throw new SchemaError( + `${typeName}.${fieldName} defined in resolvers, but not in schema`, + ); + } + const field = fields[fieldName]; + const fieldResolve = resolvers[typeName][fieldName]; + if (typeof fieldResolve === 'function') { + // for convenience. Allows shorter syntax in resolver definition file + setFieldProperties(field, { resolve: fieldResolve }); + } else { + if (typeof fieldResolve !== 'object') { + throw new SchemaError( + `Resolver ${typeName}.${fieldName} must be object or function`, + ); + } + setFieldProperties(field, fieldResolve); + } + }); + }); + + checkForResolveTypeResolver(schema, requireResolversForResolveType); +} + +function extendResolversFromInterfaces(schema: GraphQLSchema, resolvers: IResolvers) { + const typeNames = Object.keys({ + ...schema.getTypeMap(), + ...resolvers + }); + + const extendedResolvers: IResolvers = {}; + typeNames.forEach((typeName) => { + const typeResolvers = resolvers[typeName]; + const type = schema.getType(typeName); + if (type instanceof GraphQLObjectType) { + const interfaceResolvers = type.getInterfaces().map((iFace) => resolvers[iFace.name]); + extendedResolvers[typeName] = Object.assign({}, ...interfaceResolvers, typeResolvers); + } else { + if (typeResolvers) { + extendedResolvers[typeName] = typeResolvers; + } + } + }); + + return extendedResolvers; +} + +// If we have any union or interface types throw if no there is no resolveType or isTypeOf resolvers +function checkForResolveTypeResolver(schema: GraphQLSchema, requireResolversForResolveType?: boolean) { + Object.keys(schema.getTypeMap()) + .map(typeName => schema.getType(typeName)) + .forEach((type: GraphQLUnionType | GraphQLInterfaceType) => { + if (!(type instanceof GraphQLUnionType || type instanceof GraphQLInterfaceType)) { + return; + } + if (!type.resolveType) { + if (requireResolversForResolveType === false) { + return; + } + if (requireResolversForResolveType === true) { + throw new SchemaError(`Type "${type.name}" is missing a "resolveType" resolver`); + } + // tslint:disable-next-line:max-line-length + console.warn(`Type "${type.name}" is missing a "resolveType" resolver. Pass false into "resolverValidationOptions.requireResolversForResolveType" to disable this warning.`); + } + }); +} + +function setFieldProperties( + field: GraphQLField, + propertiesObj: Object, +) { + Object.keys(propertiesObj).forEach(propertyName => { + field[propertyName] = propertiesObj[propertyName]; + }); +} + +function assertResolveFunctionsPresent( + schema: GraphQLSchema, + resolverValidationOptions: IResolverValidationOptions = {}, +) { + const { + requireResolversForArgs = false, + requireResolversForNonScalar = false, + requireResolversForAllFields = false, + } = resolverValidationOptions; + + if ( + requireResolversForAllFields && + (requireResolversForArgs || requireResolversForNonScalar) + ) { + throw new TypeError( + 'requireResolversForAllFields takes precedence over the more specific assertions. ' + + 'Please configure either requireResolversForAllFields or requireResolversForArgs / ' + + 'requireResolversForNonScalar, but not a combination of them.', + ); + } + + forEachField(schema, (field, typeName, fieldName) => { + // requires a resolve function for *every* field. + if (requireResolversForAllFields) { + expectResolveFunction(field, typeName, fieldName); + } + + // requires a resolve function on every field that has arguments + if (requireResolversForArgs && field.args.length > 0) { + expectResolveFunction(field, typeName, fieldName); + } + + // requires a resolve function on every field that returns a non-scalar type + if ( + requireResolversForNonScalar && + !(getNamedType(field.type) instanceof GraphQLScalarType) + ) { + expectResolveFunction(field, typeName, fieldName); + } + }); +} + +function expectResolveFunction( + field: GraphQLField, + typeName: string, + fieldName: string, +) { + if (!field.resolve) { + console.warn( + // tslint:disable-next-line: max-line-length + `Resolve function missing for "${typeName}.${fieldName}". To disable this warning check https://github.com/apollostack/graphql-tools/issues/131`, + ); + return; + } + if (typeof field.resolve !== 'function') { + throw new SchemaError( + `Resolver "${typeName}.${fieldName}" must be a function`, + ); + } +} + +function addErrorLoggingToSchema(schema: GraphQLSchema, logger: ILogger): void { + if (!logger) { + throw new Error('Must provide a logger'); + } + if (typeof logger.log !== 'function') { + throw new Error('Logger.log must be a function'); + } + forEachField(schema, (field, typeName, fieldName) => { + const errorHint = `${typeName}.${fieldName}`; + field.resolve = decorateWithLogger(field.resolve, logger, errorHint); + }); +} + +// XXX badly named function. this doesn't really wrap, it just chains resolvers... +function wrapResolver( + innerResolver: GraphQLFieldResolver | undefined, + outerResolver: GraphQLFieldResolver, +): GraphQLFieldResolver { + return (obj, args, ctx, info) => { + return Promise.resolve(outerResolver(obj, args, ctx, info)).then(root => { + if (innerResolver) { + return innerResolver(root, args, ctx, info); + } + return defaultFieldResolver(root, args, ctx, info); + }); + }; +} + +function chainResolvers(resolvers: GraphQLFieldResolver[]) { + return ( + root: any, + args: { [argName: string]: any }, + ctx: any, + info: GraphQLResolveInfo, + ) => { + return resolvers.reduce((prev, curResolver) => { + if (curResolver) { + return curResolver(prev, args, ctx, info); + } + + return defaultFieldResolver(prev, args, ctx, info); + }, root); + }; +} + +/* + * fn: The function to decorate with the logger + * logger: an object instance of type Logger + * hint: an optional hint to add to the error's message + */ +function decorateWithLogger( + fn: GraphQLFieldResolver | undefined, + logger: ILogger, + hint: string, +): GraphQLFieldResolver { + if (typeof fn === 'undefined') { + fn = defaultFieldResolver; + } + + const logError = (e: Error) => { + // TODO: clone the error properly + const newE = new Error(); + newE.stack = e.stack; + /* istanbul ignore else: always get the hint from addErrorLoggingToSchema */ + if (hint) { + newE['originalMessage'] = e.message; + newE['message'] = `Error in resolver ${hint}\n${e.message}`; + } + logger.log(newE); + }; + + return (root, args, ctx, info) => { + try { + const result = fn(root, args, ctx, info); + // If the resolve function returns a Promise log any Promise rejects. + if ( + result && + typeof result.then === 'function' && + typeof result.catch === 'function' + ) { + result.catch((reason: Error | string) => { + // make sure that it's an error we're logging. + const error = reason instanceof Error ? reason : new Error(reason); + logError(error); + + // We don't want to leave an unhandled exception so pass on error. + return reason; + }); + } + return result; + } catch (e) { + logError(e); + // we want to pass on the error, just in case. + throw e; + } + }; +} + +function addCatchUndefinedToSchema(schema: GraphQLSchema): void { + forEachField(schema, (field, typeName, fieldName) => { + const errorHint = `${typeName}.${fieldName}`; + field.resolve = decorateToCatchUndefined(field.resolve, errorHint); + }); +} + +function decorateToCatchUndefined( + fn: GraphQLFieldResolver, + hint: string, +): GraphQLFieldResolver { + if (typeof fn === 'undefined') { + fn = defaultFieldResolver; + } + return (root, args, ctx, info) => { + const result = fn(root, args, ctx, info); + if (typeof result === 'undefined') { + throw new Error(`Resolve function for "${hint}" returned undefined`); + } + return result; + }; +} + +// XXX this function only works for resolvers +// XXX very hacky way to remember if the function +// already ran for this request. This will only work +// if people don't actually cache the operation. +// if they do cache the operation, they will have to +// manually remove the __runAtMostOnce before every request. +function runAtMostOncePerRequest( + fn: GraphQLFieldResolver, +): GraphQLFieldResolver { + let value: any; + const randomNumber = Math.random(); + return (root, args, ctx, info) => { + if (!info.operation['__runAtMostOnce']) { + info.operation['__runAtMostOnce'] = {}; + } + if (!info.operation['__runAtMostOnce'][randomNumber]) { + info.operation['__runAtMostOnce'][randomNumber] = true; + value = fn(root, args, ctx, info); + } + return value; + }; +} + +function attachDirectiveResolvers( + schema: GraphQLSchema, + directiveResolvers: IDirectiveResolvers, +) { + if (typeof directiveResolvers !== 'object') { + throw new Error( + `Expected directiveResolvers to be of type object, got ${typeof directiveResolvers}`, + ); + } + + if (Array.isArray(directiveResolvers)) { + throw new Error( + 'Expected directiveResolvers to be of type object, got Array', + ); + } + + const schemaDirectives = Object.create(null); + + Object.keys(directiveResolvers).forEach(directiveName => { + schemaDirectives[directiveName] = class extends SchemaDirectiveVisitor { + public visitFieldDefinition(field: GraphQLField) { + const resolver = directiveResolvers[directiveName]; + const originalResolver = field.resolve || defaultFieldResolver; + const directiveArgs = this.args; + field.resolve = (...args: any[]) => { + const [source, /* original args */, context, info] = args; + return resolver( + async () => originalResolver.apply(field, args), + source, + directiveArgs, + context, + info, + ); + }; + } + }; + }); + + SchemaDirectiveVisitor.visitSchemaDirectives( + schema, + schemaDirectives, + ); +} + +export { + makeExecutableSchema, + SchemaError, + forEachField, + chainResolvers, + addErrorLoggingToSchema, + addResolveFunctionsToSchema, + addCatchUndefinedToSchema, + assertResolveFunctionsPresent, + buildSchemaFromTypeDefinitions, + addSchemaLevelResolveFunction, + attachConnectorsToContext, + concatenateTypeDefs, + attachDirectiveResolvers, +}; diff --git a/src/stitching/typeFromAST.ts b/src/stitching/typeFromAST.ts index ebeb30bea9d..90a5833dae5 100644 --- a/src/stitching/typeFromAST.ts +++ b/src/stitching/typeFromAST.ts @@ -22,6 +22,7 @@ import { TypeNode, UnionTypeDefinitionNode, valueFromAST, + getDescription } from 'graphql'; import resolveFromParentType from './resolveFromParentTypename'; @@ -183,117 +184,3 @@ function resolveType( return getType(node.name.value, type); } } - -// Code below temporarily copied from graphql/graphql-js pending PR -// https://github.com/graphql/graphql-js/pull/1165 - -// MIT License - -// Copyright (c) 2015-present, Facebook, Inc. - -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: - -// The above copyright notice and this permission notice shall be included in all -// copies or substantial portions of the Software. - -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. -function getDescription(node: any, options: any): string { - if (node.description) { - return node.description.value; - } - if (options && options.commentDescriptions) { - const rawValue = getLeadingCommentBlock(node); - if (rawValue !== undefined) { - return blockStringValue('\n' + rawValue); - } - } -} - -function getLeadingCommentBlock(node: any): void | string { - const loc = node.loc; - if (!loc) { - return; - } - const comments = []; - let token = loc.startToken.prev; - while ( - token && - token.kind === 'Comment' && - token.next && - token.prev && - token.line + 1 === token.next.line && - token.line !== token.prev.line - ) { - const value = String(token.value); - comments.push(value); - token = token.prev; - } - return comments.reverse().join('\n'); -} - -/** - * Produces the value of a block string from its parsed raw value, similar to - * Coffeescript's block string, Python's docstring trim or Ruby's strip_heredoc. - * - * This implements the GraphQL spec's BlockStringValue() static algorithm. - */ -function blockStringValue(rawString: string): string { - // Expand a block string's raw value into independent lines. - const lines = rawString.split(/\r\n|[\n\r]/g); - - // Remove common indentation from all lines but first. - let commonIndent = null; - for (let i = 1; i < lines.length; i++) { - const line = lines[i]; - const indent = leadingWhitespace(line); - if ( - indent < line.length && - (commonIndent === null || indent < commonIndent) - ) { - commonIndent = indent; - if (commonIndent === 0) { - break; - } - } - } - - if (commonIndent) { - for (let i = 1; i < lines.length; i++) { - lines[i] = lines[i].slice(commonIndent); - } - } - - // Remove leading and trailing blank lines. - while (lines.length > 0 && isBlank(lines[0])) { - lines.shift(); - } - while (lines.length > 0 && isBlank(lines[lines.length - 1])) { - lines.pop(); - } - - // Return a string of the lines joined with U+000A. - return lines.join('\n'); -} - -function leadingWhitespace(str: string): number { - let i = 0; - while (i < str.length && (str[i] === ' ' || str[i] === '\t')) { - i++; - } - return i; -} - -function isBlank(str: string): boolean { - return leadingWhitespace(str) === str.length; -} diff --git a/src/test/testSchemaGenerator.ts b/src/test/testSchemaGenerator.ts index 6df296732ce..ac592412344 100644 --- a/src/test/testSchemaGenerator.ts +++ b/src/test/testSchemaGenerator.ts @@ -2393,66 +2393,6 @@ describe('can specify lexical parser options', () => { expect(schema.astNode.loc).to.equal(undefined); }); - if (['^0.11', '^0.12'].indexOf(process.env.GRAPHQL_VERSION) === -1) { - it("can specify 'allowLegacySDLEmptyFields' option", () => { - return expect(() => { - makeExecutableSchema({ - typeDefs: ` - type RootQuery { - } - schema { - query: RootQuery - } - `, - resolvers: {}, - parseOptions: { - allowLegacySDLEmptyFields: true, - }, - }); - }).to.not.throw(); - }); - - it("can specify 'allowLegacySDLImplementsInterfaces' option", () => { - const typeDefs = ` - interface A { - hello: String - } - interface B { - world: String - } - type RootQuery implements A, B { - hello: String - world: String - } - schema { - query: RootQuery - } - `; - - const resolvers = {}; - - expect(() => { - makeExecutableSchema({ - typeDefs, - resolvers, - parseOptions: { - allowLegacySDLImplementsInterfaces: true, - }, - }); - }).to.not.throw(); - - expect(() => { - makeExecutableSchema({ - typeDefs, - resolvers, - parseOptions: { - allowLegacySDLImplementsInterfaces: false, - }, - }); - }).to.throw('Syntax Error: Unexpected Name'); - }); - } - if (process.env.GRAPHQL_VERSION !== '^0.11') { it("can specify 'experimentalFragmentVariables' option", () => { const typeDefs = ` diff --git a/src/test/testingSchemas.ts b/src/test/testingSchemas.ts index eb1c5346c95..8032274a6d1 100644 --- a/src/test/testingSchemas.ts +++ b/src/test/testingSchemas.ts @@ -418,21 +418,6 @@ let SimpleProduct = `type SimpleProduct implements Product & Sellable { } `; -if (['^0.11', '^0.12'].indexOf(process.env.GRAPHQL_VERSION) !== -1) { - DownloadableProduct = ` - type DownloadableProduct implements Product, Downloadable { - id: ID! - url: String! - } - `; - - SimpleProduct = `type SimpleProduct implements Product, Sellable { - id: ID! - price: Int! - } - `; -} - const productTypeDefs = ` interface Product { id: ID!