@envelop/extended-validation
Version:
Extended validation plugin adds support for writing GraphQL validation rules, that has access to all `execute` parameters, including variables.
102 lines (100 loc) • 3.8 kB
JavaScript
import { TypeInfo, ValidationContext, visit, visitInParallel, visitWithTypeInfo } from "graphql";
import { isAsyncIterable } from "@envelop/core";
//#region src/plugin.ts
const symbolExtendedValidationRules = Symbol("extendedValidationContext");
const useExtendedValidation = (options) => {
let schemaTypeInfo;
function getTypeInfo() {
return schemaTypeInfo;
}
return {
onSchemaChange({ schema }) {
schemaTypeInfo = new TypeInfo(schema);
},
onContextBuilding({ context, extendContext }) {
let validationRulesContext = context[symbolExtendedValidationRules];
if (validationRulesContext === void 0) {
validationRulesContext = {
rules: [],
didRun: false
};
extendContext({
...context,
[symbolExtendedValidationRules]: validationRulesContext
});
}
validationRulesContext.rules.push(...options.rules);
},
onSubscribe: buildHandler("subscribe", getTypeInfo, options.onDocument, options.onValidationFailed, options.rejectOnErrors !== false),
onExecute: buildHandler("execute", getTypeInfo, options.onDocument, options.onValidationFailed, options.rejectOnErrors !== false)
};
};
function buildHandler(name, getTypeInfo, onDocument, onValidationFailed, rejectOnErrors = true) {
return function handler({ args, setResultAndStopExecution }) {
const validationRulesContext = args.contextValue[symbolExtendedValidationRules];
if (validationRulesContext === void 0) 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}'.`);
if (validationRulesContext.didRun === false) {
validationRulesContext.didRun = true;
if (validationRulesContext.rules.length !== 0) {
const errors = [];
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)));
args.document = visit(args.document, visitWithTypeInfo(typeInfo, visitor));
if (onDocument) args.document = onDocument(args.document);
if (errors.length > 0) if (rejectOnErrors) {
let result = {
data: null,
errors
};
if (onValidationFailed) onValidationFailed({
args,
result,
setResult: (newResult) => result = newResult
});
setResultAndStopExecution(result);
} else {
function onResult({ result, setResult }) {
if (isAsyncIterable(result)) {
setResult({
data: null,
errors
});
return;
}
const newResult = {
...result,
errors: [...result.errors || [], ...errors]
};
function visitPath(path, data = {}) {
let currentData = data ||= typeof path[0] === "number" ? [] : {};
for (const pathItemIndex in path.slice(0, -1)) {
const pathItem = path[pathItemIndex];
currentData = currentData[pathItem] ||= typeof path[Number(pathItemIndex) + 1] === "number" ? [] : {};
if (Array.isArray(currentData)) {
let pathItemIndexInArray = Number(pathItemIndex) + 1;
if (path[pathItemIndexInArray] === "@") pathItemIndexInArray = Number(pathItemIndex) + 2;
currentData = currentData.map((c) => visitPath(path.slice(pathItemIndexInArray), c));
}
}
currentData[path[path.length - 1]] = null;
return data;
}
errors.forEach((e) => {
if (e.path?.length) newResult.data = visitPath(e.path, newResult.data);
});
setResult(newResult);
}
return {
onSubscribeResult: onResult,
onExecuteDone: onResult
};
}
}
}
};
}
//#endregion
export { useExtendedValidation };