@ephys/graphql-joi-directives
Version:
Adds Joi-powered constraint directive for GraphQL
117 lines (116 loc) • 4.9 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildJoiDirective = exports.JoiConstrainedScalar = void 0;
const graphql_1 = require("graphql");
const graphql_tools_1 = require("graphql-tools");
function matchValidTypeName(val) {
return /^[_a-zA-Z][_a-zA-Z0-9]*$/.test(val);
}
const substitutionMap = new Map();
let substitutionI = 0;
function stringifyArgs(args) {
return Object.entries(args).map(val => {
if (typeof val[1] === 'boolean') {
return val[1] ? val[0] : null;
}
// this is a bit brittle but it replaces generated names that would not match a valid GraphQL identifier with "sub_{number}"
// currently it's ok because the only directive that accepts strings is @str(pattern: "")
// and pattern must always start with /
// if other directives start accepting strings, this will become a problem.
// alternatives:
// - hash all resulting inputs strings (md4) & ensure they're not colliding (append a number if they're colliding)
if (typeof val[1] === 'string' && !matchValidTypeName(val[1])) {
if (!substitutionMap.has(val[1])) {
substitutionMap.set(val[1], substitutionI++);
}
return `${val[0]}_sub_${substitutionMap.get(val[1])}`;
}
return val.join('_');
}).filter(val => val != null)
.join('__');
}
class JoiConstrainedScalar extends graphql_1.GraphQLScalarType {
constructor(scalarType, args, fieldName) {
super({
name: `Constrained${scalarType.name}__${stringifyArgs(args)}`,
serialize: value => {
value = scalarType.serialize(value);
this.#validateJoi(fieldName, args, value);
return value;
},
parseValue: value => {
value = scalarType.serialize(value);
return this.#validateJoi(fieldName, args, value);
},
parseLiteral: (ast, vars) => {
const value = scalarType.parseLiteral(ast, vars);
return this.#validateJoi(fieldName, args, value);
},
});
}
/** memoized joi schema */
#schema;
#validateJoi(fieldName, args, value) {
if (!this.#schema) {
this.#schema = this.buildJoi(fieldName, args, value);
}
const output = this.#schema.validate(value, {
convert: true,
});
if (output.error) {
throw new graphql_1.GraphQLError(output.error.message);
}
return output.value;
}
}
exports.JoiConstrainedScalar = JoiConstrainedScalar;
function buildJoiDirective(tag, scalarType, ConstrainedScalarType) {
class JoiConstraintDirective extends graphql_tools_1.SchemaDirectiveVisitor {
// input MyInput {
// varOne: String! @str(min: 1)
// }
visitInputFieldDefinition(field) {
return this.#wrapField(field);
}
// ! Not currently supported !
// type Object {
// myQuery: String! @list(min: 1)
// }
// visitFieldDefinition(field: GraphQLField<any, any>) {
// return this.wrapField(field);
// }
// type Query {
// myQuery(var: [String!]! @list(min: 1))
// }
visitArgumentDefinition(arg) {
return this.#wrapField(arg);
}
#wrapField(field) {
field.type = this.#wrapType(field.type, field.name);
return field;
}
#wrapType(type, fieldName) {
if (type instanceof graphql_1.GraphQLNonNull) {
return new graphql_1.GraphQLNonNull(this.#wrapType(type.ofType, fieldName));
}
if (type instanceof graphql_1.GraphQLList) {
return new graphql_1.GraphQLList(this.#wrapType(type.ofType, fieldName));
}
if (type !== scalarType) {
throw new Error(`@${tag} can only be used on type ${scalarType.name}`);
}
// This is a workaround to solve an issue where `JoiConstraintString` is not being declared in the gql schema
// but declaring it as a scalar doesn't work either because the dynamic type won't be used
// as types can only be declared statically:
// https://github.com/apollographql/apollo-server/issues/1303#issuecomment-404684981
// Current solution is to dynamically generate their name by signifying their args
// @ts-expect-error
const newType = new ConstrainedScalarType(type, this.args, fieldName);
const typeMap = this.schema.getTypeMap();
typeMap[newType.name] = typeMap[newType.name] || newType;
return newType;
}
}
return JoiConstraintDirective;
}
exports.buildJoiDirective = buildJoiDirective;