/** @category Schema */
import { devAssert } from '../jsutils/devAssert';
import { inspect } from '../jsutils/inspect';
import { instanceOf } from '../jsutils/instanceOf';
import { isObjectLike } from '../jsutils/isObjectLike';
import type { Maybe } from '../jsutils/Maybe';
import type { ObjMap } from '../jsutils/ObjMap';
import { toObjMap } from '../jsutils/toObjMap';
import type { GraphQLError } from '../error/GraphQLError';
import type {
SchemaDefinitionNode,
SchemaExtensionNode,
} from '../language/ast';
import { OperationTypeNode } from '../language/ast';
import type {
GraphQLAbstractType,
GraphQLInterfaceType,
GraphQLNamedType,
GraphQLObjectType,
GraphQLType,
} from './definition';
import {
getNamedType,
isInputObjectType,
isInterfaceType,
isObjectType,
isUnionType,
} from './definition';
import type { GraphQLDirective } from './directives';
import { isDirective, specifiedDirectives } from './directives';
import { __Schema } from './introspection';
/**
* Test if the given value is a GraphQL schema.
* @param schema - Value to inspect.
* @returns True when the value is a GraphQLSchema.
* @example
* ```ts
* import { buildSchema } from 'graphql/utilities';
* import { GraphQLString, isSchema } from 'graphql/type';
*
* const schema = buildSchema(`
* type Query {
* greeting: String
* }
* `);
*
* isSchema(schema); // => true
* isSchema(GraphQLString); // => false
* ```
*/
export function isSchema(schema: unknown): schema is GraphQLSchema {
return instanceOf(schema, GraphQLSchema);
}
/**
* Returns the value as a GraphQLSchema, or throws if it is not a schema.
* @param schema - GraphQL schema to use.
* @returns The value typed as a GraphQLSchema.
* @example
* ```ts
* import { buildSchema } from 'graphql/utilities';
* import { assertSchema, GraphQLString } from 'graphql/type';
*
* const schema = buildSchema(`
* type Query {
* greeting: String
* }
* `);
*
* assertSchema(schema); // => schema
* assertSchema(GraphQLString); // throws an error
* ```
*/
export function assertSchema(schema: unknown): GraphQLSchema {
if (!isSchema(schema)) {
throw new Error(`Expected ${inspect(schema)} to be a GraphQL schema.`);
}
return schema;
}
/**
* Custom extensions
* @remarks
* Use a unique identifier name for your extension, for example the name of
* your library or project. Do not use a shortened identifier as this increases
* the risk of conflicts. We recommend you add at most one extension field,
* an object which can contain all the values you need.
*/
export interface GraphQLSchemaExtensions {
[attributeName: string]: unknown;
}
/**
* 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
* ```ts
* const MyAppQueryRootType = new GraphQLObjectType({
* name: 'Query',
* fields: {
* greeting: { type: GraphQLString },
* },
* });
*
* const MyAppMutationRootType = new GraphQLObjectType({
* name: 'Mutation',
* fields: {
* setGreeting: { type: GraphQLString },
* },
* });
*
* const MyAppSchema = new GraphQLSchema({
* query: MyAppQueryRootType,
* mutation: MyAppMutationRootType,
* });
* ```
* @example
* 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.
*
* ```ts
* const characterInterface = new GraphQLInterfaceType({
* name: 'Character',
* fields: {
* name: { type: GraphQLString },
* },
* });
*
* const humanType = new GraphQLObjectType({
* name: 'Human',
* interfaces: [characterInterface],
* fields: {
* name: { type: GraphQLString },
* },
* });
*
* const droidType = new GraphQLObjectType({
* name: 'Droid',
* interfaces: [characterInterface],
* fields: {
* name: { type: GraphQLString },
* },
* });
*
* 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],
* });
* ```
* @example
* 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.
*
* ```ts
* const MyAppSchema = new GraphQLSchema({
* query: MyAppQueryRootType,
* directives: specifiedDirectives.concat([myCustomDirective]),
* });
* ```
*/
export class GraphQLSchema {
/** Human-readable description for this schema element, if provided. */
description: Maybe<string>;
/** Extension fields to include in the formatted result. */
extensions: Readonly<GraphQLSchemaExtensions>;
/** AST node from which this schema element was built, if available. */
astNode: Maybe<SchemaDefinitionNode>;
/** AST extension nodes applied to this schema element. */
extensionASTNodes: ReadonlyArray<SchemaExtensionNode>;
/**
* Cached schema validation errors, if validation has already run.
* @internal
*/
__validationErrors: Maybe<ReadonlyArray<GraphQLError>>;
private _queryType: Maybe<GraphQLObjectType>;
private _mutationType: Maybe<GraphQLObjectType>;
private _subscriptionType: Maybe<GraphQLObjectType>;
private _directives: ReadonlyArray<GraphQLDirective>;
private _typeMap: TypeMap;
private _subTypeMap: ObjMap<ObjMap<boolean>>;
private _implementationsMap: ObjMap<{
objects: Array<GraphQLObjectType>;
interfaces: Array<GraphQLInterfaceType>;
}>;
/**
* Creates a GraphQLSchema instance.
* @param config - Configuration describing this object.
* @example
* ```ts
* // Create a schema with the required query root.
* import {
* GraphQLObjectType,
* GraphQLSchema,
* GraphQLString,
* } from 'graphql/type';
*
* const Query = new GraphQLObjectType({
* name: 'Query',
* fields: {
* greeting: {
* type: GraphQLString,
* resolve: () => 'Hello',
* },
* },
* });
*
* const schema = new GraphQLSchema({
* description: 'The application schema.',
* query: Query,
* });
*
* schema.getQueryType(); // => Query
* schema.description; // => 'The application schema.'
* ```
* @example
* ```ts
* // This variant configures every schema option, including directives and extensions.
* import { DirectiveLocation, parse } from 'graphql/language';
* import {
* GraphQLBoolean,
* GraphQLDirective,
* GraphQLObjectType,
* GraphQLSchema,
* GraphQLString,
* } from 'graphql/type';
*
* const Query = new GraphQLObjectType({
* name: 'Query',
* fields: { greeting: { type: GraphQLString } },
* });
* const Mutation = new GraphQLObjectType({
* name: 'Mutation',
* fields: { setGreeting: { type: GraphQLString } },
* });
* const Subscription = new GraphQLObjectType({
* name: 'Subscription',
* fields: { greetingChanged: { type: GraphQLString } },
* });
* const AuditEvent = new GraphQLObjectType({
* name: 'AuditEvent',
* fields: { message: { type: GraphQLString } },
* });
* const authDirective = new GraphQLDirective({
* name: 'auth',
* locations: [DirectiveLocation.FIELD_DEFINITION],
* args: { required: { type: GraphQLBoolean } },
* });
* const schemaDocument = parse(`
* schema {
* query: Query
* mutation: Mutation
* subscription: Subscription
* }
*
* extend schema @auth
* `);
*
* const schema = new GraphQLSchema({
* description: 'Operations exposed by the application.',
* query: Query,
* mutation: Mutation,
* subscription: Subscription,
* types: [AuditEvent],
* directives: [authDirective],
* extensions: { owner: 'platform' },
* astNode: schemaDocument.definitions[0],
* extensionASTNodes: [ schemaDocument.definitions[1] ],
* assumeValid: true,
* });
*
* schema.getMutationType(); // => Mutation
* schema.getSubscriptionType(); // => Subscription
* schema.getType('AuditEvent'); // => AuditEvent
* schema.getDirective('auth'); // => authDirective
* schema.extensions; // => { owner: 'platform' }
* ```
*/
constructor(config: Readonly<GraphQLSchemaConfig>) {
// 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 = 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 allReferencedTypes) {
if (namedType == null) {
continue;
}
const typeName = namedType.name;
devAssert(
typeName,
'One of the provided types for building the Schema is missing a 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);
}
}
}
}
}
/**
* Returns the value used by `Object.prototype.toString`.
* @returns The built-in string tag for this object.
*/
get [Symbol.toStringTag]() {
return 'GraphQLSchema';
}
/**
* Returns the root object type for query operations.
* @returns The query root type, if this schema defines one.
* @example
* ```ts
* import { buildSchema } from 'graphql/utilities';
*
* const schema = buildSchema(`
* type Query {
* greeting: String
* }
* `);
*
* schema.getQueryType()?.name; // => 'Query'
* ```
*/
getQueryType(): Maybe<GraphQLObjectType> {
return this._queryType;
}
/**
* Returns the root object type for mutation operations.
* @returns The mutation root type, if this schema defines one.
* @example
* ```ts
* import { buildSchema } from 'graphql/utilities';
*
* const schema = buildSchema(`
* type Query {
* greeting: String
* }
*
* type Mutation {
* setGreeting(value: String!): String
* }
* `);
*
* schema.getMutationType()?.name; // => 'Mutation'
* ```
*/
getMutationType(): Maybe<GraphQLObjectType> {
return this._mutationType;
}
/**
* Returns the root object type for subscription operations.
* @returns The subscription root type, if this schema defines one.
* @example
* ```ts
* import { buildSchema } from 'graphql/utilities';
*
* const schema = buildSchema(`
* type Query {
* greeting: String
* }
*
* type Subscription {
* greetings: String
* }
* `);
*
* schema.getSubscriptionType()?.name; // => 'Subscription'
* ```
*/
getSubscriptionType(): Maybe<GraphQLObjectType> {
return this._subscriptionType;
}
/**
* Returns the root object type for the requested operation kind.
* @param operation - Operation kind to resolve.
* @returns The root object type for the operation kind, if this schema defines one.
* @example
* ```ts
* import { OperationTypeNode } from 'graphql/language';
* import { buildSchema } from 'graphql/utilities';
*
* const schema = buildSchema(`
* type Query {
* greeting: String
* }
*
* type Mutation {
* setGreeting(value: String!): String
* }
* `);
*
* schema.getRootType(OperationTypeNode.QUERY)?.name; // => 'Query'
* schema.getRootType(OperationTypeNode.MUTATION)?.name; // => 'Mutation'
* schema.getRootType(OperationTypeNode.SUBSCRIPTION); // => undefined
* ```
*/
getRootType(operation: OperationTypeNode): Maybe<GraphQLObjectType> {
switch (operation) {
case OperationTypeNode.QUERY:
return this.getQueryType();
case OperationTypeNode.MUTATION:
return this.getMutationType();
case OperationTypeNode.SUBSCRIPTION:
return this.getSubscriptionType();
}
}
/**
* Returns all named types known to this schema.
* @returns A map of schema types keyed by type name.
* @example
* ```ts
* import { buildSchema } from 'graphql/utilities';
*
* const schema = buildSchema(`
* type User {
* name: String
* }
*
* type Query {
* viewer: User
* }
* `);
*
* const typeMap = schema.getTypeMap();
*
* typeMap.User.name; // => 'User'
* typeMap.Query.name; // => 'Query'
* typeMap.String.name; // => 'String'
* ```
*/
getTypeMap(): TypeMap {
return this._typeMap;
}
/**
* Returns the named type with the provided name.
* @param name - The GraphQL name to look up.
* @returns The named schema type, if one exists.
* @example
* ```ts
* import { buildSchema } from 'graphql/utilities';
*
* const schema = buildSchema(`
* type User {
* name: String
* }
*
* type Query {
* viewer: User
* }
* `);
*
* schema.getType('User')?.toString(); // => 'User'
* schema.getType('Missing'); // => undefined
* ```
*/
getType(name: string): GraphQLNamedType | undefined {
return this.getTypeMap()[name];
}
/**
* Returns object types that may be returned for an abstract type.
* @param abstractType - Interface or union type to inspect.
* @returns Object types that may satisfy the abstract type.
* @example
* ```ts
* import { buildSchema } from 'graphql/utilities';
* import { assertInterfaceType, assertUnionType } from 'graphql/type';
*
* const schema = buildSchema(`
* interface Node {
* id: ID!
* }
*
* type User implements Node {
* id: ID!
* }
*
* type Organization implements Node {
* id: ID!
* }
*
* union SearchResult = User | Organization
*
* type Query {
* node: Node
* search: [SearchResult]
* }
* `);
*
* const Node = assertInterfaceType(schema.getType('Node'));
* const SearchResult = assertUnionType(schema.getType('SearchResult'));
*
* schema.getPossibleTypes(Node).map((type) => type.name); // => ['User', 'Organization']
* schema.getPossibleTypes(SearchResult).map((type) => type.name); // => ['User', 'Organization']
* ```
*/
getPossibleTypes(
abstractType: GraphQLAbstractType,
): ReadonlyArray<GraphQLObjectType> {
return isUnionType(abstractType)
? abstractType.getTypes()
: this.getImplementations(abstractType).objects;
}
/**
* Returns objects and interfaces that implement an interface type.
* @param interfaceType - Interface type to inspect.
* @returns Object and interface implementations of the interface.
* @example
* ```ts
* import { buildSchema } from 'graphql/utilities';
* import { assertInterfaceType } from 'graphql/type';
*
* const schema = buildSchema(`
* interface Resource {
* url: String!
* }
*
* interface Image implements Resource {
* url: String!
* width: Int
* }
*
* type Photo implements Resource & Image {
* url: String!
* width: Int
* }
*
* type Query {
* resource: Resource
* }
* `);
*
* const Resource = assertInterfaceType(schema.getType('Resource'));
* const implementations = schema.getImplementations(Resource);
*
* implementations.interfaces.map((type) => type.name); // => ['Image']
* implementations.objects.map((type) => type.name); // => ['Photo']
* ```
*/
getImplementations(interfaceType: GraphQLInterfaceType): {
objects: ReadonlyArray<GraphQLObjectType>;
interfaces: ReadonlyArray<GraphQLInterfaceType>;
} {
const implementations = this._implementationsMap[interfaceType.name];
return implementations ?? { objects: [], interfaces: [] };
}
/**
* Returns whether one type is a possible runtime subtype of an abstract type.
* @param abstractType - Interface or union type to inspect.
* @param maybeSubType - Object or interface type to test as a possible subtype.
* @returns True when the subtype may satisfy the abstract type.
* @example
* ```ts
* import { buildSchema } from 'graphql/utilities';
* import { assertInterfaceType, assertObjectType } from 'graphql/type';
*
* const schema = buildSchema(`
* interface Node {
* id: ID!
* }
*
* type User implements Node {
* id: ID!
* }
*
* type Review {
* body: String
* }
*
* type Query {
* node: Node
* review: Review
* }
* `);
*
* const Node = assertInterfaceType(schema.getType('Node'));
* const User = assertObjectType(schema.getType('User'));
* const Review = assertObjectType(schema.getType('Review'));
*
* schema.isSubType(Node, User); // => true
* schema.isSubType(Node, Review); // => false
* ```
*/
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;
}
/**
* Returns directives available in this schema.
* @returns Directives available in this schema.
* @example
* ```ts
* import { buildSchema } from 'graphql/utilities';
*
* const schema = buildSchema(`
* directive @upper on FIELD_DEFINITION
*
* type Query {
* greeting: String @upper
* }
* `);
*
* schema.getDirectives().map((directive) => directive.name); // => ['include', 'skip', 'deprecated', 'specifiedBy', 'oneOf', 'upper']
* ```
*/
getDirectives(): ReadonlyArray<GraphQLDirective> {
return this._directives;
}
/**
* Returns the current directive definition.
* @param name - The GraphQL name to look up.
* @returns The current directive definition, if known.
* @example
* ```ts
* import { buildSchema } from 'graphql/utilities';
*
* const schema = buildSchema(`
* directive @upper on FIELD_DEFINITION
*
* type Query {
* greeting: String @upper
* }
* `);
*
* schema.getDirective('upper')?.name; // => 'upper'
* schema.getDirective('missing'); // => undefined
* ```
*/
getDirective(name: string): Maybe<GraphQLDirective> {
return this.getDirectives().find((directive) => directive.name === name);
}
/**
* Returns a normalized configuration object for this object.
*
* The returned config preserves the original `assumeValid` flag so the schema
* can be recreated with the same validation behavior.
* @returns A configuration object that can be used to recreate this object.
* @example
* ```ts
* import { buildSchema } from 'graphql/utilities';
* import { GraphQLSchema } from 'graphql/type';
*
* const schema = buildSchema(`
* type Query {
* greeting: String
* }
* `);
*
* const config = schema.toConfig();
* const schemaCopy = new GraphQLSchema(config);
*
* config.query?.name; // => 'Query'
* schemaCopy.getQueryType()?.name; // => 'Query'
* ```
*/
toConfig(): GraphQLSchemaNormalizedConfig {
return {
description: this.description,
query: this.getQueryType(),
mutation: this.getMutationType(),
subscription: this.getSubscriptionType(),
types: Object.values(this.getTypeMap()),
directives: this.getDirectives(),
extensions: this.extensions,
astNode: this.astNode,
extensionASTNodes: this.extensionASTNodes,
assumeValid: this.__validationErrors !== undefined,
};
}
}
type TypeMap = ObjMap<GraphQLNamedType>;
/** @internal */
export interface 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;
}
/** Configuration used to construct a GraphQLSchema. */
export interface GraphQLSchemaConfig extends GraphQLSchemaValidationOptions {
/** Human-readable description for this schema element, if provided. */
description?: Maybe<string>;
/** Root object type for query operations. */
query?: Maybe<GraphQLObjectType>;
/** Root object type for mutation operations. */
mutation?: Maybe<GraphQLObjectType>;
/** Root object type for subscription operations. */
subscription?: Maybe<GraphQLObjectType>;
/** Object types that belong to this union type. */
types?: Maybe<ReadonlyArray<GraphQLNamedType>>;
/** Directives available in this schema or applied to this AST node. */
directives?: Maybe<ReadonlyArray<GraphQLDirective>>;
/** Extension fields to include in the formatted result. */
extensions?: Maybe<Readonly<GraphQLSchemaExtensions>>;
/** AST node from which this schema element was built, if available. */
astNode?: Maybe<SchemaDefinitionNode>;
/** AST extension nodes applied to this schema element. */
extensionASTNodes?: Maybe<ReadonlyArray<SchemaExtensionNode>>;
}
/** @internal */
export interface GraphQLSchemaNormalizedConfig extends GraphQLSchemaConfig {
description: Maybe<string>;
types: ReadonlyArray<GraphQLNamedType>;
directives: ReadonlyArray<GraphQLDirective>;
extensions: Readonly<GraphQLSchemaExtensions>;
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 Object.values(namedType.getFields())) {
collectReferencedTypes(field.type, typeSet);
for (const arg of field.args) {
collectReferencedTypes(arg.type, typeSet);
}
}
} else if (isInputObjectType(namedType)) {
for (const field of Object.values(namedType.getFields())) {
collectReferencedTypes(field.type, typeSet);
}
}
}
return typeSet;
}