import { concatAST, DocumentNode, extendSchema, GraphQLSchema, isObjectType, isTypeDefinitionNode, isTypeExtensionNode, Kind, TypeDefinitionNode, TypeExtensionNode, DirectiveDefinitionNode, SchemaDefinitionNode, SchemaExtensionNode, OperationTypeNode, GraphQLObjectType, GraphQLEnumType, isAbstractType, isScalarType, isEnumType, GraphQLEnumValueConfig, DirectiveNode, } from "graphql"; import { validateSDL } from "graphql/validation/validate"; import { isDocumentNode, isNode } from "../utilities/graphql"; import { GraphQLResolverMap } from "./resolverMap"; import { GraphQLSchemaValidationError } from "./GraphQLSchemaValidationError"; import { specifiedSDLRules } from "graphql/validation/specifiedRules"; // TODO(Node.js 10): When we deprecate Node.js 10, remove this and switch // to using `Array.prototype.flat`. When doing this, deleting the hand-rolled // types in `./packages/apollo-gateway/src/types/` that go with it. import flat from "core-js-pure/features/array/flat"; import { KnownTypeNamesRule, UniqueDirectivesPerLocationRule, ValidationRule, } from "graphql/validation"; import { mapValues } from "../utilities/mapValues"; export interface GraphQLSchemaModule { typeDefs: DocumentNode; resolvers?: GraphQLResolverMap; } const skippedSDLRules: ValidationRule[] = [ KnownTypeNamesRule, UniqueDirectivesPerLocationRule, ]; // BREAKING VERSION: Remove this when graphql-js 15 is minimum version. // Currently, this PossibleTypeExtensions rule is experimental and thus not // exposed directly from the rules module above. This may change in the future! // Additionally, it does not exist in prior graphql versions. Thus this try/catch. try { const PossibleTypeExtensions: typeof import("graphql/validation/rules/PossibleTypeExtensions").PossibleTypeExtensions = require("graphql/validation/rules/PossibleTypeExtensions").PossibleTypeExtensions; if (PossibleTypeExtensions) { skippedSDLRules.push(PossibleTypeExtensions); } } catch (e) { // No need to fail in this case. Instead, if this validation rule is missing, we will assume its not used // by the version of `graphql` that is available to us. } const sdlRules = specifiedSDLRules.filter( (rule) => !skippedSDLRules.includes(rule) ); export function modulesFromSDL( modulesOrSDL: (GraphQLSchemaModule | DocumentNode)[] | DocumentNode ): GraphQLSchemaModule[] { if (Array.isArray(modulesOrSDL)) { return modulesOrSDL.map((moduleOrSDL) => { if (isNode(moduleOrSDL) && isDocumentNode(moduleOrSDL)) { return { typeDefs: moduleOrSDL }; } else { return moduleOrSDL; } }); } else { return [{ typeDefs: modulesOrSDL }]; } } export function buildSchemaFromSDL( modulesOrSDL: (GraphQLSchemaModule | DocumentNode)[] | DocumentNode, schemaToExtend?: GraphQLSchema ): GraphQLSchema { const modules = modulesFromSDL(modulesOrSDL); const documentAST = concatAST(modules.map((module) => module.typeDefs)); const errors = validateSDL(documentAST, schemaToExtend, sdlRules); if (errors.length > 0) { throw new GraphQLSchemaValidationError(errors); } const definitionsMap: { [name: string]: TypeDefinitionNode[]; } = Object.create(null); const extensionsMap: { [name: string]: TypeExtensionNode[]; } = Object.create(null); const directiveDefinitions: DirectiveDefinitionNode[] = []; const schemaDefinitions: SchemaDefinitionNode[] = []; const schemaExtensions: SchemaExtensionNode[] = []; const schemaDirectives: DirectiveNode[] = []; for (const definition of documentAST.definitions) { if (isTypeDefinitionNode(definition)) { const typeName = definition.name.value; if (definitionsMap[typeName]) { definitionsMap[typeName].push(definition); } else { definitionsMap[typeName] = [definition]; } } else if (isTypeExtensionNode(definition)) { const typeName = definition.name.value; if (extensionsMap[typeName]) { extensionsMap[typeName].push(definition); } else { extensionsMap[typeName] = [definition]; } } else if (definition.kind === Kind.DIRECTIVE_DEFINITION) { directiveDefinitions.push(definition); } else if (definition.kind === Kind.SCHEMA_DEFINITION) { schemaDefinitions.push(definition); schemaDirectives.push( ...(definition.directives ? definition.directives : []) ); } else if (definition.kind === Kind.SCHEMA_EXTENSION) { schemaExtensions.push(definition); } } let schema = schemaToExtend ? schemaToExtend : new GraphQLSchema({ query: undefined, }); const missingTypeDefinitions: TypeDefinitionNode[] = []; for (const [extendedTypeName, extensions] of Object.entries(extensionsMap)) { if (!definitionsMap[extendedTypeName]) { const extension = extensions[0]; const kind = extension.kind; const definition = { kind: extKindToDefKind[kind], name: extension.name, } as TypeDefinitionNode; missingTypeDefinitions.push(definition); } } schema = extendSchema( schema, { kind: Kind.DOCUMENT, definitions: [ ...flat(Object.values(definitionsMap)), ...missingTypeDefinitions, ...directiveDefinitions, ], }, { assumeValidSDL: true, } ); schema = extendSchema( schema, { kind: Kind.DOCUMENT, definitions: flat(Object.values(extensionsMap)), }, { assumeValidSDL: true, } ); let operationTypeMap: { [operation in OperationTypeNode]?: string }; if (schemaDefinitions.length > 0 || schemaExtensions.length > 0) { operationTypeMap = {}; const operationTypes = flat( [...schemaDefinitions, ...schemaExtensions] .map((node) => node.operationTypes) .filter(isNotNullOrUndefined) ); for (const { operation, type } of operationTypes) { operationTypeMap[operation] = type.name.value; } } else { operationTypeMap = { query: "Query", mutation: "Mutation", subscription: "Subscription", }; } schema = new GraphQLSchema({ ...schema.toConfig(), ...mapValues(operationTypeMap, (typeName) => typeName ? (schema.getType(typeName) as GraphQLObjectType) : undefined ), astNode: { kind: Kind.SCHEMA_DEFINITION, directives: schemaDirectives, operationTypes: [], // satisfies typescript, will be ignored }, }); for (const module of modules) { if (!module.resolvers) continue; addResolversToSchema(schema, module.resolvers); } return schema; } const extKindToDefKind = { [Kind.SCALAR_TYPE_EXTENSION]: Kind.SCALAR_TYPE_DEFINITION, [Kind.OBJECT_TYPE_EXTENSION]: Kind.OBJECT_TYPE_DEFINITION, [Kind.INTERFACE_TYPE_EXTENSION]: Kind.INTERFACE_TYPE_DEFINITION, [Kind.UNION_TYPE_EXTENSION]: Kind.UNION_TYPE_DEFINITION, [Kind.ENUM_TYPE_EXTENSION]: Kind.ENUM_TYPE_DEFINITION, [Kind.INPUT_OBJECT_TYPE_EXTENSION]: Kind.INPUT_OBJECT_TYPE_DEFINITION, }; export function addResolversToSchema( schema: GraphQLSchema, resolvers: GraphQLResolverMap ) { for (const [typeName, fieldConfigs] of Object.entries(resolvers)) { const type = schema.getType(typeName); if (isAbstractType(type)) { for (const [fieldName, fieldConfig] of Object.entries(fieldConfigs)) { if (fieldName.startsWith("__")) { (type as any)[fieldName.substring(2)] = fieldConfig; } } } if (isScalarType(type)) { for (const fn in fieldConfigs) { (type as any)[fn] = (fieldConfigs as any)[fn]; } } if (isEnumType(type)) { const values = type.getValues(); const newValues: { [key: string]: GraphQLEnumValueConfig } = {}; values.forEach((value) => { let newValue = (fieldConfigs as any)[value.name]; if (newValue === undefined) { newValue = value.name; } newValues[value.name] = { value: newValue, deprecationReason: value.deprecationReason, description: value.description, astNode: value.astNode, extensions: undefined, }; }); // In place updating hack to get around pulling in the full // schema walking and immutable updating machinery from graphql-tools Object.assign( type, new GraphQLEnumType({ ...type.toConfig(), values: newValues, }) ); } if (!isObjectType(type)) continue; const fieldMap = type.getFields(); for (const [fieldName, fieldConfig] of Object.entries(fieldConfigs)) { if (fieldName.startsWith("__")) { (type as any)[fieldName.substring(2)] = fieldConfig; continue; } const field = fieldMap[fieldName]; if (!field) continue; if (typeof fieldConfig === "function") { field.resolve = fieldConfig; } else { field.resolve = fieldConfig.resolve; } } } } function isNotNullOrUndefined(value: T | null | undefined): value is T { return value !== null && typeof value !== "undefined"; }