@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
JavaScript
// 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
};