@envelop/extended-validation
Version:
Extended validation plugin adds support for writing GraphQL validation rules, that has access to all `execute` parameters, including variables.
72 lines (71 loc) • 3.32 kB
JavaScript
import { getNamedType, GraphQLError, GraphQLInputObjectType, GraphQLNonNull, isListType, } from 'graphql';
import { createGraphQLError, getArgumentValues } from '@graphql-tools/utils';
import { getDirectiveFromAstNode } from '../common.js';
export const ONE_OF_DIRECTIVE_SDL = /* GraphQL */ `
directive @oneOf on INPUT_OBJECT | FIELD_DEFINITION
`;
export const OneOfInputObjectsRule = (validationContext, executionArgs) => {
return {
Field: node => {
if (node.arguments?.length) {
const fieldType = validationContext.getFieldDef();
if (!fieldType) {
return;
}
const values = getArgumentValues(fieldType, node, executionArgs.variableValues || undefined);
const isOneOfFieldType = fieldType.extensions?.oneOf ||
(fieldType.astNode && getDirectiveFromAstNode(fieldType.astNode, 'oneOf'));
if (isOneOfFieldType && Object.keys(values).length !== 1) {
validationContext.reportError(new GraphQLError(`Exactly one key must be specified for input for field "${fieldType.type.toString()}.${node.name.value}"`, [node]));
}
for (const arg of node.arguments) {
const argType = fieldType.args.find(typeArg => typeArg.name === arg.name.value);
if (argType) {
traverseVariables(validationContext, arg, argType.type, values[arg.name.value]);
}
}
}
},
};
};
function getNonNullType(ttype) {
if (ttype instanceof GraphQLNonNull) {
return ttype.ofType;
}
return ttype;
}
function traverseVariables(validationContext, arg, graphqlType, currentValue) {
// if the current value is empty we don't need to traverse deeper
// if it shouldn't be empty, the "original" validation phase should complain.
if (currentValue == null) {
return;
}
const unwrappedType = getNonNullType(graphqlType);
if (isListType(unwrappedType)) {
if (!Array.isArray(currentValue)) {
// because of graphql type coercion a single object should be treated as an array of one object
currentValue = [currentValue];
}
currentValue.forEach(value => {
traverseVariables(validationContext, arg, unwrappedType.ofType, value);
});
return;
}
if (typeof currentValue !== 'object' || currentValue == null) {
// in case the value is not an object, the "original" validation phase should complain.
return;
}
const inputType = getNamedType(graphqlType);
const isOneOfInputType = inputType.extensions?.oneOf ||
(inputType.astNode && getDirectiveFromAstNode(inputType.astNode, 'oneOf'));
if (isOneOfInputType && Object.keys(currentValue).length !== 1) {
validationContext.reportError(createGraphQLError(`Exactly one key must be specified for input type "${inputType.name}"`, {
nodes: [arg],
}));
}
if (inputType instanceof GraphQLInputObjectType) {
for (const [name, fieldConfig] of Object.entries(inputType.getFields())) {
traverseVariables(validationContext, arg, fieldConfig.type, currentValue[name]);
}
}
}