Policy
Allow / forbid rules with composable conditions
Express authorization as small rules over an object. Forbid always wins over allow.
import { Policy, eq, and, or, has } from "semola/policy";Define rules
type Post = {
id: number;
authorId: number;
status: "draft" | "published";
tags: string[];
};
const posts = new Policy<Post>();
const currentUser = { id: 1 };
posts.allow({
action: "read",
conditions: { status: eq("published") },
});
posts.allow({
action: ["update", "delete"],
conditions: { authorId: eq(currentUser.id) },
reason: "Authors can edit their own posts",
});
posts.forbid({
action: "delete",
conditions: { status: eq("published") },
reason: "Published posts cannot be deleted",
});Check
const post = {
id: 1,
authorId: 1,
status: "draft" as const,
tags: ["notes"],
};
const result = posts.can("update", post);
if (!result.allowed) {
console.log(result.reason);
return;
}
// proceedaction can be a string or an array (one rule covers many verbs). Omit conditions to match every object for that action.
Condition helpers
Import what you need from semola/policy:
eq, neq, gt, gte, lt, lte, not, and, or, startsWith, endsWith, includes, matches, has, hasAny, hasLength, isEmpty, isDefined, isNullish
Combine helpers on fields:
posts.allow({
action: "feature",
conditions: {
tags: has("featured"),
status: eq("published"),
},
});For a one-off check on a field, any object with a fn that receives the field value works:
posts.allow({
action: "archive",
conditions: {
status: {
fn: (status: Post["status"]) => status === "draft",
},
},
});