@relay-graphql-js/validation-rules
Version:
shared relay validation rules for graphql-js parser
199 lines (191 loc) • 7.24 kB
text/typescript
import type {
DirectiveNode,
FragmentDefinitionNode,
FragmentSpreadNode,
ObjectValueNode,
TypeNode,
ValidationContext,
ValidationRule,
} from "graphql";
import { GraphQLError, KnownArgumentNamesRule, parseType, visit } from "graphql";
import { getArgumentDefinitions } from "./argumentDefinitions";
import { containsVariableNodes } from "./utils";
// tslint:disable-next-line: no-shadowed-variable
export const RelayKnownArgumentNames: ValidationRule = function RelayKnownArgumentNames(context) {
const originalRuleVisitor = KnownArgumentNamesRule(context);
return {
Argument(argumentNode) {
/**
* Always forward field arguments to the original rule.
*/
visit(argumentNode, originalRuleVisitor);
return false;
},
FragmentSpread(fragmentSpreadNode) {
const fragmentDefinitionNode = context.getFragment(fragmentSpreadNode.name.value);
if (
fragmentDefinitionNode &&
(!fragmentSpreadNode.directives ||
fragmentSpreadNode.directives.findIndex((directive) => directive.name.value === "arguments") === -1) &&
getArgumentDefinitions(fragmentDefinitionNode)
) {
validateFragmentArguments(context, fragmentDefinitionNode, fragmentSpreadNode);
}
},
Directive(directiveNode, _key, _parent, _nodePath, ancestors) {
if (directiveNode.name.value === "argumentDefinitions") {
validateFragmentArgumentDefinitions(context, directiveNode);
return false;
}
if (directiveNode.name.value === "arguments") {
const fragmentSpreadNode = ancestors[ancestors.length - 1] as FragmentSpreadNode;
const fragmentDefinitionNode = context.getFragment(fragmentSpreadNode.name.value);
if (fragmentDefinitionNode) {
validateFragmentArguments(context, fragmentDefinitionNode, fragmentSpreadNode, directiveNode);
}
return false;
}
/**
* Forward any other directives to original rule.
*/
visit(directiveNode, originalRuleVisitor);
return false;
},
};
};
function validateFragmentArgumentDefinitions(context: ValidationContext, directiveNode: DirectiveNode) {
if (!directiveNode.arguments || directiveNode.arguments.length === 0) {
context.reportError(new GraphQLError(`Missing required argument definitions.`, directiveNode));
} else {
directiveNode.arguments.forEach((argumentNode) => {
const metadataNode = argumentNode.value;
if (metadataNode.kind !== "ObjectValue" || !metadataNode.fields.some((field) => field.name.value === "type")) {
context.reportError(
new GraphQLError(
`Metadata of argument definition should be of type "Object" with a "type" and optional "defaultValue" key.`,
metadataNode
)
);
} else {
metadataNode.fields.forEach((fieldNode) => {
const name = fieldNode.name.value;
if (name !== "type" && name !== "defaultValue") {
context.reportError(
new GraphQLError(`Unknown key "${name}" in argument definition metadata.`, fieldNode.name)
);
}
const valueNode = fieldNode.value;
if (name === "type") {
if (valueNode.kind !== "StringValue") {
context.reportError(
new GraphQLError(
`Value for "type" in argument definition metadata must be specified as string literal.`,
valueNode
)
);
} else {
let typeNode: TypeNode | null = null;
try {
typeNode = parseType(valueNode.value);
} catch (error) {
context.reportError(new GraphQLError(error.message, valueNode));
}
if (typeNode) {
while (typeNode.kind === "NonNullType" || typeNode.kind === "ListType") {
typeNode = typeNode.type;
}
if (!context.getSchema().getType(typeNode.name.value)) {
context.reportError(
new GraphQLError(
`Unknown type "${typeNode.name.value}" in argument definition metadata.`,
valueNode
)
);
}
}
}
} else if (name === "defaultValue") {
if (containsVariableNodes(fieldNode.value)) {
context.reportError(
new GraphQLError(
`defaultValue contains variables for argument ${argumentNode.name.value} in argument definition metadata.`,
valueNode
)
);
}
}
});
}
});
}
}
function isNullableArgument(argumentDefinition: ObjectValueNode): boolean {
const typeField = argumentDefinition.fields.find((f) => f.name.value === "type");
if (typeField == null) {
return false;
}
if (typeField.value.kind !== "StringValue") {
return false;
}
try {
const type = parseType(typeField.value.value);
return type.kind !== "NonNullType";
} catch (e) {
return false;
}
}
function validateFragmentArguments(
context: ValidationContext,
fragmentDefinitionNode: FragmentDefinitionNode,
fragmentSpreadNode: FragmentSpreadNode,
directiveNode?: DirectiveNode
) {
const argumentDefinitionNodes = getArgumentDefinitions(fragmentDefinitionNode);
const suggestionList = require("graphql/jsutils/suggestionList").default;
const didYouMean = require("graphql/jsutils/didYouMean").default;
if (!argumentDefinitionNodes) {
context.reportError(
new GraphQLError(
`No fragment argument definitions exist for fragment "${fragmentSpreadNode.name.value}".`,
fragmentSpreadNode
)
);
} else {
const argumentNodes = [...((directiveNode && directiveNode.arguments) || [])];
argumentDefinitionNodes.forEach((argumentDef) => {
const argumentIndex = argumentNodes.findIndex((a) => a.name.value === argumentDef.name.value);
if (argumentIndex >= 0) {
argumentNodes.splice(argumentIndex, 1);
} else {
const value = argumentDef.value;
if (value.kind === "ObjectValue") {
if (
value.fields.findIndex((field) => field.name.value === "defaultValue") === -1 &&
!isNullableArgument(value)
) {
context.reportError(
new GraphQLError(
`Missing required fragment argument "${argumentDef.name.value}".`,
directiveNode || fragmentSpreadNode
)
);
}
} else {
console.log(`Unexpected fragment argument value kind "${value.kind}".`);
}
}
});
argumentNodes.forEach((argumentNode) => {
const suggestions: string[] = suggestionList(
argumentNode.name.value,
argumentDefinitionNodes.map((argDef) => argDef.name.value)
);
context.reportError(
new GraphQLError(
`Unknown fragment argument "${argumentNode.name.value}".` + didYouMean(suggestions.map((x) => `"${x}"`)),
directiveNode
)
);
});
}
}