trpc-shield
Version:
tRPC permissions as another layer of abstraction!
348 lines (305 loc) • 8.76 kB
text/typescript
import { ILogicRule, IOptions, IRule, IRuleConstructorOptions, IRuleFunction, IRuleResult, ShieldRule } from './types';
export class Rule<TContext extends Record<string, any>> implements IRule<TContext> {
readonly name: string;
private func: IRuleFunction<TContext>;
constructor(name: string, func: IRuleFunction<TContext>, _constructorOptions: IRuleConstructorOptions) {
this.name = name;
this.func = func;
}
async resolve(
ctx: TContext,
type: string,
path: string,
input: { [name: string]: any },
rawInput: unknown,
options: IOptions<TContext>,
): Promise<IRuleResult<TContext>> {
try {
/* Resolve */
const res = await this.executeRule(ctx, type, path, input, rawInput, options);
if (res instanceof Error) {
return res;
} else if (typeof res === 'string') {
return new Error(res);
} else if (res === true) {
return true;
} else if (typeof res === 'object' && res !== null && 'ctx' in res) {
// Context extension object
return res as { ctx: Partial<TContext> };
} else {
return false;
}
} catch (err) {
if (options.debug) {
throw err;
} else {
return false;
}
}
}
/**
*
* Compares a given rule with the current one
* and checks whether their functions are equal.
*
*/
equals(rule: Rule<TContext>): boolean {
return this.func === rule.func;
}
private executeRule<TContext extends Record<string, any>>(
ctx: TContext,
type: string,
path: string,
input: { [name: string]: any },
rawInput: unknown,
options: IOptions<TContext>,
): string | boolean | Error | { ctx: Partial<TContext> } | Promise<IRuleResult<TContext>> {
// @ts-ignore
return this.func(ctx, type, path, input, rawInput, options);
}
}
export class LogicRule<TContext extends Record<string, any>> implements ILogicRule<TContext> {
private rules: ShieldRule<TContext>[];
constructor(rules: ShieldRule<TContext>[]) {
this.rules = rules;
}
/**
* By default logic rule resolves to false.
*/
async resolve(
_ctx: TContext,
_type: string,
_path: string,
_input: { [name: string]: any },
_rawInput: unknown,
_options: IOptions<TContext>,
): Promise<IRuleResult<TContext>> {
return false;
}
/**
* Evaluates all the rules.
*/
async evaluate(
ctx: TContext,
type: string,
path: string,
input: { [name: string]: any },
rawInput: unknown,
options: IOptions<TContext>,
): Promise<IRuleResult<TContext>[]> {
const rules = this.getRules();
const tasks = rules.map((rule) => rule.resolve(ctx, type, path, input, rawInput, options));
return Promise.all(tasks);
}
/**
* Returns rules in a logic rule.
*/
getRules() {
return this.rules;
}
}
// Extended Types
export class RuleOr<TContext extends Record<string, any>> extends LogicRule<TContext> {
constructor(rules: ShieldRule<TContext>[]) {
super(rules);
}
/**
* Makes sure that at least one of them has evaluated to true.
*/
async resolve(
ctx: TContext,
type: string,
path: string,
input: { [name: string]: any },
rawInput: unknown,
options: IOptions<TContext>,
): Promise<IRuleResult<TContext>> {
const result = await this.evaluate(ctx, type, path, input, rawInput, options);
// Look for context extensions
const contextExtension = result.find((res) => typeof res === 'object' && res !== null && 'ctx' in res);
if (result.some((res) => res === true || (typeof res === 'object' && res !== null && 'ctx' in res))) {
return contextExtension || true;
} else {
const customError = result.find((res) => res instanceof Error);
return customError || false;
}
}
}
export class RuleAnd<TContext extends Record<string, any>> extends LogicRule<TContext> {
constructor(rules: ShieldRule<TContext>[]) {
super(rules);
}
/**
* Makes sure that all of them have resolved to true.
*/
async resolve(
ctx: TContext,
type: string,
path: string,
input: { [name: string]: any },
rawInput: unknown,
options: IOptions<TContext>,
): Promise<IRuleResult<TContext>> {
const result = await this.evaluate(ctx, type, path, input, rawInput, options);
// Look for context extensions
const contextExtension = result.find((res) => typeof res === 'object' && res !== null && 'ctx' in res);
if (result.some((res) => res !== true && !(typeof res === 'object' && res !== null && 'ctx' in res))) {
const customError = result.find((res) => res instanceof Error);
return customError || false;
} else {
return contextExtension || true;
}
}
}
export class RuleChain<TContext extends Record<string, any>> extends LogicRule<TContext> {
constructor(rules: ShieldRule<TContext>[]) {
super(rules);
}
/**
* Makes sure that all of them have resolved to true.
*/
async resolve(
ctx: TContext,
type: string,
path: string,
input: { [name: string]: any },
rawInput: unknown,
options: IOptions<TContext>,
): Promise<IRuleResult> {
const result = await this.evaluate(ctx, type, path, input, rawInput, 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(
ctx: TContext,
type: string,
path: string,
input: { [name: string]: any },
rawInput: unknown,
options: IOptions<TContext>,
): Promise<IRuleResult[]> {
const rules = this.getRules();
return iterate(rules);
async function iterate([rule, ...otherRules]: ShieldRule<TContext>[]): Promise<IRuleResult[]> {
if (rule === undefined) return [];
return rule.resolve(ctx, type, path, input, rawInput, options).then((res) => {
if (res !== true) {
return [res];
} else {
return iterate(otherRules).then((ress) => ress.concat(res));
}
});
}
}
}
export class RuleRace<TContext extends Record<string, any>> extends LogicRule<TContext> {
constructor(rules: ShieldRule<TContext>[]) {
super(rules);
}
/**
* Makes sure that at least one of them resolved to true.
*/
async resolve(
ctx: TContext,
type: string,
path: string,
input: { [name: string]: any },
rawInput: unknown,
options: IOptions<TContext>,
): Promise<IRuleResult> {
const result = await this.evaluate(ctx, type, path, input, rawInput, 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(
ctx: TContext,
type: string,
path: string,
input: { [name: string]: any },
rawInput: unknown,
options: IOptions<TContext>,
): Promise<IRuleResult[]> {
const rules = this.getRules();
return iterate(rules);
async function iterate([rule, ...otherRules]: ShieldRule<TContext>[]): Promise<IRuleResult[]> {
if (rule === undefined) return [];
return rule.resolve(ctx, type, path, input, rawInput, options).then((res) => {
if (res === true) {
return [res];
} else {
return iterate(otherRules).then((ress) => ress.concat(res));
}
});
}
}
}
export class RuleNot<TContext extends Record<string, any>> extends LogicRule<TContext> {
error?: Error;
constructor(rule: ShieldRule<TContext>, error?: Error) {
super([rule]);
this.error = error;
}
/**
*
* Negates the result.
*
*/
async resolve(
ctx: TContext,
type: string,
path: string,
input: { [name: string]: any },
rawInput: unknown,
options: IOptions<TContext>,
): Promise<IRuleResult> {
const [res] = await this.evaluate(ctx, type, path, input, rawInput, 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<TContext extends Record<string, any>> extends LogicRule<TContext> {
constructor() {
super([]);
}
/**
*
* Always true.
*
*/
async resolve(): Promise<IRuleResult> {
return true;
}
}
export class RuleFalse<TContext extends Record<string, any>> extends LogicRule<TContext> {
constructor() {
super([]);
}
/**
*
* Always false.
*
*/
async resolve(): Promise<IRuleResult> {
return false;
}
}