Guard / foldkit-guard
Serving writes
A declared write (Mutation.write) is served by RemoteServer.write with no
handler. Wrap its writer, and the write rules are checked before it lands:
import { Write } from 'foldkit-entity'
import { Mutation } from 'foldkit-remote'
import { writer } from 'foldkit-remote-drizzle'
const EditTodo = Mutation.write(
'EditTodo',
Write.update(Entity.input(Todo, Schema.Struct({ id: Schema.String, title: Schema.String })), {
id: 'id',
}),
)
RemoteServer.write(EditTodo, GuardServer.writer(Todo, Policy, writer(Db.Todo)))- An update or a delete reads the row first, as its writer may see it. An update of a row they may not see is refused; a delete of one changes nothing and answers it gone, as for a row already deleted.
- The row's rule for the command must hold over the row, and an update's over the row as it would be after it too: a write cannot move a row out of its writer's reach.
- Each member the write sets needs its own
CreateorUpdaterule to hold, and each relation it points elsewhere itsUnlinkandLink. What a relation was linked to is read as stored (the writer'slinked), since a link to a row hidden from its writer reads to them as none; a writer that cannot say treats every link it sees as none as one that may be there, so changing it needsUnlinktoo. - A create is checked over the values it proposes. A guard that reads a field
it leaves unset does not hold, whatever it asks (
isNullandisNotNullalike): the create never said what the field holds. - A refusal is
Forbidden, with the sentenceGuards.explaingave; the write never runs. - The answer is read back under the binding's
visibleand the Entity's member Read rules: the row as its writer may now see it, with no more fields than the write set, and a row the write hid from them indeleted, so the client takes it out of its lists instead of placing it.
The binding must have a visible rule, which is how the writer reads a row
as its writer may see it; the writer refuses one without. That the rule is
GuardDrizzle.visible's is what GuardServer.validate checks of the
Entity's Source, so give the writer that Source's binding.
A client should not choose whose a new row is. presets sets a field from
who is asking on every create, before the rules are checked, so a rule over
it holds of the creator:
GuardServer.writer(Todo, Policy, writer(Db.Todo), { presets: { ownerId: user => user.id } })The create's input leaves ownerId out; an input that sets it is refused as
the server's own mistake. A visitor has no one to preset from, and is refused
even where the rules would let anyone create. A preset must be a field of the
Entity, of that field's type.
A handler writes what its code writes, so it is guarded where it is defined:
GuardServer.mutation is RemoteServer.mutation with the row's rule for one
interaction checked before the handler runs.
const ArchiveTodo = Mutation.make('ArchiveTodo', { Input: { id: Schema.String }, Output: {} })
GuardServer.mutation(
ArchiveTodo,
{ entity: Todo, policy: Policy, interaction: Archive.key, id: input => input.id, source: TodoSource },
({ input, row }) => Effect.succeed({ output: {} }), // `row`: the todo, as its writer sees it
)- The row
idnames is read throughsource, which must apply the Entity's Read rules (it is refused where it is defined otherwise); a row the writer may not see is refused, and the rule must hold over the rest. - With no
id(a create), the rule must hold of who is asking alone. Anidthat answers anOptionserves create and edit in one mutation (oneCrud.editorfor both):Option.none()is a create, checked against the row'sCreaterule. - What the handler writes is its own to check: members it sets, relations it
points.
fieldsreads more of the row for it. - Each row of the Entity its answer carries is read back through
source, itsauthorizeandfilterapplied, as a declared write's is: the answer shows the writer only what they may now see of the fields it named, and a row the handler hid from them answers as deleted.