UNPKG

@falcondev-oss/caps

Version:

Simple, fully type-safe library to handle permissions/access control by defining capabilities with generators.

140 lines (138 loc) 3.52 kB
// src/capabilties.ts import * as R from "remeda"; function collectGenerator(generator) { const items = []; let next = generator.next(); do { items.push(next.value); next = generator.next(); } while (!next.done); items.push(next.value); return items.filter(Boolean).flat(); } function createActor() { return { build(builder) { return (actor, opts) => { return builder(createCapability(actor, opts)); }; } }; } var filterInputSymbol = Symbol("filterInput"); function createCapability(actor, opts) { function getDefine() { return (resolver, args) => createQuery({ actor, resolver, opts }); } return { subject: () => ({ define: getDefine() }), define: getDefine() }; } var MissingCapabilityError = class extends Error { constructor(capability) { super(`Missing capability: '${capability}'`); this.name = "MissingCapabilityError"; } }; function createQuery({ actor, resolver, opts }) { function list({ subject, args = {} }) { const generator = resolver({ actor, subject, args }); return collectGenerator(generator); } function getCan(subject) { return (capability, ...args) => { const getCapabilities = () => list({ subject, args: args[0] ?? {} }); return { throw: () => { const actorCaps = list({ subject, args: args[0] }); if (actorCaps.includes(capability)) return; throw opts?.createError?.({ capability }) ?? new MissingCapabilityError(capability); }, check: () => getCapabilities().includes(capability) }; }; } const query = { subject: (subject) => ({ can: getCan(subject), list(args) { return list({ subject, args }); } }), subjects: (subjects, ...args) => { const mapper = args[0]; const mappedSubjects = mapper ? subjects.map(mapper) : subjects; return { canSome: (capability, ...args2) => { return { check: () => mappedSubjects.some((subject) => getCan(subject)(capability, ...args2).check()), throw: () => mappedSubjects.some((subject) => getCan(subject)(capability, ...args2).throw()) }; }, canEvery: (capability, ...args2) => { return { check: () => mappedSubjects.every((subject) => getCan(subject)(capability, ...args2).check()), throw: () => { if (mappedSubjects.every((subject) => getCan(subject)(capability, ...args2).check())) return; throw opts?.createError?.({ capability }) ?? new MissingCapabilityError(capability); } }; }, filter: (capabilities, args2) => { return subjects.map((s) => ({ ...mapper ? mapper(s) : s, [filterInputSymbol]: s })).filter( (subject) => R.intersection( list({ subject, args: args2 }), capabilities ).length === capabilities.length ).map((s) => s[filterInputSymbol]); } }; }, can: getCan(void 0), list(args) { return list({ subject: void 0, args }); } }; return query; } // src/index.ts function arg() { return {}; } function mode(key, obj) { return { __mode: key, ...obj }; } export { MissingCapabilityError, arg, createActor, mode };