Skip to content

v0.8.0

Choose a tag to compare

@github-actions github-actions released this 21 Sep 16:13
· 3457 commits to main since this release

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. isNull and isNotNull are 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. contains treats 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's like ignores 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, so foldkit-cms-drizzle
    registers it as query(descriptor, { entity }) and the and(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
    by visible, 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 : isNull produced.

  • 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-drizzle against a real SQLite, and require the same ids in the
    same order. It follows SQL, not JavaScript — null = null is 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 with Query.define needs only query(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 computed orderBy does, since a body says what the
    rows mean and not how a cursor walks them; a literal orderBy given at
    registration is left as written. The compiled
    predicates are conjoined with the server's own where and the binding's
    visible rule, so a body is the application's question and never widens what a
    principal may see; an orderBy given at registration replaces the body's, since
    a connection pages on exactly one order. Query.make is unchanged and still
    requires an orderBy.

  • foldkit-cms: Cms.bySlug is declared by what it means. Its body is
    eq(the slug field, input.slug) ordered by id, so foldkit-cms-drizzle
    registers the source without restating the where or 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, ref and connection identity
    Query.make gives — carrying its body besides, 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
    from Query.make has no body at all. The body is built once: input.slug
    is a placeholder typed from Input, never the value, so a condition that
    depends on what was passed is a comparison over the placeholder. A body needs
    a foldkit-entity Entity (Entity.define), which is what has addressable
    fields; this package's own Entity.make describes a field as the schema of
    its value and has nothing to point at. Input must 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 with pipe: Query.from(Post).pipe(published, newest). Each step is
    a new frozen value and performs no work. Two wheres conjoin and two
    orderBys append
    — neither replaces, so piping a fragment can only narrow a
    query, and Query.unfiltered / Query.unordered are the only ways back. A
    predicate or ordering term over a different Entity than Query.from is
    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 three wheres. foldkit-remote spreads
    these into its own Query, so Query.make and Query.from come 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; an Expr says something about one row of it.
    Expr.eq compares 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.desc is 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. Only eq exists: 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 practice Agent.when({ projection: Data.confirmed(...) }).
    There is deliberately no Data.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/Loading reach loading, Ready/Refreshing reach data, and a
    Failed still carrying the value it had reaches data too, so a failed read
    does not throw away what the reader was looking at. notFound is its own
    branch and not optional. The data branch is told which it got through one
    Freshness tag (Fresh / Refreshing / Stale with its error), not a pair
    of booleans that could claim both. match is 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: a networkOnly entry observing an invalidated
    connection plans the same query either way. refresh moves from a Struct to a
    runtime field in the Model schema, beside loading and gaps. 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,
    beside mutations.pending for 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.md says 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. FormModel gains subject, a plain record set with
    Message.About({ subject }) and read with form.subject(model), and a check's
    context gains it. It changes no draft, answers no submit, and survives fill
    and Reset. Cms.editor sends 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.make and the Form.checks step both read a check's requirement (R)
    as never, because Effect's R is not an inference site a mapped type wins —
    so a check needing RemoteClient was refused by the shape meant to accept it,
    and the documented "the check's requirements become the Bundle's" was
    unreachable. Form.checks now 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's bySlug
    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;
    Conflict with reload and overwrite; discard. End to end tests drive it
    against the real server over SQLite.

  • Scheduling and archiving. CmsSchedule promises a draft that would publish
    now, CmsUnschedule takes it back, and cms.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. CmsArchive also takes what can be hidden off show. The editor gains
    ScheduleAsked({ at }), which submits and saves first, and UnscheduleAsked,
    ArchiveAsked, UnarchiveAsked. cms_drafts gains scheduled_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
      allow would not let schedule.
    • Correctness: Transaction.statements takes turns at its one connection; a
      write arriving between another request's begin and rollback was 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 in foldkit-form);
      follows a previewed post to the row publishing gives it; and gains flush,
      for an author who leaves within the rest of their last edit.
  • A server's word about one key, on that key. foldkit-form gains
    Message.Refused({ key, error }): the key reads invalid with that reason,
    keeps what was typed, and clears on the next edit. Checked could not serve,
    since it only answers a check the form itself started. Cms.editor uses it, so
    a publish refused for a taken address marks the address, with nothing to wire.

  • foldkit-cms: Opened, beside Editing. Editing meant both "open and
    idle" and "edited since the last save", so no application could word its status
    truthfully. Editing now means there are unsaved edits, Opened means the form
    is as it was found, and a draft the form was filled from reads Saved.

  • 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-cms gains
    Cms.editorView and the editor's pageId, which the browser mode needed, and
    annotates an entry's id as hidden and its state with a title, so a worklist
    shows Title and State rather than a raw uuid and a lowercase header.

  • In-app preview. foldkit-remote gains Data.overlay(model, id, operations)
    and Data.lift(model, id) (OverlayShown, OverlayLifted): optimistic
    operations shown with no request behind them. A content type's preview says
    how a value would look in the store, and the editor's PreviewShown lays what
    is in the form over it, edit by edit, until PreviewHidden.

  • Restoring. CmsRestore makes a revision's value the working copy and
    publishes nothing; the editor's RestoreAsked({ revision }) shows it in the form.

  • foldkit-cms: kinds and their renderers. Cms.slug(from) and
    Cms.dateTime() for a form's inputs; Cms.Display.State and
    Cms.Display.Moment, which Cms.Entities are annotated with;
    Cms.controlRenderers() and Cms.displayRenderers() to spread beside the
    mixins' own. foldkit-cms now depends on foldkit-crud.

  • Breaking, foldkit-cms and foldkit-cms-drizzle: CmsSaveDraft's entry
    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. CmsEntry gains revision, a column of
    cms_entries, which a publish compares and sets inside its transaction. Every
    operation patches the entry with its state. newId is 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.refresh of 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-drizzle 0.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) and CmsDiscardDraft; the Cms.Entries worklist; 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's visible rule, which
    CmsServer.make requires of a type that can be unpublished. CmsPublish runs
    the application's own create or update handler with the draft's value,
    inside a transaction (Transaction.statements or Transaction.drizzle) that
    also shows the row, appends the revision and removes the draft; a publish made
    from an older revision is a conflict. CmsUnpublish hides the row and keeps it.
    A visitor is refused before anything is looked up. A slug role adds a
    <name>BySlug query (Cms.bySlug), and a publish to a taken slug fails as
    CmsSlugTaken: <key>: ..., whether the check or the unique index caught it.
    foldkit-cms gains Cms.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 a one ref (which reads null), 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, through policies.

  • foldkit-cms 0.1.0 (new): the pure core. Cms.roles marks the members of
    an Entity that play a CMS part (label, slug, published) as metadata, read
    with Cms.rolesOf. Cms.content declares a type of content: its Entity, its
    form, the application's own publish operations, its words. Cms.Entities
    (Entry, Draft, Revision) and Cms.Operations are ordinary Entities and
    Remote mutations. Cms.state and Cms.offers derive 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 gains touched; a control gains follows; a form gains
    isFollowing, and a drawn field is told following. The first step of
    the CMS design.

  • foldkit-remote: Selection.from compiles an Entity Selection once. Data.get
    and Data.query are 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-remote and foldkit-remote-server: a relation may be read whole and
    by the page at once.
    A page of a whole list (Entity.page, or
    Selection.connection over 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; RemoteServer reads 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 in fields, windows and relations. A request may page one
    relation at most four ways, since each is a source read the client names, and
    Entity.make refuses a field whose name contains @. Both packages must be
    upgraded together: an older server does not know an alias. RELATION_ALIAS,
    relationAlias and aliasedField are exported.

  • Words are text. Breaking. FormMessages entries may be text with blanks
    ('{label} is required'). foldkit-mixins-form takes words: { submit, search, add, remove }
    in place of submitLabel, searchLabel, addLabel and removeLabel, and
    foldkit-mixins-crud's failed is 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. fillWords is exported by foldkit-form.
    New dependency of foldkit-mixins-crud: foldkit-form.

  • foldkit-form: forms pipe. Form.inputs, Form.checks, Form.messages,
    Form.nested and Form.debounce are steps that give a new form with the
    option added to. A form now carries name, options, and pipe.

  • foldkit-entity: Entity.fields(entity, ...keys) gives field schemas to spread
    into an input's struct, and an Entity.input mapping may name a member by its
    key ({ authorId: 'author' }).

  • Typed ids reach the read. An Entity Selection carries its Entity's id type
    (a fourth, defaulted type parameter), and foldkit-remote's Data.get /
    Data.live take it. Breaking for an Entity with a branded id read with a
    plain string. Crud.remover takes id: 'id' in place of input when 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 the sort input 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 the active of every placed
    editor, list, detail, and Crud.options result, for Data.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.author is that form, typed, and
    form.row('author', id) gives its Message constructors wrapped for the row.
    Exports FormFor, NestedForms, RowHandle.

  • One primitive for controls, and one for displays. Breaking. A form's
    Control is { kind, draft, shown, searches, data, parse?, unparsed? } and a
    Crud Display is { kind, shown, data, text }; both were closed unions on
    _tag. Input.kind and Display.kind make a kind, and the shipped ones
    (Input.Text, Input.Number, Input.RelationOne, ..., Display.Flag, ...)
    are made with them. Narrow with Kind.is(control) and read control.data
    where code read control._tag and its fields. foldkit-mixins-form draws by a
    table of renderers whose defaults are entries; foldkit-mixins-crud takes
    renderers by kind beside cells by column. FormMessages.notANumber is
    unparsed. Input.search() is a ControlChange, not a control.

  • foldkit-mixins-crud 0.1.0 (new). Draws a Crud.list as an accessible
    table and a Crud.detail as a description list through Mixins slots. Opening
    a row, sorting, and loading more are Messages the application passes in.

  • Display in foldkit-crud: how a selected member shows, without a
    renderer. list.columns and detail.fields carry a display, set with
    Display.of, resolved from how a relation was selected or from the schema
    otherwise; Display.show is the text any view can fall back on. A list also
    exposes a type-only Row, and a detail a type-only Value. New dependency:
    foldkit-metadata.

  • Typed ids. foldkit-entity: an Entity's id field keeps its type.
    IdOf<E>, EntityRef<Name, Id>, a ref schema that is the id's own, and
    Relation.input taking the target's id type. An id of Schema.String types
    as before; a branded one now rejects another Entity's id. Breaking only for
    code that mapped a plain Schema.String key to a relation whose target has a
    branded id: give the key the id's schema.

  • foldkit-remote-drizzle: bind stores an optional one on the target's table
    ({ foreignKey }), the inverse side of a one-to-one, and refuses a foreign key
    that is not unique. ManyRelation gains single.

  • foldkit-remote-drizzle: a query's orderBy may be a function of the input
    and the principal, as where is, 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 gains searches. foldkit-mixins-form draws the search box
    (search slot, searchLabel). foldkit-crud: Crud.options(form, lists, { chosen })
    keeps what a picker holds among its choices and returns active to require
    those rows; a placed list gains row(id), choiceOf(model, id) and owner.

  • examples/entity shows 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's open and new target(model) use the Entity's id
    type; a remover takes the id type its input names; a list's rows are typed
    from its Selection with or without a choice.