Repository navigation
v0.8.0
foldkit-entity 0.2.0; foldkit-remote,
foldkit-remote-drizzle and foldkit-remote-server 0.5.0; foldkit-cms,
foldkit-cms-drizzle and foldkit-mixins-crud published for the first time at
0.1.0. foldkit-crud, foldkit-form and foldkit-mixins-form are republished
at 0.1.1 only so their pinned foldkit-entity is the current one — every
package pins its workspace dependencies exactly, so a dependent left behind
would install a second copy of it.
-
foldkit-entity:Expr.isNull/Expr.isNotNull/Expr.contains, and a
predicate may stand where a boolean is wanted. The three operations the CMS
worklist needs, and no more.isNullandisNotNullare one node with the
answer absence gives flipped, so nothing has to negate a predicate.
Expr.eq(Expr.isNotNull(field), input.flag)is how a query depends on an
input without branching on it — a body is built once, so there is nothing to
branch on.containstreats the empty string as everything, which makes an
empty search box the same query as a full one; over a nullable column that
is not the same as no filter, because a null contains nothing. It is
case-insensitive (ASCII folding), which is stated rather than left to the
backend: SQLite'slikeignores case and Postgres's does not, so a body that
left it open would mean two things. On Postgres this makes the CMS search
case-insensitive where it was not. -
foldkit-cms: the worklist is declared by what it means.CmsEntries
carries all three of its questions and its order, sofoldkit-cms-drizzle
registers it asquery(descriptor, { entity })and theand(eq, ternary, ternary)written in Drizzle's dialect is gone. Same rows, same escaping of
%and_, same audience boundary — the boundary is conjoined with the body
byvisible, as for every other source. -
foldkit-remote-drizzle: a predicate compared to a boolean is resolved at
request time, into that predicate or its negation, rather than sent to the
database as a boolean parameter — which dialects disagree about, and which
SQLite refuses outright. The SQL is then exactly what a hand-written
archived ? isNotNull : isNullproduced. -
foldkit-remote-server:evaluate, the reference interpreter. Runs a
query body over rows already in memory — pure, reading the rows it is given and
nothing else. It is what makes a body source-neutral in fact rather than in
principle: the tests run the same body through it and through
foldkit-remote-drizzleagainst a real SQLite, and require the same ids in the
same order. It follows SQL, not JavaScript —null = nullis unknown and
matches no row, where JavaScript would call the two equal. It refuses rather
than guesses in two places: ordering by a column that is null in some row
(SQLite sorts nulls first, Postgres last, so there is no answer to be
conformant to), and comparing values it has no order for. -
foldkit-remote-drizzle: a query body compiles to SQL. A descriptor
declared withQuery.defineneeds onlyquery(descriptor, { entity: binding })
— the columns and the order come from its body, through the binding that knows
which column holds which field. A field the binding has no column for is refused
when the source is registered, not when a request arrives — a predicate's
fields as well as an ordering's, so a server that starts is a server whose
queries can be answered. A body order that does not already end on the id gets
it appended, exactly as a computedorderBydoes, since a body says what the
rows mean and not how a cursor walks them; a literalorderBygiven at
registration is left as written. The compiled
predicates are conjoined with the server's ownwhereand the binding's
visiblerule, so a body is the application's question and never widens what a
principal may see; anorderBygiven at registration replaces the body's, since
a connection pages on exactly one order.Query.makeis unchanged and still
requires anorderBy. -
foldkit-cms:Cms.bySlugis declared by what it means. Its body is
eq(the slug field, input.slug)ordered by id, sofoldkit-cms-drizzle
registers the source without restating thewhereor the order in Drizzle's
dialect. The audience boundary is unchanged: a visitor still finds only
published rows, because visibility is conjoined with the body rather than
expressed by it. -
foldkit-remote:Query.define, a query declared by what it means.
Query.define(name, Input, ({ input }) => body)returns an ordinary
QueryDescriptor— the same name,Input,refand connection identity
Query.makegives — carrying itsbodybesides, with the result a connection
over the Entity the body reads so it is not named twice. A server can compile
that body instead of being told the same thing again in its own dialect; one
that would rather answer the query its own way still can, and a descriptor
fromQuery.makehas no body at all. The body is built once:input.slug
is a placeholder typed fromInput, never the value, so a condition that
depends on what was passed is a comparison over the placeholder. A body needs
afoldkit-entityEntity (Entity.define), which is what has addressable
fields; this package's ownEntity.makedescribes a field as the schema of
its value and has nothing to point at.Inputmust be fields or a plain
Schema.Struct: a codec exposing no keys is refused at declaration rather
than handing the body an empty object, which would compare a column to
nothing while every type agreed. -
foldkit-entity:Query, which rows a query is about. An Entity to
read, the predicates every row must hold, and the order to read them in,
composed withpipe:Query.from(Post).pipe(published, newest). Each step is
a new frozen value and performs no work. Twowheres conjoin and two
orderBys append — neither replaces, so piping a fragment can only narrow a
query, andQuery.unfiltered/Query.unorderedare the only ways back. A
predicate or ordering term over a different Entity thanQuery.fromis
refused where it is piped, by identity rather than by name. The
list of predicates is the conjunction, which is why there is still no
Expr.and: three conditions are threewheres.foldkit-remotespreads
these into its ownQuery, soQuery.makeandQuery.fromcome from one
import instead of two namespaces of the same name. -
foldkit-entity:Expr, a query's scalar computations as values. An
Entity says what a domain has; anExprsays something about one row of it.
Expr.eqcompares two scalars, coercing a field or a plain value on either
side;Expr.input(key, schema)stands for a value the query is given when it
runs;Order.asc/Order.descis one term of an ordering;
dependenciesOf(...)says which fields and inputs an expression reads and
which operations it uses. Building one performs no work — an interpreter
compiles it, which is what will let one query mean the same thing in more than
one place. Comparing a field to the wrong kind of value is an error where it is
written. Onlyeqexists: the operator set grows from real queries in this
repository, not from what a database could express. First step of
data-query-DESIGN §32, sized to the CMS's
bySlug. -
foldkit-remote:Data.confirmed(projection). The same projection read
over the server-derived store alone, with the pending optimistic layers and
connection overlays left off. It plans exactly what the projection plans, so
observing it fetches what observing the projection fetches and only what it
shows differs. For a reader that must not believe a change until the server
has agreed — in practiceAgent.when({ projection: Data.confirmed(...) }).
There is deliberately noData.visible: a projection already is the visible
read. async-semantics-DESIGN gated this on Remote having optimistic mutation
layers; the CMS's preview built them, and nothing had gone back to notice. -
foldkit-remote:RemoteData.render(data, cases). The view-oriented
fold: the six states as the three a view draws, keeping useful data on screen.
Initial/Loadingreachloading,Ready/Refreshingreachdata, and a
Failedstill carrying the value it had reachesdatatoo, so a failed read
does not throw away what the reader was looking at.notFoundis its own
branch and not optional. Thedatabranch is told which it got through one
Freshnesstag (Fresh/Refreshing/Stalewith its error), not a pair
of booleans that could claim both.matchis unchanged and still the
exhaustive fold. -
foldkit-remote: a refresh restarts only what it refreshed. The refresh
generation was one counter on the Remote store, which every read entry carried
as a dependency, so refreshing one Projection restarted every read stream and
cancelled every read in flight. Generations are now per field mark and per
connection identity, and an entry takes the highest over what it plans. A
connection needs its own mark: anetworkOnlyentry observing an invalidated
connection plans the same query either way.refreshmoves from a Struct to a
runtime field in the Model schema, besideloadingandgaps. The marks are
collected with the entities and connections they belong to, so they stay
bounded by what the Model holds rather than by how often it has refreshed. -
foldkit-remote:Data.inspect(model).loading. The reads in flight,
besidemutations.pendingfor the writes, so a tool can answer "what is
Remote doing now" from the Model rather than from the fibers doing the work. -
The optimistic vocabulary is stated once. Remote and Sync mean the same
four words by different mechanisms — confirmed or committed, plus pending, is
visible; settled is answered either way.docs/state-model.mdsays it with
both packages' names side by side, and both READMEs point there. -
foldkit-form: a form can say what it is editing. A check is given the
key's value and whatever else in the form decodes, which is not enough to ask
"is this address taken?": a post's input carries a title and an address, not
the row's id, so the check cannot tell a post's own address from someone
else's.FormModelgainssubject, a plain record set with
Message.About({ subject })and read withform.subject(model), and a check's
context gains it. It changes no draft, answers no submit, and survivesfill
andReset.Cms.editorsends it the row's id as it opens, so a content
type's slug check knows which row to pass over. This is the seam the CMS design
doc recorded as missing. -
foldkit-form: a check that needs something can now be added at all.
Form.makeand theForm.checksstep both read a check's requirement (R)
asnever, because Effect'sRis not an inference site a mapped type wins —
so a check needingRemoteClientwas refused by the shape meant to accept it,
and the documented "the check's requirements become the Bundle's" was
unreachable.Form.checksnow reads the requirement off the functions it is
given, and a type test pins it.Form.make's options still cannot infer it;
the README and the skill say to use the step. -
foldkit-cms:Cms.addressFree(type). A form check that says while the
author types what a publish would refuse. It asks the content type'sbySlug
query — which an author reads through the same binding as everyone else, so it
sees unpublished rows too — and excepts the row the editor says the form is
about. It needs no new server surface. It is advice, deliberately not the rule:
two authors can both be told an address is free, and the server refuses the
second on the same key. A check that cannot reach the server says nothing. -
foldkit-cms:Cms.editor, the authoring editor's state. A Bundle around
the content type's form: autosave after a rest, valid or not; publish by
submitting the form, saving first; a draft resumed by its saved Model, then its
values key by key, then what is published, and never failing to open;
Conflictwith reload and overwrite; discard. End to end tests drive it
against the real server over SQLite. -
Scheduling and archiving.
CmsSchedulepromises a draft that would publish
now,CmsUnscheduletakes it back, andcms.due(now, { as })publishes what has
come due, each in its own transaction, as whoever scheduled it; a failure stays
scheduled with its reason and waits for the draft to change. The package owns
no timer.CmsArchivealso takes what can be hidden off show. The editor gains
ScheduleAsked({ at }), which submits and saves first, andUnscheduleAsked,
ArchiveAsked,UnarchiveAsked.cms_draftsgainsscheduled_by. -
Review of the CMS, and what it fixed.
- Security: a scheduled draft is published as whoever scheduled it, and any
author could save, restore or discard over it, so an author who may not
publish could publish through one who may. That is now refused to anyone
allowwould not letschedule. - Correctness:
Transaction.statementstakes turns at its one connection; a
write arriving between another request'sbeginandrollbackwas rolled
back with it after being reported done. Every operation is now one
transaction, so a save cannot leave an entry without its draft. - The worklist's search treats
%and_as characters, and a draft's size is
bounded (maxDraftSize, one million characters of JSON by default). - The editor asks once when publish is pressed twice; lists a draft by what its
author typed, valid or not; forgets an old failure at the next save; resumes
a stored Model with nothing in flight (form.settled, new infoldkit-form);
follows a previewed post to the row publishing gives it; and gainsflush,
for an author who leaves within the rest of their last edit.
- Security: a scheduled draft is published as whoever scheduled it, and any
-
A server's word about one key, on that key.
foldkit-formgains
Message.Refused({ key, error }): the key reads invalid with that reason,
keeps what was typed, and clears on the next edit.Checkedcould not serve,
since it only answers a check the form itself started.Cms.editoruses it, so
a publish refused for a taken address marks the address, with nothing to wire. -
foldkit-cms:Opened, besideEditing.Editingmeant both "open and
idle" and "edited since the last save", so no application could word its status
truthfully.Editingnow means there are unsaved edits,Openedmeans the form
is as it was found, and a draft the form was filled from readsSaved. -
examples/cms: a post from its first keystroke to being taken off show, from
three chairs, over SQLite, with its transcript pinned, and the same application
in a browser (pnpm dev) where the chair is in the address.foldkit-cmsgains
Cms.editorViewand the editor'spageId, which the browser mode needed, and
annotates an entry'sidas hidden and itsstatewith a title, so a worklist
shows Title and State rather than a raw uuid and a lowercase header. -
In-app preview.
foldkit-remotegainsData.overlay(model, id, operations)
andData.lift(model, id)(OverlayShown,OverlayLifted): optimistic
operations shown with no request behind them. A content type'spreviewsays
how a value would look in the store, and the editor'sPreviewShownlays what
is in the form over it, edit by edit, untilPreviewHidden. -
Restoring.
CmsRestoremakes a revision's value the working copy and
publishes nothing; the editor'sRestoreAsked({ revision })shows it in the form. -
foldkit-cms: kinds and their renderers.Cms.slug(from)and
Cms.dateTime()for a form'sinputs;Cms.Display.Stateand
Cms.Display.Moment, whichCms.Entitiesare annotated with;
Cms.controlRenderers()andCms.displayRenderers()to spread beside the
mixins' own.foldkit-cmsnow depends onfoldkit-crud. -
Breaking,
foldkit-cmsandfoldkit-cms-drizzle:CmsSaveDraft'sentry
is required, and the first save of an id nobody has makes the entry
(Cms.newEntryId()), because a mutation's status carries no output for a
client to learn a server-made id from.CmsEntrygainsrevision, a column of
cms_entries, which a publish compares and sets inside its transaction. Every
operation patches the entry with itsstate.newIdis gone from the server's
config. A save based on a draft that is gone no longer leaves an entry behind. -
Fix,
foldkit-remote:Data.refreshof an entity known to be absent asks
for it again, as the README said it did. It did nothing: a tombstone has no
field to mark stale, and nothing planned a read of it. -
foldkit-form:form.partial(model), what decodes as it stands, by key. -
foldkit-cms-drizzle0.1.0 (new): drafts, publishing, and the audience boundary. The
entries, drafts and revisions tables for SQLite and Postgres;CmsSaveDraft
(compare and set on what the save was based on, so a second author is a
conflict) andCmsDiscardDraft; theCms.Entriesworklist; an entry's state
derived with the server's clock. To a principal that is not an author, the
three tables are empty on every read path and every operation is refused, and
published(column, isAuthor)makes a content table'svisiblerule, which
CmsServer.makerequires of a type that can be unpublished.CmsPublishruns
the application's owncreateorupdatehandler with the draft's value,
inside a transaction (Transaction.statementsorTransaction.drizzle) that
also shows the row, appends the revision and removes the draft; a publish made
from an older revision is a conflict.CmsUnpublishhides the row and keeps it.
A visitor is refused before anything is looked up. Aslugrole adds a
<name>BySlugquery (Cms.bySlug), and a publish to a taken slug fails as
CmsSlugTaken: <key>: ..., whether the check or the unique index caught it.
foldkit-cmsgainsCms.Entries. -
foldkit-remote-drizzle: a derived member may be{ supplied: true }: bound,
and answered by the application's own source. -
foldkit-remote-drizzle: row visibility. A binding takes
visible: principal => SQL | undefined, applied on every path its table is
read: by id, as a relation's children (listed, counted, paged), as the target
of aoneref (which readsnull), and through a query. Before this, a
by-id read and a query returned any row to any principal; only a collection
relation could be filtered, throughpolicies. -
foldkit-cms0.1.0 (new): the pure core.Cms.rolesmarks the members of
an Entity that play a CMS part (label,slug,published) as metadata, read
withCms.rolesOf.Cms.contentdeclares a type of content: its Entity, its
form, the application's own publish operations, its words.Cms.Entities
(Entry,Draft,Revision) andCms.Operationsare ordinary Entities and
Remote mutations.Cms.stateandCms.offersderive an entry's state and its
transitions from facts and a clock; nothing stores a status. No server yet. -
foldkit-form:Input.following(key, through)writes a key's draft from
another key until the author writes it themselves (a slug from a title).
Emptying it hands it back, and a form filled with a value for it does not
follow. The Model gainstouched; a control gainsfollows; a form gains
isFollowing, and a drawn field is toldfollowing. The first step of
the CMS design. -
foldkit-remote:Selection.fromcompiles an Entity Selection once.Data.get
andData.queryare called with one on every render, and compiling it each
time rebuilt its schemas and missed Remote's read cache, which is keyed by the
Selection: a list read is about eleven times faster, and repeated reads now
return the same value, not an equal one. -
foldkit-remoteandfoldkit-remote-server: a relation may be read whole and
by the page at once. A page of a whole list (Entity.page, or
Selection.connectionover an array of refs) is read under an alias,
comments@first=10, so it is a field of its own to the store, the planner and
the wire;RemoteServerreads the alias apart and answers under it, and the
live hub re-reads a subscriber's page when its list changes. A write to the
list marks its pages stale. Requirements for such a Selection now name the
alias infields,windowsandrelations. A request may page one
relation at most four ways, since each is a source read the client names, and
Entity.makerefuses a field whose name contains@. Both packages must be
upgraded together: an older server does not know an alias.RELATION_ALIAS,
relationAliasandaliasedFieldare exported. -
Words are text. Breaking.
FormMessagesentries may be text with blanks
('{label} is required').foldkit-mixins-formtakeswords: { submit, search, add, remove }
in place ofsubmitLabel,searchLabel,addLabelandremoveLabel, and
foldkit-mixins-crud'sfailedis text ('{message}'); both were functions in
places, which Foldkit refuses inside a placed view's inputs. The three shapes
share no key, so one object serves all.fillWordsis exported byfoldkit-form.
New dependency offoldkit-mixins-crud:foldkit-form. -
foldkit-form: forms pipe.Form.inputs,Form.checks,Form.messages,
Form.nestedandForm.debounceare steps that give a new form with the
option added to. A form now carriesname,options, andpipe. -
foldkit-entity:Entity.fields(entity, ...keys)gives field schemas to spread
into an input's struct, and anEntity.inputmapping may name a member by its
key ({ authorId: 'author' }). -
Typed ids reach the read. An Entity
Selectioncarries its Entity's id type
(a fourth, defaulted type parameter), andfoldkit-remote'sData.get/
Data.livetake it. Breaking for an Entity with a brandedidread with a
plain string.Crud.removertakesid: 'id'in place ofinputwhen the
mutation's input is just the id. -
foldkit-crud:Sort.make(columns)is a list's sort state written once: its
schema,toggle, and thesortinput a drawn table takes.
foldkit-remote-drizzle:sortTerms(sort, columns)reads that state into order
terms through the columns the server offers. -
foldkit-crud:Crud.actives({ ...pieces })gathers theactiveof every placed
editor, list, detail, andCrud.optionsresult, forData.wiring. -
foldkit-form: a nested key takes a form. Breaking.nested: { author: AuthorForm }
replaces the recursing options bag; the form that edits an author alone is the
one a post nests.form.nested.authoris that form, typed, and
form.row('author', id)gives its Message constructors wrapped for the row.
ExportsFormFor,NestedForms,RowHandle. -
One primitive for controls, and one for displays. Breaking. A form's
Controlis{ kind, draft, shown, searches, data, parse?, unparsed? }and a
CrudDisplayis{ kind, shown, data, text }; both were closed unions on
_tag.Input.kindandDisplay.kindmake a kind, and the shipped ones
(Input.Text,Input.Number,Input.RelationOne, ...,Display.Flag, ...)
are made with them. Narrow withKind.is(control)and readcontrol.data
where code readcontrol._tagand its fields.foldkit-mixins-formdraws by a
table ofrendererswhose defaults are entries;foldkit-mixins-crudtakes
renderersby kind besidecellsby column.FormMessages.notANumberis
unparsed.Input.search()is aControlChange, not a control. -
foldkit-mixins-crud0.1.0 (new). Draws aCrud.listas an accessible
table and aCrud.detailas a description list through Mixins slots. Opening
a row, sorting, and loading more are Messages the application passes in. -
Displayinfoldkit-crud: how a selected member shows, without a
renderer.list.columnsanddetail.fieldscarry adisplay, set with
Display.of, resolved from how a relation was selected or from the schema
otherwise;Display.showis the text any view can fall back on. A list also
exposes a type-onlyRow, and a detail a type-onlyValue. New dependency:
foldkit-metadata. -
Typed ids.
foldkit-entity: an Entity'sidfield keeps its type.
IdOf<E>,EntityRef<Name, Id>, a ref schema that is the id's own, and
Relation.inputtaking the target's id type. AnidofSchema.Stringtypes
as before; a branded one now rejects another Entity's id. Breaking only for
code that mapped a plainSchema.Stringkey to a relation whose target has a
branded id: give the key the id's schema. -
foldkit-remote-drizzle:bindstores an optionaloneon the target's table
({ foreignKey }), the inverse side of a one-to-one, and refuses a foreign key
that is not unique.ManyRelationgainssingle. -
foldkit-remote-drizzle: aquery'sorderBymay be a function of the input
and the principal, aswhereis, so a list sorts by what the user chose. A
computed order without the id is tie-broken by it. -
Pickers that search.
foldkit-form:Input.search()marks a relation
picker as searching; the form holds the text (Searched,form.search). The
form's Model gainssearches.foldkit-mixins-formdraws the search box
(searchslot,searchLabel).foldkit-crud:Crud.options(form, lists, { chosen })
keeps what a picker holds among its choices and returnsactiveto require
those rows; a placed list gainsrow(id),choiceOf(model, id)andowner. -
examples/entityshows a page of a relation (Entity.page) and a nested write
(Relation.nested, a form that nests a form, a server that writes both). -
foldkit-crud: an editor'sopenand newtarget(model)use the Entity's id
type; a remover takes the id type itsinputnames; a list's rows are typed
from its Selection with or without achoice.