-
-
Notifications
You must be signed in to change notification settings - Fork 825
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
SchemaDirectiveVisitor.visitInputFieldDefinition resolver doesn't fire #858
Comments
I have the same problem. I need to validate input fields but function resolve in visitInputFieldDefinition doesn't fire |
I have the same problem. Anybody found a solution? |
The reason is quite simple, Facing the same issue, wanted to restrict mutation on specific field based on user authorization. |
Agreed, I would also really like to see a resolve on input types. However I found this on the Apollo blog which uses a custom scalar handler to validate instead. Not really ideal but it works. |
@ryall could you share your solution which uses scalar for input field auth? |
Here you go. It works a bit differently from the example I linked before. I prefer one directive per constraint rather than smashing them all together. This works by defining a # Validators
directive @email on INPUT_FIELD_DEFINITION
directive @length(min: Int, max: Int) on INPUT_FIELD_DEFINITION
input AccountCreateInput {
email: String! @email
password: String! @length(min: 8)
} export class ValidationTypeError extends Error {}
class EmailDirective extends SchemaDirectiveVisitor {
visitInputFieldDefinition(field: GraphQLInputField): GraphQLInputField | void | null {
field.type = ValidationType.create(field.type, new EmailConstraint());
}
}
class LengthDirective extends SchemaDirectiveVisitor {
visitInputFieldDefinition(field: GraphQLInputField): GraphQLInputField | void | null {
field.type = ValidationType.create(field.type, new LengthConstraint(this.args));
}
}
export const DIRECTIVES: Record<string, typeof SchemaDirectiveVisitor> = {
email: EmailDirective,
length: LengthDirective,
}; interface ValidationConstraint {
getName(): string;
validate(value);
getCompatibleScalarKinds(): string[];
}
export class EmailConstraint implements ValidationConstraint {
getName(): string {
return 'Email';
}
validate(value) {
if (_.isString(value) && !validator.isEmail(value)) {
throw new ValidationError('email', value);
}
}
getCompatibleScalarKinds(): string[] {
return [Kind.STRING];
}
}
export class LengthConstraint implements ValidationConstraint {
private readonly args: { [name: string]: any };
constructor(args: { [name: string]: any }) {
this.args = args;
}
getName(): string {
return 'Length';
}
validate(value) {
if (_.isString(value) && !validator.isByteLength(value, this.args)) {
throw new ValidationError('length', value, this.args);
}
}
getCompatibleScalarKinds(): string[] {
return [Kind.STRING];
}
} /**
* A validation type is injected from a validation directive and serves the purpose of
* applying the passed constraint to the type.
*
* Unfortunately input types don't currently have a "resolve" mechanism from directives
* so this is a workaround
*/
export class ValidationType extends GraphQLScalarType {
/**
* Create a new validation type with the existing type wrapped inside
*/
static create(type, constraint: ValidationConstraint) {
// Wrap scalar types directly
if (type instanceof GraphQLScalarType) {
return new this(type, constraint);
}
// If the root is a non-null type, we should wrap the inner type instead
if (type instanceof GraphQLNonNull && type.ofType instanceof GraphQLScalarType) {
return new GraphQLNonNull(new this(type.ofType, constraint));
}
throw new Error(`Type ${type} cannot be validated. Only scalars are accepted`);
}
/**
* Create the wrapper type and validation handler for the constraint on the type
*/
private constructor(type, constraint: ValidationConstraint) {
super({
name: `Is${constraint.getName()}`,
description: 'Scalar type wrapper for input validation',
/**
* Server -> Client
*/
serialize(value) {
return type.serialize(value);
},
/**
* Client (Variable) -> Server
*/
parseValue(value) {
const parsedValue = type.parseValue(value);
constraint.validate(parsedValue);
return parsedValue;
},
/**
* Client (Param) -> Server
*/
parseLiteral(valueNode: ValueNode, variables?: Maybe<{ [key: string]: any }>) {
const parsedValue = type.parseLiteral(valueNode, variables);
constraint.validate(parsedValue);
return parsedValue;
},
});
}
} |
Hitting the same issue, is there any way around it / is the resolve property on the roadmap to be added to input types? |
It would be really nice to be able to validate an entire input object with one directive, but cannot see how without each of the fields having a resolve prop 😫 |
@sami616 you should be able to get the parent from the details, and then all the fields from the parent const fields = objectType.getFields();
Object.keys(fields).forEach(fieldName => {
const field = fields[fieldName];
const { resolve = defaultFieldResolver } = field;
field.resolve = async function (...args) {
// Custom validation here
resolve.apply(this, args);
}
} |
@gaillota This indeed works for visitFieldDefinition but an input doesn't have a resolve function so that doesn't work with visitInputDefinition. |
I resorted to wrapping the mutation instead, and explicitly throwing if an input type is wrapped. Schema: directive @hasRole(
requires: RoleAccessType
) on FIELD_DEFINITION | OBJECT
type Mutation {
updateConfig (
patch: ConfigPatch!
): Config @hasRole(requires: IsAdmin)
} Directive: export class HasRoleDirective extends SchemaDirectiveVisitor {
visitInputObject(type: GraphQLInputObjectType) {
console.error(`HasRoleDirective cannot wrap input "${type.name}", wrap a type instead!`);
process.abort();
}
} |
Got the same issue, just need to figure out a way to access |
Hi guys, I have implemented a directive that is checking access control for Input Fields on mutations. My implementation gives access to context and value on the input field. ( Of course, it's proof of concept but it can be helpful for someone) import {defaultFieldResolver} from 'graphql';
import {SchemaDirectiveVisitor} from 'graphql-tools';
import filter from 'lodash/filter';
import union from 'lodash/union';
export default class AuthDirective extends SchemaDirectiveVisitor {
getMutations(predicate = null) {
if (!this._mutations) {
this._mutations = Object.values(
this.schema.getMutationType().getFields()
);
}
if (!predicate) {
return this._mutations || [];
}
return filter(this._mutations, predicate);
}
visitInputFieldDefinition(field, {objectType}) {
const {name, defaultValue} = field;
addAuthInfoToDescription(field, this.args.roles);
const mutationsForInput = this.getMutations(({args = []}) => {
return args.find(arg => arg && arg.type && arg.type.ofType === objectType;
});
mutationsForInput.forEach(mutation => {
const {resolve = defaultFieldResolver} = mutation;
mutation.resolve = function staffResolve(...args) {
const params = args[1];
// some lookup...
const subKey = Object.values(params).find(el => el && el[name]);
if (
params[name] !== defaultValue ||
(subKey && subKey[name] !== defaultValue)
) {
const context = args[2];
// throws an error if no auth
ensureIsAuth(context, this.args.roles);
}
return resolve.apply(this, args);
};
});
}
visitArgumentDefinition(argument, {field}) {
const {name, defaultValue} = argument;
const {resolve = defaultFieldResolver} = field;
addAuthInfoToDescription(argument, this.args.roles);
field.resolve = function staffResolve(...args) {
const params = args[1];
if (params[name] !== defaultValue) {
const context = args[2];
// throws an error if no auth
ensureIsAuth(context, this.args.roles);
}
return resolve.apply(this, args);
};
}
visitObject(type) {
this.ensureFieldsWrapped(type);
type.__staff = true;
type.__staffRoles = this.args.roles || [];
addAuthInfoToDescription(type, this.args.roles);
}
visitFieldDefinition(field, details) {
this.ensureFieldsWrapped(details.objectType);
field.__staff = true;
field.__staffRoles = this.args.roles || [];
addAuthInfoToDescription(field, this.args.roles);
}
ensureFieldsWrapped(objectType) {
// Mark the GraphQLObjectType object to avoid re-wrapping:
if (objectType._staffFieldsWrapped) return;
objectType._staffFieldsWrapped = true;
const fields = objectType.getFields();
Object.keys(fields).forEach(fieldName => {
const field = fields[fieldName];
const {resolve = defaultFieldResolver} = field;
field.resolve = function staffResolve(...args) {
if (field.__staff || objectType.__staff) {
const context = args[2];
// throws an error if no auth
ensureIsAuth(context, union(
field.__staffRoles,
objectType.__staffRoles
));
}
return resolve.apply(this, args);
};
});
}
}
// Adds annotation to the schema, helpful e.g. in a playground
function addAuthInfoToDescription(field, roles) {
roles = roles || [];
if (!roles.length) {
roles.push('AUTH');
}
field.description = `**REQUIRE:** ${roles.join(
', '
)} \n ${field.description || ''}`;
} ##############
# Directives #
##############
"Authorisation"
directive @authf(
roles: [SomeAuthRoles]
) on OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION |
const mutationsForInput = this.getMutations(({args = []}) => {
return args.find(arg => arg && arg.type && arg.type.ofType === objectType;
}); Something is missing here. Where do you get args from? |
Hi @carloschneider, |
This problem is really challenging to work around. We'd really like to allow basic data transforms on our input types to reduce code bloat. We often have stuff where we want to do something like |
See #789 |
Solved this with https://github.com/profusion/apollo-validation-directives/ but it wasn't that simple, had to mark all input types requiring validation, then walk all the fields to see if any had input arguments that would lead to the validated type (even nested), if so then wrap the resolver to first validate the argument (or nested) and just after that call the resolver. That also introduces a |
Closing for now as working as designed with package from community available to help streamline. |
I have created a hacky solution with: https://github.com/fastify/fastify-request-context On context initialization I inject the graphql context into the global context and then use the docs example: https://www.apollographql.com/docs/apollo-server/schema/custom-scalars/ And on parse literal I get the context and do whatever I need. I think this is a bad practice but its much more concise that other aforementioned examples |
Using @cristo-rabani 's code above I was able to do something similar in typescript(with a lot of ts-ignores). This might be handy for anyone else who is trying to make it work in typescript or might find the variable names unclear, although it's still pretty terse: visitInputFieldDefinition(field: GraphQLInputField, details: any) {
const { name, defaultValue } = field;
// @ts-ignore
const schema: GraphQLSchema = this.schema;
// @ts-ignore
const objectType: GraphQLInputObjectType = details.objectType;
const mutationType = schema.getMutationType();
if (!mutationType) {
return;
}
const mutationsForInput = Object.values(mutationType.getFields()).filter(({ args = [] }): { args: any[] } => {
// @ts-ignore
return args.find((arg) => arg && arg.type === objectType);
});
mutationsForInput.forEach((mutation) => {
const originalResolver = mutation.resolve;
if (!originalResolver) {
throw new Error('Cant make stuff without resolver');
}
mutation.resolve = async function wrappedResolver(...resolverArguments: any[]) {
// @ts-ignore
const args: GraphQLArgument[] = resolverArguments[1];
if (!args) {
throw new Error('Cannot wrap function without args');
}
// @ts-ignore
const argument = Object.values(args).find((el) => el && el[name]);
if (argument) {
// @ts-ignore
const value = argument[name];
if (!grantService) {
throw new Error('LunaSec Grant Service was not configured for the graphql token directive.');
}
const context: ExpressContext = resolverArguments[2];
if (value && value !== defaultValue) {
await SOME_ASYNC_FUNCTION // throws if there is an error. Could also overwrite the value if needed
}
}
// @ts-ignore
return originalResolver.apply(this, resolverArguments);
};
});
} Seriously, graphql should trye to make this easier. Obviously it's a hard problem, based on the above code, but clearly a lot of people need it! We shouldn't have to do something so crazy. 🙃 |
While attempting to build a custom auth directive I'm unable to get wrapped resolvers to invoke when working with Input types. Is this the expected behavor? In my case I'd like to use a custom directive to limit write access via mutation input arguments. Consider the following example
Starting the server prints
Submitting the following graphQL mutation
Prints
saving author...
I'd expect to see
Most examples I see that use custom directives and input types only change
filed.type
, but I'm hoping to invoke some custom auth logic before the mutation resolver runs.#640 seems like it should help here but I've been unable to get my auth resolver to invoke.
The text was updated successfully, but these errors were encountered: