UNPKG

@envelop/extended-validation

Version:

Extended validation plugin adds support for writing GraphQL validation rules, that has access to all `execute` parameters, including variables.

68 lines (67 loc) 3.36 kB
import { TypeInfo, ValidationContext, visit, visitInParallel, visitWithTypeInfo, } from 'graphql'; const symbolExtendedValidationRules = Symbol('extendedValidationContext'); export const useExtendedValidation = (options) => { let schemaTypeInfo; function getTypeInfo() { return schemaTypeInfo; } return { onSchemaChange({ schema }) { schemaTypeInfo = new TypeInfo(schema); }, onContextBuilding({ context, extendContext }) { // We initialize the validationRules context in onContextBuilding as onExecute is already too late! let validationRulesContext = context[symbolExtendedValidationRules]; if (validationRulesContext === undefined) { validationRulesContext = { rules: [], didRun: false, }; extendContext({ ...context, [symbolExtendedValidationRules]: validationRulesContext, }); } validationRulesContext.rules.push(...options.rules); }, onSubscribe: buildHandler('subscribe', getTypeInfo, options.onValidationFailed), onExecute: buildHandler('execute', getTypeInfo, options.onValidationFailed), }; }; function buildHandler(name, getTypeInfo, onValidationFailed) { return function handler({ args, setResultAndStopExecution, }) { // We hook into onExecute/onSubscribe even though this is a validation pattern. The reasoning behind // it is that hooking right after validation and before execution has started is the // same as hooking into the validation step. The benefit of this approach is that // we may use execution context in the validation rules. const validationRulesContext = args.contextValue[symbolExtendedValidationRules]; if (validationRulesContext === undefined) { throw new Error('Plugin has not been properly set up. ' + `The 'contextFactory' function is not invoked and the result has not been passed to '${name}'.`); } // we only want to run the extended execution once. if (validationRulesContext.didRun === false) { validationRulesContext.didRun = true; if (validationRulesContext.rules.length !== 0) { const errors = []; // We replicate the default validation step manually before execution starts. const typeInfo = getTypeInfo() ?? new TypeInfo(args.schema); const validationContext = new ValidationContext(args.schema, args.document, typeInfo, e => { errors.push(e); }); const visitor = visitInParallel(validationRulesContext.rules.map(rule => rule(validationContext, args))); visit(args.document, visitWithTypeInfo(typeInfo, visitor)); if (errors.length > 0) { let result = { data: null, errors, }; if (onValidationFailed) { onValidationFailed({ args, result, setResult: newResult => (result = newResult) }); } setResultAndStopExecution(result); } } } }; }