// @flow strict import { type ObjMap } from '../jsutils/ObjMap'; import { type GraphQLError } from '../error/GraphQLError'; import { Kind } from '../language/kinds'; import { type ASTVisitor, visit } from '../language/visitor'; import { type DocumentNode, type OperationDefinitionNode, type VariableNode, type SelectionSetNode, type FragmentSpreadNode, type FragmentDefinitionNode, } from '../language/ast'; import { type GraphQLSchema } from '../type/schema'; import { type GraphQLDirective } from '../type/directives'; import { type GraphQLInputType, type GraphQLOutputType, type GraphQLCompositeType, type GraphQLField, type GraphQLArgument, } from '../type/definition'; import { TypeInfo, visitWithTypeInfo } from '../utilities/TypeInfo'; type NodeWithSelectionSet = OperationDefinitionNode | FragmentDefinitionNode; type VariableUsage = {| +node: VariableNode, +type: ?GraphQLInputType, +defaultValue: ?mixed, |}; /** * An instance of this class is passed as the "this" context to all validators, * allowing access to commonly useful contextual information from within a * validation rule. */ export class ASTValidationContext { _ast: DocumentNode; _onError: (err: GraphQLError) => void; _fragments: ?ObjMap; _fragmentSpreads: Map>; _recursivelyReferencedFragments: Map< OperationDefinitionNode, $ReadOnlyArray, >; constructor(ast: DocumentNode, onError: (err: GraphQLError) => void): void { this._ast = ast; this._fragments = undefined; this._fragmentSpreads = new Map(); this._recursivelyReferencedFragments = new Map(); this._onError = onError; } reportError(error: GraphQLError): void { this._onError(error); } getDocument(): DocumentNode { return this._ast; } getFragment(name: string): ?FragmentDefinitionNode { let fragments = this._fragments; if (!fragments) { this._fragments = fragments = this.getDocument().definitions.reduce( (frags, statement) => { if (statement.kind === Kind.FRAGMENT_DEFINITION) { frags[statement.name.value] = statement; } return frags; }, Object.create(null), ); } return fragments[name]; } getFragmentSpreads( node: SelectionSetNode, ): $ReadOnlyArray { let spreads = this._fragmentSpreads.get(node); if (!spreads) { spreads = []; const setsToVisit: Array = [node]; while (setsToVisit.length !== 0) { const set = setsToVisit.pop(); for (const selection of set.selections) { if (selection.kind === Kind.FRAGMENT_SPREAD) { spreads.push(selection); } else if (selection.selectionSet) { setsToVisit.push(selection.selectionSet); } } } this._fragmentSpreads.set(node, spreads); } return spreads; } getRecursivelyReferencedFragments( operation: OperationDefinitionNode, ): $ReadOnlyArray { let fragments = this._recursivelyReferencedFragments.get(operation); if (!fragments) { fragments = []; const collectedNames = Object.create(null); const nodesToVisit: Array = [operation.selectionSet]; while (nodesToVisit.length !== 0) { const node = nodesToVisit.pop(); for (const spread of this.getFragmentSpreads(node)) { const fragName = spread.name.value; if (collectedNames[fragName] !== true) { collectedNames[fragName] = true; const fragment = this.getFragment(fragName); if (fragment) { fragments.push(fragment); nodesToVisit.push(fragment.selectionSet); } } } } this._recursivelyReferencedFragments.set(operation, fragments); } return fragments; } } export type ASTValidationRule = ASTValidationContext => ASTVisitor; export class SDLValidationContext extends ASTValidationContext { _schema: ?GraphQLSchema; constructor( ast: DocumentNode, schema: ?GraphQLSchema, onError: (err: GraphQLError) => void, ): void { super(ast, onError); this._schema = schema; } getSchema(): ?GraphQLSchema { return this._schema; } } export type SDLValidationRule = SDLValidationContext => ASTVisitor; export class ValidationContext extends ASTValidationContext { _schema: GraphQLSchema; _typeInfo: TypeInfo; _variableUsages: Map>; _recursiveVariableUsages: Map< OperationDefinitionNode, $ReadOnlyArray, >; constructor( schema: GraphQLSchema, ast: DocumentNode, typeInfo: TypeInfo, onError: (err: GraphQLError) => void, ): void { super(ast, onError); this._schema = schema; this._typeInfo = typeInfo; this._variableUsages = new Map(); this._recursiveVariableUsages = new Map(); } getSchema(): GraphQLSchema { return this._schema; } getVariableUsages(node: NodeWithSelectionSet): $ReadOnlyArray { let usages = this._variableUsages.get(node); if (!usages) { const newUsages = []; const typeInfo = new TypeInfo(this._schema); visit( node, visitWithTypeInfo(typeInfo, { VariableDefinition: () => false, Variable(variable) { newUsages.push({ node: variable, type: typeInfo.getInputType(), defaultValue: typeInfo.getDefaultValue(), }); }, }), ); usages = newUsages; this._variableUsages.set(node, usages); } return usages; } getRecursiveVariableUsages( operation: OperationDefinitionNode, ): $ReadOnlyArray { let usages = this._recursiveVariableUsages.get(operation); if (!usages) { usages = this.getVariableUsages(operation); for (const frag of this.getRecursivelyReferencedFragments(operation)) { usages = usages.concat(this.getVariableUsages(frag)); } this._recursiveVariableUsages.set(operation, usages); } return usages; } getType(): ?GraphQLOutputType { return this._typeInfo.getType(); } getParentType(): ?GraphQLCompositeType { return this._typeInfo.getParentType(); } getInputType(): ?GraphQLInputType { return this._typeInfo.getInputType(); } getParentInputType(): ?GraphQLInputType { return this._typeInfo.getParentInputType(); } getFieldDef(): ?GraphQLField { return this._typeInfo.getFieldDef(); } getDirective(): ?GraphQLDirective { return this._typeInfo.getDirective(); } getArgument(): ?GraphQLArgument { return this._typeInfo.getArgument(); } } export type ValidationRule = ValidationContext => ASTVisitor;