Entity / foldkit-entity
Declaring what an input writes
An input says what each key is; a Write says what an operation does with
them: Write.update one row, named by a key, setting every other key mapped to
a field; Write.insert a row under an id the client chose; or Write.delete
one, which sets nothing.
import { Write } from 'foldkit-entity'
const EditPostInput = Schema.Struct({ id: Schema.String, title: Schema.String })
const EditPost = Entity.input(Blog.Post, EditPostInput)
const Edit = Write.update(EditPost, { id: 'id' })
Write.writes(Edit) // the fields and `one` relations any invocation may change
Write.bind(Edit, { id: 'p1', title: 'Compilers' })
// { entity: 'Post', id: 'p1', values: { title: 'Compilers' }, links: {}, increments: {}, assignments: {} }It is declared data, like a query body: nothing is written by building one, and
an Entity having a field grants no one the right to set it. bind encodes each
value through its field's schema, so the patch is in the store's shape, and
takes the keys to write when an author changed only some. expect names a key
holding the revision the row was read at, for an interpreter to write only a row
still at it. An unmapped key is not written. A key mapped to a one relation
points the row elsewhere: bind gives it as a ref in links (none for an
optional relation set to nothing), and each interpreter carries it its own way,
a ref key in a store, a foreign key in a table. A many relation or a nested
key is refused where the write is declared.
Values no client chooses: Entity.sets
Some values no client should choose: who wrote a row, and when. The Entity
says so once, with Entity.sets, after Entity.relate when it names a
relation. A value is a term of the write's context, Expr.viewer or
Expr.now, and on says which writes set it:
const AuthoredPost = Blog.Post.pipe(Entity.sets({ author: Expr.viewer(Author) }, { on: 'create' }))Expr.viewer(Author) is whoever asks: a one relation to an Author pointed
at them, or a field stored as text holding their id. Expr.now is the time,
for a field stored as a number. A path from the viewer is what their row, or
a row reached from it, holds: Expr.path(Expr.viewer(Member),
Expr.path(Member.relations.team, Team.fields.orgId)) sets a note's orgId to
its writer's team's, for a field that holds what the path's end holds. A
server reads it through its own sources as the asker may see them; a client
reads it from the rows it holds, and predicts nothing where it lacks one. A
query body or a guard rule compares with one as with any value
(Expr.eq(Note.fields.orgId, orgOfViewer)): SQL reads it in subqueries under
each reached table's visible rule, and Judge from the rows its holder has
(rows), unknown where it lacks the viewer's row.
on: 'create' sets the member on every insert; on: 'write' on every insert
and every update (an editedAt). A delete sets nothing. Every write of the
Entity follows from this one declaration (write.context), and a write whose
input sets, links, moves or computes such a member is refused where it is
declared. Write.contextValues(write, { viewer, now }, read) works out what
they come to, read giving one member of one row at a time for a path. Each interpreter fills them from what
it has: a server from its principal and its clock, a client as a prediction. A
value that comes to null where the member holds none (a visitor) is named
unfilled: a server refuses the write, a client predicts nothing for it.
Nothing else about what a write does is declared anywhere: what it moves is
read from the Entities' derived members and the queries' bodies.
When a write applies: when
An update or a delete may also say when it makes sense: when is what the row
must hold for it to land, written as a query body is: () =>
Expr.eq(Post.fields.title, 'Draft'), or with the input, ({ input }) =>
Expr.eq(Post.fields.title, input.from). It reads the row as it is before the write and the input,
never who is asking (that is a guard's), and only its own Entity: a field of
another is a type error, and refused where the write is declared. It is held as
a query body (write.when), so an interpreter checks it in the statement that
writes, and a row it does not hold of is refused as unmet. A write declared with
one is a ConditionedWrite, which is how Mutation.write knows it may be
refused so.
Moving a number: increment
An update may move a number field by an amount the input holds, rather than set
it: increment: { stock: 'quantity' } adds the input's quantity (an unmapped
key, a number; negative takes away) to the stored stock, in the statement that
writes, so no concurrent change is lost. A retry must not add twice, so either it
expects a revision (a retry of a write that landed is then a conflict), or its
writer keeps a ledger, which answers the retry from its record and lets two
concurrent increments both land. RemoteServer.write refuses one with neither,
and one of a real number with no revision even under a ledger: floating-point
addition does not associate, so two concurrent sums could land as two results.
The field must be stored as a number, and may not be the id, the revision, or
set by another key. Write.bind gives the amounts apart from the values (increments), so a
client can add them to the value it holds as it reads it (Remote does), never
fixing a sum over a row that may since have moved. Write.reads(write) is what a write
reads from its row: what its when reads, each field it increments, what its
assignments read, and the revision. How a declaration, its guard, its writer and
the client fit together is the declared writes guide.
Computing a field: compute
An update may also compute a field from the row before it and the input:
compute: { label: ({ row, input }) => concat(input.prefix, row.name) }, with an
operator such as the concat below. It
reads neither the row after the write nor another Entity, so assignments never
depend on each other, and is refused where it is declared for a field another
key writes or a value of a kind the field does not store. A defined operator in
it is expanded there, so Write.bind carries it as data (assignments, each a
Computation: fields by key, values encoded, operators by name), which a writer
compiles into the statement and a client predicts by its own table. A
computation does not repeat without effect, so a retry of one is held to it as
an increment's is; without a revision it lands only under a ledger, and there
only where the writer pins the row it read (a guarded writer with a revision)
or by an operator whose commutativity and associativity are tested over every
value for each implementation the writer runs (Conformance.laws in memory,
Conformance.lawsBy in SQL).
The row a write leaves: Write.apply
Write.apply(row, bound, { operators }) is the row a bound write leaves over
the row before it: values set, amounts added (a null stays null, as in SQL),
assignments computed. It is the one computation of that row: a guard judges
it, a writer explains a miss by it, a client predicts it. unknown names each
field it could not work out (one the row lacks, or an operator the table lacks),
which keeps what the row holds.
What a client foresees of a supplied member
A count or an exists says what it is, so a client predicts it for a row it
writes. A Derived.make member says nothing, so a client waits for the server,
unless it says how to foresee it:
Derived.make(schema, { predict }):predict({ entity, row, rows, context })reads the row the write leaves as the holder has it (aHeldRow, with the holder's otherrowsand the write'scontext) and returns the value, encoded, or none where it cannot know.Derived.make(schema, { forbids }):forbids(value, surface)says which write to a row the value refuses.surfaceis aWriteSurface(kindUpdateorDelete, themembersit writes, therelationsit points asLinkorUnlink), and the answer isOption<{ reason }>, only for a definite refusal. A client holding the value refuses that write where it is made, showing and sending nothing.
foldkit-guard's may declares both.