/** * Copyright (c) 2015-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @flow strict */ import { isObjectType, isInterfaceType, isUnionType, isInputObjectType, isWrappingType, } from './definition'; import type { GraphQLType, GraphQLNamedType, GraphQLAbstractType, GraphQLObjectType, } from './definition'; import type { SchemaDefinitionNode } from '../language/ast'; import { GraphQLDirective, isDirective, specifiedDirectives, } from './directives'; import type { GraphQLError } from '../error/GraphQLError'; import { __Schema } from './introspection'; import find from '../jsutils/find'; import instanceOf from '../jsutils/instanceOf'; import invariant from '../jsutils/invariant'; import objectValues from '../jsutils/objectValues'; import type { ObjMap } from '../jsutils/ObjMap'; /** * Test if the given value is a GraphQL schema. */ declare function isSchema(schema: mixed): boolean %checks(schema instanceof GraphQLSchema); // eslint-disable-next-line no-redeclare export function isSchema(schema) { return instanceOf(schema, GraphQLSchema); } /** * Schema Definition * * A Schema is created by supplying the root types of each type of operation, * query and mutation (optional). A schema definition is then supplied to the * validator and executor. * * Example: * * const MyAppSchema = new GraphQLSchema({ * query: MyAppQueryRootType, * mutation: MyAppMutationRootType, * }) * * Note: If an array of `directives` are provided to GraphQLSchema, that will be * the exact list of directives represented and allowed. If `directives` is not * provided then a default set of the specified directives (e.g. @include and * @skip) will be used. If you wish to provide *additional* directives to these * specified directives, you must explicitly declare them. Example: * * const MyAppSchema = new GraphQLSchema({ * ... * directives: specifiedDirectives.concat([ myCustomDirective ]), * }) * */ export class GraphQLSchema { astNode: ?SchemaDefinitionNode; _queryType: ?GraphQLObjectType; _mutationType: ?GraphQLObjectType; _subscriptionType: ?GraphQLObjectType; _directives: $ReadOnlyArray<GraphQLDirective>; _typeMap: TypeMap; _implementations: ObjMap<Array<GraphQLObjectType>>; _possibleTypeMap: ?ObjMap<ObjMap<boolean>>; // Used as a cache for validateSchema(). __validationErrors: ?$ReadOnlyArray<GraphQLError>; // Referenced by validateSchema(). __allowedLegacyNames: ?$ReadOnlyArray<string>; constructor(config: GraphQLSchemaConfig): void { // If this schema was built from a source known to be valid, then it may be // marked with assumeValid to avoid an additional type system validation. if (config && config.assumeValid) { this.__validationErrors = []; } else { // Otherwise check for common mistakes during construction to produce // clear and early error messages. invariant( typeof config === 'object', 'Must provide configuration object.', ); invariant( !config.types || Array.isArray(config.types), `"types" must be Array if provided but got: ${String(config.types)}.`, ); invariant( !config.directives || Array.isArray(config.directives), '"directives" must be Array if provided but got: ' + `${String(config.directives)}.`, ); invariant( !config.allowedLegacyNames || Array.isArray(config.allowedLegacyNames), '"allowedLegacyNames" must be Array if provided but got: ' + `${String(config.allowedLegacyNames)}.`, ); } this.__allowedLegacyNames = config.allowedLegacyNames; this._queryType = config.query; this._mutationType = config.mutation; this._subscriptionType = config.subscription; // Provide specified directives (e.g. @include and @skip) by default. this._directives = config.directives || specifiedDirectives; this.astNode = config.astNode; // Build type map now to detect any errors within this schema. let initialTypes: Array<?GraphQLNamedType> = [ this.getQueryType(), this.getMutationType(), this.getSubscriptionType(), __Schema, ]; const types = config.types; if (types) { initialTypes = initialTypes.concat(types); } // Keep track of all types referenced within the schema. let typeMap: TypeMap = Object.create(null); // First by deeply visiting all initial types. typeMap = initialTypes.reduce(typeMapReducer, typeMap); // Then by deeply visiting all directive types. typeMap = this._directives.reduce(typeMapDirectiveReducer, typeMap); // Storing the resulting map for reference by the schema. this._typeMap = typeMap; // Keep track of all implementations by interface name. this._implementations = Object.create(null); Object.keys(this._typeMap).forEach(typeName => { const type = this._typeMap[typeName]; if (isObjectType(type)) { type.getInterfaces().forEach(iface => { if (isInterfaceType(iface)) { const impls = this._implementations[iface.name]; if (impls) { impls.push(type); } else { this._implementations[iface.name] = [type]; } } }); } }); } getQueryType(): ?GraphQLObjectType { return this._queryType; } getMutationType(): ?GraphQLObjectType { return this._mutationType; } getSubscriptionType(): ?GraphQLObjectType { return this._subscriptionType; } getTypeMap(): TypeMap { return this._typeMap; } getType(name: string): ?GraphQLNamedType { return this.getTypeMap()[name]; } getPossibleTypes( abstractType: GraphQLAbstractType, ): $ReadOnlyArray<GraphQLObjectType> { if (isUnionType(abstractType)) { return abstractType.getTypes(); } return this._implementations[abstractType.name]; } isPossibleType( abstractType: GraphQLAbstractType, possibleType: GraphQLObjectType, ): boolean { let possibleTypeMap = this._possibleTypeMap; if (!possibleTypeMap) { this._possibleTypeMap = possibleTypeMap = Object.create(null); } if (!possibleTypeMap[abstractType.name]) { const possibleTypes = this.getPossibleTypes(abstractType); invariant( Array.isArray(possibleTypes), `Could not find possible implementing types for ${abstractType.name} ` + 'in schema. Check that schema.types is defined and is an array of ' + 'all possible types in the schema.', ); possibleTypeMap[abstractType.name] = possibleTypes.reduce( (map, type) => ((map[type.name] = true), map), Object.create(null), ); } return Boolean(possibleTypeMap[abstractType.name][possibleType.name]); } getDirectives(): $ReadOnlyArray<GraphQLDirective> { return this._directives; } getDirective(name: string): ?GraphQLDirective { return find(this.getDirectives(), directive => directive.name === name); } } type TypeMap = ObjMap<GraphQLNamedType>; export type GraphQLSchemaValidationOptions = {| /** * When building a schema from a GraphQL service's introspection result, it * might be safe to assume the schema is valid. Set to true to assume the * produced schema is valid. * * Default: false */ assumeValid?: boolean, /** * If provided, the schema will consider fields or types with names included * in this list valid, even if they do not adhere to the specification's * schema validation rules. * * This option is provided to ease adoption and may be removed in a future * major release. */ allowedLegacyNames?: ?$ReadOnlyArray<string>, |}; export type GraphQLSchemaConfig = { query?: ?GraphQLObjectType, mutation?: ?GraphQLObjectType, subscription?: ?GraphQLObjectType, types?: ?Array<GraphQLNamedType>, directives?: ?Array<GraphQLDirective>, astNode?: ?SchemaDefinitionNode, ...GraphQLSchemaValidationOptions, }; function typeMapReducer(map: TypeMap, type: ?GraphQLType): TypeMap { if (!type) { return map; } if (isWrappingType(type)) { return typeMapReducer(map, type.ofType); } if (map[type.name]) { invariant( map[type.name] === type, 'Schema must contain unique named types but contains multiple ' + `types named "${type.name}".`, ); return map; } map[type.name] = type; let reducedMap = map; if (isUnionType(type)) { reducedMap = type.getTypes().reduce(typeMapReducer, reducedMap); } if (isObjectType(type)) { reducedMap = type.getInterfaces().reduce(typeMapReducer, reducedMap); } if (isObjectType(type) || isInterfaceType(type)) { objectValues(type.getFields()).forEach(field => { if (field.args) { const fieldArgTypes = field.args.map(arg => arg.type); reducedMap = fieldArgTypes.reduce(typeMapReducer, reducedMap); } reducedMap = typeMapReducer(reducedMap, field.type); }); } if (isInputObjectType(type)) { objectValues(type.getFields()).forEach(field => { reducedMap = typeMapReducer(reducedMap, field.type); }); } return reducedMap; } function typeMapDirectiveReducer( map: TypeMap, directive: ?GraphQLDirective, ): TypeMap { // Directives are not validated until validateSchema() is called. if (!isDirective(directive)) { return map; } return directive.args.reduce( (_map, arg) => typeMapReducer(_map, arg.type), map, ); }