import type { Maybe } from '../../jsutils/Maybe.ts';
import { GraphQLError } from '../../error/GraphQLError.ts';
import type { ValueNode, VariableDefinitionNode } from '../../language/ast.ts';
import { Kind } from '../../language/kinds.ts';
import type { ASTVisitor } from '../../language/visitor.ts';
import type { GraphQLType } from '../../type/definition.ts';
import {
isInputObjectType,
isNonNullType,
isNullableType,
} from '../../type/definition.ts';
import type { GraphQLSchema } from '../../type/schema.ts';
import { isTypeSubTypeOf } from '../../utilities/typeComparators.ts';
import { typeFromAST } from '../../utilities/typeFromAST.ts';
import type { ValidationContext } from '../ValidationContext.ts';
export function VariablesInAllowedPositionRule(
context: ValidationContext,
): ASTVisitor {
let varDefMap: Map<string, VariableDefinitionNode>;
return {
OperationDefinition: {
enter(operation) {
varDefMap = new Map();
if (operation.variableDefinitions) {
for (const varDef of operation.variableDefinitions) {
varDefMap.set(varDef.variable.name.value, varDef);
}
}
},
leave(operation) {
const usages = context.getRecursiveVariableUsages(operation);
for (const {
node,
type,
parentType,
defaultValue,
fragmentVariableDefinition,
} of usages) {
const varName = node.name.value;
let varDef = fragmentVariableDefinition;
varDef ??= varDefMap.get(varName);
if (varDef && type) {
const schema = context.getSchema();
const varType = typeFromAST(schema, varDef.type);
if (
varType &&
!allowedVariableUsage(
schema,
varType,
varDef.defaultValue,
type,
defaultValue,
)
) {
context.reportError(
new GraphQLError(
`Variable "$${varName}" of type "${varType}" used in position expecting type "${type}".`,
{ nodes: [varDef, node] },
),
);
}
if (
isInputObjectType(parentType) &&
parentType.isOneOf &&
isNullableType(varType)
) {
context.reportError(
new GraphQLError(
`Variable "$${varName}" is of type "${varType}" but must be non-nullable to be used for OneOf Input Object "${parentType}".`,
{ nodes: [varDef, node] },
),
);
}
}
}
},
},
};
}
function allowedVariableUsage(
schema: GraphQLSchema,
varType: GraphQLType,
varDefaultValue: Maybe<ValueNode>,
locationType: GraphQLType,
locationDefaultValue: unknown,
): boolean {
if (isNonNullType(locationType) && !isNonNullType(varType)) {
const hasNonNullVariableDefaultValue =
varDefaultValue != null && varDefaultValue.kind !== Kind.NULL;
const hasLocationDefaultValue = locationDefaultValue !== undefined;
if (!hasNonNullVariableDefaultValue && !hasLocationDefaultValue) {
return false;
}
const nullableLocationType = locationType.ofType;
return isTypeSubTypeOf(schema, varType, nullableLocationType);
}
return isTypeSubTypeOf(schema, varType, locationType);
}