blob: 5cb5241547b06966cdbbf9aaf316da008724cd7c [file] [log] [blame]
// @flow strict
import find from '../polyfills/find';
import arrayFrom from '../polyfills/arrayFrom';
import objectValues from '../polyfills/objectValues';
import { SYMBOL_TO_STRING_TAG } from '../polyfills/symbols';
import inspect from '../jsutils/inspect';
import toObjMap from '../jsutils/toObjMap';
import devAssert from '../jsutils/devAssert';
import instanceOf from '../jsutils/instanceOf';
import isObjectLike from '../jsutils/isObjectLike';
import {
type ObjMap,
type ReadOnlyObjMap,
type ReadOnlyObjMapLike,
} from '../jsutils/ObjMap';
import { type GraphQLError } from '../error/GraphQLError';
import {
type SchemaDefinitionNode,
type SchemaExtensionNode,
} from '../language/ast';
import { __Schema } from './introspection';
import {
GraphQLDirective,
isDirective,
specifiedDirectives,
} from './directives';
import {
type GraphQLType,
type GraphQLNamedType,
type GraphQLAbstractType,
type GraphQLObjectType,
type GraphQLInterfaceType,
isObjectType,
isInterfaceType,
isUnionType,
isInputObjectType,
getNamedType,
} from './definition';
/**
* 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);
}
export function assertSchema(schema: mixed): GraphQLSchema {
if (!isSchema(schema)) {
throw new Error(`Expected ${inspect(schema)} to be a GraphQL schema.`);
}
return schema;
}
/**
* 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: When the schema is constructed, by default only the types that are
* reachable by traversing the root types are included, other types must be
* explicitly referenced.
*
* Example:
*
* const characterInterface = new GraphQLInterfaceType({
* name: 'Character',
* ...
* });
*
* const humanType = new GraphQLObjectType({
* name: 'Human',
* interfaces: [characterInterface],
* ...
* });
*
* const droidType = new GraphQLObjectType({
* name: 'Droid',
* interfaces: [characterInterface],
* ...
* });
*
* const schema = new GraphQLSchema({
* query: new GraphQLObjectType({
* name: 'Query',
* fields: {
* hero: { type: characterInterface, ... },
* }
* }),
* ...
* // Since this schema references only the `Character` interface it's
* // necessary to explicitly list the types that implement it if
* // you want them to be included in the final schema.
* types: [humanType, droidType],
* })
*
* 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 {
description: ?string;
extensions: ?ReadOnlyObjMap<mixed>;
astNode: ?SchemaDefinitionNode;
extensionASTNodes: ?$ReadOnlyArray<SchemaExtensionNode>;
_queryType: ?GraphQLObjectType;
_mutationType: ?GraphQLObjectType;
_subscriptionType: ?GraphQLObjectType;
_directives: $ReadOnlyArray<GraphQLDirective>;
_typeMap: TypeMap;
_subTypeMap: ObjMap<ObjMap<boolean>>;
_implementationsMap: ObjMap<{|
objects: Array<GraphQLObjectType>,
interfaces: Array<GraphQLInterfaceType>,
|}>;
// Used as a cache for validateSchema().
__validationErrors: ?$ReadOnlyArray<GraphQLError>;
constructor(config: $ReadOnly<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.
this.__validationErrors = config.assumeValid === true ? [] : undefined;
// Check for common mistakes during construction to produce early errors.
devAssert(isObjectLike(config), 'Must provide configuration object.');
devAssert(
!config.types || Array.isArray(config.types),
`"types" must be Array if provided but got: ${inspect(config.types)}.`,
);
devAssert(
!config.directives || Array.isArray(config.directives),
'"directives" must be Array if provided but got: ' +
`${inspect(config.directives)}.`,
);
this.description = config.description;
this.extensions = config.extensions && toObjMap(config.extensions);
this.astNode = config.astNode;
this.extensionASTNodes = config.extensionASTNodes;
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;
// To preserve order of user-provided types, we add first to add them to
// the set of "collected" types, so `collectReferencedTypes` ignore them.
const allReferencedTypes: Set<GraphQLNamedType> = new Set(config.types);
if (config.types != null) {
for (const type of config.types) {
// When we ready to process this type, we remove it from "collected" types
// and then add it together with all dependent types in the correct position.
allReferencedTypes.delete(type);
collectReferencedTypes(type, allReferencedTypes);
}
}
if (this._queryType != null) {
collectReferencedTypes(this._queryType, allReferencedTypes);
}
if (this._mutationType != null) {
collectReferencedTypes(this._mutationType, allReferencedTypes);
}
if (this._subscriptionType != null) {
collectReferencedTypes(this._subscriptionType, allReferencedTypes);
}
for (const directive of this._directives) {
// Directives are not validated until validateSchema() is called.
if (isDirective(directive)) {
for (const arg of directive.args) {
collectReferencedTypes(arg.type, allReferencedTypes);
}
}
}
collectReferencedTypes(__Schema, allReferencedTypes);
// Storing the resulting map for reference by the schema.
this._typeMap = Object.create(null);
this._subTypeMap = Object.create(null);
// Keep track of all implementations by interface name.
this._implementationsMap = Object.create(null);
for (const namedType of arrayFrom(allReferencedTypes)) {
if (namedType == null) {
continue;
}
const typeName = namedType.name;
if (this._typeMap[typeName] !== undefined) {
throw new Error(
`Schema must contain uniquely named types but contains multiple types named "${typeName}".`,
);
}
this._typeMap[typeName] = namedType;
if (isInterfaceType(namedType)) {
// Store implementations by interface.
for (const iface of namedType.getInterfaces()) {
if (isInterfaceType(iface)) {
let implementations = this._implementationsMap[iface.name];
if (implementations === undefined) {
implementations = this._implementationsMap[iface.name] = {
objects: [],
interfaces: [],
};
}
implementations.interfaces.push(namedType);
}
}
} else if (isObjectType(namedType)) {
// Store implementations by objects.
for (const iface of namedType.getInterfaces()) {
if (isInterfaceType(iface)) {
let implementations = this._implementationsMap[iface.name];
if (implementations === undefined) {
implementations = this._implementationsMap[iface.name] = {
objects: [],
interfaces: [],
};
}
implementations.objects.push(namedType);
}
}
}
}
}
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> {
return isUnionType(abstractType)
? abstractType.getTypes()
: this.getImplementations(abstractType).objects;
}
getImplementations(
interfaceType: GraphQLInterfaceType,
): {|
objects: /* $ReadOnly */ Array<GraphQLObjectType>,
interfaces: /* $ReadOnly */ Array<GraphQLInterfaceType>,
|} {
const implementations = this._implementationsMap[interfaceType.name];
return implementations ?? { objects: [], interfaces: [] };
}
// @deprecated: use isSubType instead - will be removed in v16.
isPossibleType(
abstractType: GraphQLAbstractType,
possibleType: GraphQLObjectType,
): boolean {
return this.isSubType(abstractType, possibleType);
}
isSubType(
abstractType: GraphQLAbstractType,
maybeSubType: GraphQLObjectType | GraphQLInterfaceType,
): boolean {
let map = this._subTypeMap[abstractType.name];
if (map === undefined) {
map = Object.create(null);
if (isUnionType(abstractType)) {
for (const type of abstractType.getTypes()) {
map[type.name] = true;
}
} else {
const implementations = this.getImplementations(abstractType);
for (const type of implementations.objects) {
map[type.name] = true;
}
for (const type of implementations.interfaces) {
map[type.name] = true;
}
}
this._subTypeMap[abstractType.name] = map;
}
return map[maybeSubType.name] !== undefined;
}
getDirectives(): $ReadOnlyArray<GraphQLDirective> {
return this._directives;
}
getDirective(name: string): ?GraphQLDirective {
return find(this.getDirectives(), directive => directive.name === name);
}
toConfig(): GraphQLSchemaNormalizedConfig {
return {
description: this.description,
query: this.getQueryType(),
mutation: this.getMutationType(),
subscription: this.getSubscriptionType(),
types: objectValues(this.getTypeMap()),
directives: this.getDirectives().slice(),
extensions: this.extensions,
astNode: this.astNode,
extensionASTNodes: this.extensionASTNodes ?? [],
assumeValid: this.__validationErrors !== undefined,
};
}
// $FlowFixMe Flow doesn't support computed properties yet
get [SYMBOL_TO_STRING_TAG]() {
return 'GraphQLSchema';
}
}
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,
|};
export type GraphQLSchemaConfig = {|
description?: ?string,
query?: ?GraphQLObjectType,
mutation?: ?GraphQLObjectType,
subscription?: ?GraphQLObjectType,
types?: ?Array<GraphQLNamedType>,
directives?: ?Array<GraphQLDirective>,
extensions?: ?ReadOnlyObjMapLike<mixed>,
astNode?: ?SchemaDefinitionNode,
extensionASTNodes?: ?$ReadOnlyArray<SchemaExtensionNode>,
...GraphQLSchemaValidationOptions,
|};
/**
* @internal
*/
export type GraphQLSchemaNormalizedConfig = {|
...GraphQLSchemaConfig,
description: ?string,
types: Array<GraphQLNamedType>,
directives: Array<GraphQLDirective>,
extensions: ?ReadOnlyObjMap<mixed>,
extensionASTNodes: $ReadOnlyArray<SchemaExtensionNode>,
assumeValid: boolean,
|};
function collectReferencedTypes(
type: GraphQLType,
typeSet: Set<GraphQLNamedType>,
): Set<GraphQLNamedType> {
const namedType = getNamedType(type);
if (!typeSet.has(namedType)) {
typeSet.add(namedType);
if (isUnionType(namedType)) {
for (const memberType of namedType.getTypes()) {
collectReferencedTypes(memberType, typeSet);
}
} else if (isObjectType(namedType) || isInterfaceType(namedType)) {
for (const interfaceType of namedType.getInterfaces()) {
collectReferencedTypes(interfaceType, typeSet);
}
for (const field of objectValues(namedType.getFields())) {
collectReferencedTypes(field.type, typeSet);
for (const arg of field.args) {
collectReferencedTypes(arg.type, typeSet);
}
}
} else if (isInputObjectType(namedType)) {
for (const field of objectValues(namedType.getFields())) {
collectReferencedTypes(field.type, typeSet);
}
}
}
return typeSet;
}