Guard / foldkit-guard
Sixty seconds
ts
import { Effect, Option, Schema } from 'effect'
import { Entity, Expr } from 'foldkit-entity'
import { Guards } from 'foldkit-guard'
const User = Schema.Struct({
id: Schema.String,
role: Schema.Literals(['admin', 'member']),
projectIds: Schema.Array(Schema.String),
})
const TodoFields = Entity.define(
'Todo',
Schema.Struct({
id: Schema.String,
ownerId: Schema.String,
projectId: Schema.String,
title: Schema.String,
}),
)
// Shared: names and predicates, which are data.
const G = Guards.declare(
'Todos',
User,
guard => ({
isAdmin: guard.principal(),
isOwner: guard.row(TodoFields, p => Expr.eq(TodoFields.fields.ownerId, p.id)),
inProject: guard.row(TodoFields, p => Expr.in(TodoFields.fields.projectId, p.projectIds)),
}),
{ key: user => user.id, unguarded: 'deny' },
)
// Server: the body of every principal guard, and nothing else.
const Policy = G.implement({
isAdmin: principal => Option.exists(principal, user => user.role === 'admin'),
})
Effect.runSync(
Policy.decide(G.isOwner, {
principal: Option.some({ id: 'ada', role: 'member', projectIds: [] }),
interaction: 'Read',
row: { id: 't1', ownerId: 'ada', projectId: 'p1', title: 'Write the README' },
}),
) // trueWhat each call does, and does not:
Guards.declarereads nothing and stores nothing. It builds each row guard's predicate once, with placeholders (p.idis the inputprincipal.id), and checks it reads only its Entity's fields. Branching on a placeholder (p.role === 'admin') is a type error: there is no value yet.G.implementis the only place a body exists. A missing or extra key is a type error, and refused at runtime for a caller the types do not reach.Policy.decideis anEffectof aDecision:true,false, or{ allowed: false, reason }for a refusal the caller may read. A visitor isOption.none(), and passes no row guard that reads a principal's fields; one over the row alone (isPublished) holds for a visitor too.G.Principalis the server's Context key for who is asking, anOptionof the principal;G.key(user)names the person, for what is theirs whatever their claims, such as a cache snapshot (see Who is asking).