@envelop/extended-validation
Version:
Extended validation plugin adds support for writing GraphQL validation rules, that has access to all `execute` parameters, including variables.
122 lines (121 loc) • 6.49 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.useExtendedValidation = void 0;
const graphql_1 = require("graphql");
const core_1 = require("@envelop/core");
const symbolExtendedValidationRules = Symbol('extendedValidationContext');
const useExtendedValidation = (options) => {
let schemaTypeInfo;
function getTypeInfo() {
return schemaTypeInfo;
}
return {
onSchemaChange({ schema }) {
schemaTypeInfo = new graphql_1.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, options.rejectOnErrors !== false),
onExecute: buildHandler('execute', getTypeInfo, options.onValidationFailed, options.rejectOnErrors !== false),
};
};
exports.useExtendedValidation = useExtendedValidation;
function buildHandler(name, getTypeInfo, onValidationFailed, rejectOnErrors = true) {
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 graphql_1.TypeInfo(args.schema);
const validationContext = new graphql_1.ValidationContext(args.schema, args.document, typeInfo, e => {
errors.push(e);
});
const visitor = (0, graphql_1.visitInParallel)(validationRulesContext.rules.map(rule => rule(validationContext, args)));
args.document = (0, graphql_1.visit)(args.document, (0, graphql_1.visitWithTypeInfo)(typeInfo, visitor));
if (errors.length > 0) {
if (rejectOnErrors) {
let result = {
data: null,
errors,
};
if (onValidationFailed) {
onValidationFailed({ args, result, setResult: newResult => (result = newResult) });
}
setResultAndStopExecution(result);
}
else {
// eslint-disable-next-line no-inner-declarations
function onResult({ result, setResult, }) {
if ((0, core_1.isAsyncIterable)(result)) {
// rejectOnErrors is false doesn't work with async iterables
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' ||
path[Number(pathItemIndex) + 1]
? []
: {};
if (Array.isArray(currentData)) {
let pathItemIndexInArray = Number(pathItemIndex) + 1;
if (path[pathItemIndexInArray] === '@') {
pathItemIndexInArray = Number(pathItemIndex) + 2;
}
currentData = currentData.map((c, i) => 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,
};
}
}
}
}
};
}
;