I’ve just finished implementing this directive. It still needs some improvements, such as supporting arrays of periods and possibly allowing different kinds of values, like ISO date strings or raw numbers (for example, to define min/max age or min/max price) and other kinds of validations and optimizations.
Maybe this directive could be renamed to @range, where it could accept a value type as an argument, but for now, it’s a good starting version.
You can test:
import {
GraphQLInputObjectType,
GraphQLSchema,
defaultFieldResolver,
isObjectType,
isInputObjectType,
getNamedType,
GraphQLInputField,
} from "graphql";
import { getDirective, mapSchema, MapperKind } from "@graphql-tools/utils";
const directiveName = "validateDateRange";
type ValidateDateRangeDirectiveType = {
start: string;
end: string;
};
export function validateDateRangeDirective() {
const metadata = new Map<string, ValidateDateRangeDirectiveType>();
return {
validateDateRangeDirectiveTransformer: (schema: GraphQLSchema) => {
mapSchema(schema, {
[MapperKind.INPUT_OBJECT_TYPE]: (fieldConfig) => {
const directive = getDirective(
schema,
fieldConfig,
directiveName
)?.[0];
if (directive) {
metadata.set(fieldConfig.name, {
start: directive.start,
end: directive.end,
});
return fieldConfig;
}
},
});
return mapSchema(schema, {
[MapperKind.OBJECT_FIELD]: (fieldConfig) => {
const resolver = fieldConfig.resolve ?? defaultFieldResolver;
fieldConfig.resolve = async (source, args, context, info) => {
const parentType = info.schema.getType(info.parentType.name);
if (isObjectType(parentType)) {
const field = parentType.getFields()[info.fieldName];
const argumentDefs = field.args;
for (const def of argumentDefs) {
const argName = def.name;
const argValue = args[argName];
const inputType = getNamedType(def.type);
if (
isInputObjectType(inputType) &&
argValue &&
typeof argValue === "object"
) {
validateInputObject(argValue, inputType, metadata);
}
}
}
return resolver(source, args, context, info);
};
return fieldConfig;
},
});
},
};
}
function validateInputObject(
value: any,
inputType: GraphQLInputObjectType,
inputTypeDirectives: Map<string, ValidateDateRangeDirectiveType>
) {
const directiveArgs = inputTypeDirectives.get(inputType.name);
if (directiveArgs) {
const { start, end } = directiveArgs;
const startValue = new Date(parseInt(value[start]));
const endValue = new Date(parseInt(value[end]));
if (startValue > endValue) {
throw new Error(
`Invalid date range in input '${inputType.name}': field '${start}' must be before '${end}'`
);
}
}
// Recursively check nested input fields
const fields = inputType.getFields();
for (const fieldName in fields) {
const field: GraphQLInputField = fields[fieldName];
const fieldType = getNamedType(field.type);
const fieldValue = value[fieldName];
if (
isInputObjectType(fieldType) &&
fieldValue &&
typeof fieldValue === "object"
) {
validateInputObject(fieldValue, fieldType, inputTypeDirectives);
}
}
}
Make sure to apply the schema transformer to activate the directive
const { validateDateRangeDirectiveTransformer } =
validateDateRangeDirective();
const directiveTransformers = [
//...othersDirectives,
validateDateRangeDirectiveTransformer
]
const transformedSchema = directiveTransformers.reduce(
(schema, transformer) => transformer(schema),
buildSubgraphSchema({
typeDefs,
resolvers,
})
);
In my tests, I used timestamps to represent the data:
"data": {
"period": {
"startAt": "1745857565081",
"finishAt": "1743857565081"
}
}