Hi @maticzav! First of all thanks for the awesome library!
I have few questions/suggestions about CustomErrors:
Example:
I have isAuthenticated rule
export default rule()((parent, args, { user }) => Boolean(user));
I want to return my custom 401 error like this
import { SevenBoom } from 'graphql-apollo-errors';
rule(null, {
customError: SevenBoom.unauthorized('You are not authenticated')
})((parent, args, { user }) => Boolean(user))
Example:
import { SevenBoom } from 'graphql-apollo-errors';
shield({ /* Rules… */ }, {
customError: SevenBoom.forbidden('You are not allowed to do this action.')
})
I can implement this and looks like it is not a breaking change.
Just want to clarify if there is no way to do this in current version of the library?
Let me know if you have any questions. Thanks!
@kolybasov this is great. I have nothing to add to second question, let’s make a PR! 🎉
Right now, there are two ways to solving the first question. You could use allowExternalErrors which will allow all errors that happen during execution to pass through. The second one is by throwing CustomError in rule evaluation.
Hope this answers your question 🙂
@maticzav thanks for the fast response!
Let's imagine I have 2 rules: isAuthenticated and isAdmin.
// isAuthenticated.js
import { shield, rule, and } from 'graphql-shield';
// User should be present in context
const isAuthenticated = rule()((parent, args, ctx) => Boolean(ctx.user));
// User role should be equal to 'admin'
const isAdmin = rule()((parent, args, ctx) => ctx.user.role === 'admin');
// I am combining this 2 rules together
const permissions = shield({
Query: {
getAllUsers: and(isAuthenticated, isAdmin)
}
});
My goal to have this:
If I understood correctly now we always will have CustomError('Not Authorised!') if rules check was failed, right?
Thanks for helping with this!
Hey 👋
Almost correct; logical operators, though, don't act like new rules (they should act like logical operators!). So, you can do something like this:
// isAuthenticated.js
import { shield, rule, and, CustomError } from "graphql-shield";
// User should be present in context
const isAuthenticated = rule()((parent, args, ctx) => {
if (ctx.user) {
return true;
} else {
throw new CustomError(401);
}
});
// User role should be equal to 'admin'
const isAdmin = rule()((parent, args, ctx) => {
if (ctx.user.role === 'admin') {
return true;
} else {
throw new CustomError(403);
}
});
// I am combining this 2 rules together
const permissions = shield({
Query: {
getAllUsers: and(isAuthenticated, isAdmin)
}
});
As you can see, if one of the rules fails it will return desired error and vice versa.
And yes, big time! I would love to see a PR for customizable default error. I hope this helps you out. 🙂
And in case of or operator it will still throw an error, right?
import { shield, rule, or, CustomError } from "graphql-shield";
// User should be present in context
const isAuthenticated = rule()((parent, args, ctx) => {
if (ctx.user) {
return true;
} else {
throw new CustomError(401);
}
});
// User role should be equal to 'admin'
const isAdmin = rule()((parent, args, ctx) => {
if (ctx.user.role === 'admin') {
return true;
} else {
throw new CustomError(403);
}
});
// I am combining this 2 rules together
const permissions = shield({
Query: {
getAllUsers: or(isAuthenticated, isAdmin)
}
});
So if I need only some of this rules then throwing custom error is not the case. Correct?
The first question, indeed, it will work with all logical operators includingor. I am not sure I understand the second one, though. Both of these rules work with or without being combined with or. Therefore, they will act the same way - considering errors - once they are combined or used alone.
Is this what you had in mind?
Sorry for the confusion.
For me it looks like when I need to check against few rules combined with or and first rule will be false but second rule will result to true shield will throw an error.
My example don't plays really nice together(treat it as pseudocode) but if we will take both rules isAuthenticated and isAdmin and combine them with or(isAuthenticated, isAdmin) then rule never pass if isAuthenticated will throw an error. But it should pass if we will use booleans, right?
OK, I can see it now. I think we should find a way to combine the CustomError somehow to make it work with or. I think this is more of a philosophical question in a way. Therefore, I would love to have a short discussion here before making changes.
My thinking process looks like this;
and is pretty clear since everything should work for it to pass.or and not are a bit different in a way.Maybe the best option would be to return an error instead of throwing it since this also better aligns with code writing philosophy in my opinion.
To conclude, I propose to change the functionality by adding customError or error function which is returned from the rule in case of "failure".
Nevertheless, if anything throws an error, the rule should fail. I believe this is crucial for reducing unexpected errors.
What do you think?
Hey @maticzav! I am late a little bit :)
Totally agree with this:
Nevertheless, if anything throws an error, the rule should fail. I believe this is crucial for reducing unexpected errors.
Seems the best option in my opinion!
Maybe the best option would be to return an error instead of throwing it since this also better aligns with code writing philosophy in my opinion.
To conclude, I propose to change the functionality by adding customError or error function which is returned from the rule in case of "failure".
Any help required with this? I see you already started to implement some parts. Thanks!
Hey @kolybasov, I need some help. Could you give me your opinion on this; In a hypothetical situation, a user has defined a rule with a custom error. Unfortunately, they weren't precise and made a mistake in their resolver (not rule). Should the rule return customMessage or fallback by default?
@maticzav I imagine it this way:
import { rule, and } from 'graphql-shield';
const isAuthenticated = rule()((parent, args, { user }) => {
if (user) return true;
return new Error('Not Authenticated');
});
const isAdmin = rule()((parent, args, { user }) => {
if (user.admin) return true;
return new Error('You have no permission to do this');
});
const canBanUser = and(isAuthenticated, isAdmin);
type Mutation {
banUser(userId: ID!): Boolean
}
isAuthenticated rule fails we raise Not Authenticated error.isAdmin rule fails we raise You have no permission to do this error.allowExternalErrors: true we raise original error.allowExternalErrors: false we raise fallback or default error.debug: true original error should always be raised.Hope it makes sense. Let me know if you have more questions :)
And thank you very much for handling this!
@maticzav so basically, graphql-shield rule can be in 4 states.
true – all is finefalse – rule is failed, use default/fallback errorReturnedError instanceof Error – rule is failed, use returned errorUnhandled Exception – rules is failed, use default/fallback error depends on debug and allowExternalErrors options.Personally, I do not use graphql-shield ability to hide internal errors. I format them with graphql-apollo-errors https://github.com/GiladShoham/graphql-apollo-errors#usage
Ok, so my current implementation works like this; I used your first suggestion over mine because it seemed better in the meantime.
const isAdmin = rule({
error: "You don't have permission to do this."
})(async (parent, args, ctx, info) => {
if (ctx.user) {
return true
} else {
return false
}
})
// no user -> error "You don't have permission to do this."
const isOwner = rule({
error: new ApolloError('Something')
})(async (parent, args, ctx, info) => {
if (ctx.user) {
return true
} else {
return false
}
})
// no user -> ApolloError "Something."
Now that I think about it since having the ability to throw an error from inside the rule gives us the ability to access parent, args, ctx, info etc. This might be useful. Nevertheless, your approach from above seems clearer. At the same time, I believe throwing errors is not the best practice.
What do you think?
@maticzav sorry for the initial confusion I was not sure about best solution also.
I am suggesting not to throw error but return it. So abstract rules check will looks like this:
const result = await rule.resolve(parent, args, ctx, info);
if (result instanceof Error) {
throw result;
} else if (result === false) {
throw new CustomError('Not Authorised!'); // Current default error, also make it replacebels via global config(IOptions interface)
} else {
// all is ok
return resolver(parent, args, ctx, info);
}
I see... Should we make both cases possible? Can you imagine a scenario where the first idea would make more sense?
@maticzav I believe they are just alternatives to each other and it is just matter of preference how you would like to specify custom error.
In my opinion returning an error instead of passing it as param gives more flexibility because then rule may have many custom errors specified in case you need them for some complex one. And it is hard to imagine implemented with passing an error as param.
rule()((parent, args, ctx, info) => {
if (args.something) {
return new Error('Something');
} else if (args.somethingElse) {
return new Error('Something else');
} else if (args.evenMore) {
return new Error('Even more!');
}
return true;
});
I agree, the new code already reflects that. @kolybasov let me know if you have a bit of spare time, I could really use a hand to write docs and examples. You could also give feedback on the changes and introduce the new ones. Tell me if that's an option for you!
@maticzav sure! Just let me know what I can do and I will help!
Perfect! Are you already in Prisma Slack? If not, could you join it here and find me at maticzav. I think we can better communicate there.
@maticzav I will join and ping you tomorrow then!
@maticzav by some reason I can not receive invite to Prisma Slack. Any chance you are using Prisma Spectrum chat? https://spectrum.chat/prisma
Just released the new version, going to close the issue now.
I’m a few years late, but this seems to be a general problem with functions doing more than one thing. Rules should just test and return a boolean. That’s a separate concern from deciding which error to throw.