Created
April 24, 2019 18:00
-
-
Save FluorescentHallucinogen/75d12beb731f1a253c8f188919d5f5a7 to your computer and use it in GitHub Desktop.
@hasRole GraphQL custom directive
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
directive @hasRole ( | |
OR: [ConstraintInput!] | |
NOT: [ConstraintInput!] | |
AND: [ConstraintInput!] | |
has_some: [UserRole!] | |
has_every: [UserRole!] | |
) on OBJECT | FIELD_DEFINITION | |
input ConstraintInput { | |
OR: [ConstraintInput!] | |
NOT: [ConstraintInput!] | |
AND: [ConstraintInput!] | |
has_some: [UserRole!] | |
has_every: [UserRole!] | |
} | |
enum UserRole { | |
ADMIN | |
EDITOR | |
MANAGER | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const { defaultFieldResolver } = require('graphql'); | |
const { SchemaDirectiveVisitor } = require('graphql-tools'); | |
const { createError } = require('apollo-errors'); | |
const { validate } = require('./validation'); | |
class HasRole extends SchemaDirectiveVisitor { | |
visitObject(type) { | |
this.ensureFieldsWrapped(type); | |
type._authRule = this.args; | |
} | |
visitFieldDefinition(field, details) { | |
this.ensureFieldsWrapped(details.objectType); | |
field._authRule = this.args; | |
} | |
ensureFieldsWrapped(objectType) { | |
if (objectType._authFieldsWrapped) return; | |
objectType._authFieldsWrapped = true; | |
const fields = objectType.getFields(); | |
Object.keys(fields).forEach(fieldName => { | |
const field = fields[fieldName]; | |
const { resolve = defaultFieldResolver } = field; | |
field.resolve = async function (...args) { | |
const rule = | |
field._authRule || | |
objectType._authRule; | |
if (!rule) { | |
return resolve.apply(this, args); | |
} | |
//const roles = ['ADMIN', 'EDITOR']; | |
//const { roles } = await getUser(args[2]); | |
const errors = validate(roles, rule); | |
if (errors && errors.length) { | |
const AuthorizationError = createError('AuthorizationError', { | |
message: 'Access denied', | |
internalData: { | |
rule, | |
errors | |
} | |
}); | |
throw new AuthorizationError(); | |
} | |
return resolve.apply(this, args); | |
} | |
}); | |
} | |
} | |
module.exports = HasRole; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ValidationError extends Error { | |
constructor(message) { | |
super(message || 'Validation error'); | |
this.name = 'ValidationError'; | |
} | |
} | |
class NegationError extends ValidationError { | |
constructor(condition) { | |
super('Invalid negation in logical expression'); | |
this.name = 'NegationError'; | |
this.condition = condition; | |
} | |
} | |
const validators = { | |
has_some: (value, list) => { | |
const values = [].concat(value); | |
if (!list.some(item => values.includes(item))) { | |
throw new ValidationError(); | |
} | |
}, | |
has_every: (value, list) => { | |
const values = [].concat(value); | |
if (!list.every(item => values.includes(item))) { | |
throw new ValidationError(); | |
} | |
} | |
}; | |
function validate(value, condition) { | |
/* | |
if (Array.isArray(condition)) { | |
return handleOrCondition(condition) | |
} else | |
*/ | |
if (condition && typeof condition === 'object') { | |
return handleObjectCondition(condition); | |
} else { | |
throw new Error('Invalid validation rule'); | |
} | |
function handleObjectCondition(props) { | |
return [].concat(...Object.entries(props).map(([key, param]) => { | |
switch (key) { | |
case 'OR': return handleOrCondition(param); | |
case 'AND': return handleAndCondition(param); | |
case 'NOT': return handleNotCondition(param); | |
default: return handleValidation(key, param); | |
} | |
})); | |
} | |
function handleAndCondition(list) { | |
return [].concat(...list.map(handleObjectCondition)); | |
} | |
function handleOrCondition(list) { | |
const hasError = res => res.length; | |
const fails = list.map(handleObjectCondition).filter(hasError); | |
return fails.length === list.length ? [].concat(...fails) : []; | |
} | |
function handleNotCondition(list) { | |
const hasError = res => res.length; | |
const fails = list.map(handleObjectCondition).filter(hasError); | |
//return fails.length !== list.length ? [new NegationError(list)] : []; | |
return fails.length ? [] : [new NegationError(list)]; | |
} | |
function handleValidation(key, param) { | |
const validate = validators[key]; | |
if (!validate) { | |
throw new Error(`Unknown validator: ${key}`); | |
} | |
try { | |
validate(value, param); | |
} catch (error) { | |
if (error instanceof ValidationError) { | |
error.condition = { [key]: param }; | |
return [error]; | |
} else { | |
throw error; | |
} | |
} | |
return []; | |
} | |
} | |
module.exports = { | |
validate | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment