graphql-shield
Version:
GraphQL Server permissions as another layer of abstraction!
368 lines (367 loc) • 9.93 kB
JavaScript
import * as Yup from 'yup';
import { isLogicRule } from './utils.js';
import { isUndefined } from 'util';
export class Rule {
constructor(name, func, constructorOptions) {
const options = this.normalizeOptions(constructorOptions);
this.name = name;
this.func = func;
this.cache = options.cache;
this.fragment = options.fragment;
}
/**
*
* @param parent
* @param args
* @param ctx
* @param info
*
* Resolves rule and writes to cache its result.
*
*/
async resolve(parent, args, ctx, info, options) {
try {
/* Resolve */
const res = await this.executeRule(parent, args, ctx, info, options);
if (res instanceof Error) {
return res;
}
else if (typeof res === 'string') {
return new Error(res);
}
else if (res === true) {
return true;
}
else {
return false;
}
}
catch (err) {
if (options.debug) {
throw err;
}
else {
return false;
}
}
}
/**
*
* @param rule
*
* Compares a given rule with the current one
* and checks whether their functions are equal.
*
*/
equals(rule) {
return this.func === rule.func;
}
/**
*
* Extracts fragment from the rule.
*
*/
extractFragment() {
return this.fragment;
}
/**
*
* @param options
*
* Sets default values for options.
*
*/
normalizeOptions(options) {
return {
cache: options.cache !== undefined
? this.normalizeCacheOption(options.cache)
: 'no_cache',
fragment: options.fragment !== undefined ? options.fragment : undefined,
};
}
/**
*
* @param cache
*
* This ensures backward capability of shield.
*
*/
normalizeCacheOption(cache) {
switch (cache) {
case true: {
return 'strict';
}
case false: {
return 'no_cache';
}
default: {
return cache;
}
}
}
/**
* Executes a rule and writes to cache if needed.
*
* @param parent
* @param args
* @param ctx
* @param info
*/
executeRule(parent, args, ctx, info, options) {
switch (typeof this.cache) {
case 'function': {
/* User defined cache function. */
const key = `${this.name}-${this.cache(parent, args, ctx, info)}`;
return this.writeToCache(key)(parent, args, ctx, info);
}
case 'string': {
/* Standard cache option. */
switch (this.cache) {
case 'strict': {
const key = options.hashFunction({ parent, args });
return this.writeToCache(`${this.name}-${key}`)(parent, args, ctx, info);
}
case 'contextual': {
return this.writeToCache(this.name)(parent, args, ctx, info);
}
case 'no_cache': {
return this.func(parent, args, ctx, info);
}
}
}
/* istanbul ignore next */
default: {
throw new Error(`Unsupported cache format: ${typeof this.cache}`);
}
}
}
/**
* Writes or reads result from cache.
*
* @param key
*/
writeToCache(key) {
return (parent, args, ctx, info) => {
if (!ctx._shield.cache[key]) {
ctx._shield.cache[key] = this.func(parent, args, ctx, info);
}
return ctx._shield.cache[key];
};
}
}
export class InputRule extends Rule {
constructor(name, schema, options) {
const validationFunction = (parent, args, ctx) => schema(Yup, ctx)
.validate(args, options)
.then(() => true)
.catch((err) => err);
super(name, validationFunction, { cache: 'strict', fragment: undefined });
}
}
export class LogicRule {
constructor(rules) {
this.rules = rules;
}
/**
* By default logic rule resolves to false.
*/
async resolve(parent, args, ctx, info, options) {
return false;
}
/**
* Evaluates all the rules.
*/
async evaluate(parent, args, ctx, info, options) {
const rules = this.getRules();
const tasks = rules.map((rule) => rule.resolve(parent, args, ctx, info, options));
return Promise.all(tasks);
}
/**
* Returns rules in a logic rule.
*/
getRules() {
return this.rules;
}
/**
* Extracts fragments from the defined rules.
*/
extractFragments() {
const fragments = this.rules.reduce((fragments, rule) => {
if (isLogicRule(rule)) {
return fragments.concat(...rule.extractFragments());
}
const fragment = rule.extractFragment();
if (fragment)
return fragments.concat(fragment);
return fragments;
}, []);
return fragments;
}
}
// Extended Types
export class RuleOr extends LogicRule {
constructor(rules) {
super(rules);
}
/**
* Makes sure that at least one of them has evaluated to true.
*/
async resolve(parent, args, ctx, info, options) {
const result = await this.evaluate(parent, args, ctx, info, options);
if (result.every((res) => res !== true)) {
const customError = result.find((res) => res instanceof Error);
return customError || false;
}
else {
return true;
}
}
}
export class RuleAnd extends LogicRule {
constructor(rules) {
super(rules);
}
/**
* Makes sure that all of them have resolved to true.
*/
async resolve(parent, args, ctx, info, options) {
const result = await this.evaluate(parent, args, ctx, info, options);
if (result.some((res) => res !== true)) {
const customError = result.find((res) => res instanceof Error);
return customError || false;
}
else {
return true;
}
}
}
export class RuleChain extends LogicRule {
constructor(rules) {
super(rules);
}
/**
* Makes sure that all of them have resolved to true.
*/
async resolve(parent, args, ctx, info, options) {
const result = await this.evaluate(parent, args, ctx, info, options);
if (result.some((res) => res !== true)) {
const customError = result.find((res) => res instanceof Error);
return customError || false;
}
else {
return true;
}
}
/**
* Evaluates all the rules.
*/
async evaluate(parent, args, ctx, info, options) {
const rules = this.getRules();
return iterate(rules);
async function iterate([rule, ...otherRules]) {
if (isUndefined(rule))
return [];
return rule.resolve(parent, args, ctx, info, options).then((res) => {
if (res !== true) {
return [res];
}
else {
return iterate(otherRules).then((ress) => ress.concat(res));
}
});
}
}
}
export class RuleRace extends LogicRule {
constructor(rules) {
super(rules);
}
/**
* Makes sure that at least one of them resolved to true.
*/
async resolve(parent, args, ctx, info, options) {
const result = await this.evaluate(parent, args, ctx, info, options);
if (result.some((res) => res === true)) {
return true;
}
else {
const customError = result.find((res) => res instanceof Error);
return customError || false;
}
}
/**
* Evaluates all the rules.
*/
async evaluate(parent, args, ctx, info, options) {
const rules = this.getRules();
return iterate(rules);
async function iterate([rule, ...otherRules]) {
if (isUndefined(rule))
return [];
return rule.resolve(parent, args, ctx, info, options).then((res) => {
if (res === true) {
return [res];
}
else {
return iterate(otherRules).then((ress) => ress.concat(res));
}
});
}
}
}
export class RuleNot extends LogicRule {
constructor(rule, error) {
super([rule]);
this.error = error;
}
/**
*
* @param parent
* @param args
* @param ctx
* @param info
*
* Negates the result.
*
*/
async resolve(parent, args, ctx, info, options) {
const [res] = await this.evaluate(parent, args, ctx, info, options);
if (res instanceof Error) {
return true;
}
else if (res !== true) {
return true;
}
else {
if (this.error)
return this.error;
return false;
}
}
}
export class RuleTrue extends LogicRule {
constructor() {
super([]);
}
/**
*
* Always true.
*
*/
async resolve() {
return true;
}
}
export class RuleFalse extends LogicRule {
constructor() {
super([]);
}
/**
*
* Always false.
*
*/
async resolve() {
return false;
}
}