Entity / foldkit-entity
Rows of several shapes: variants
Experimental (#357): the Entity, its Selections and
Expr. Remote's store, forms and guards read the stored row for now.
A domain search answers Available, with a price and a term, or
Unconfirmed, with neither. Entity.defineVariants declares that as one
Entity whose row is one of its cases, each case's fields by its tag, the shape
Schema.TaggedUnion takes:
const DomainName = { id: Schema.String, name: Schema.String }
const Domain = Entity.defineVariants('Domain', {
Available: { ...DomainName, price: Schema.Finite, years: Schema.Int },
Unconfirmed: DomainName,
})
// The row: Effect's tagged union of the cases, with its own `match`.
const priceOf = (row: typeof Domain.variants.Type) =>
Domain.variants.match(row, { Available: ({ price }) => price, Unconfirmed: () => 0 })One Entity has two views of one row:
row Domain.variants { _tag: 'Available', id, name, price, years }
| { _tag: 'Unconfirmed', id, name }
stored row Domain.fields { _tag, id, name, price | null, years | null }- The stored row is what
fieldsandschemadescribe:_tag, every field all cases have as declared, and every other field nullable, since a row of another case holds null there (a field whose stored form holds null already,Schema.OptionFromNullOr, stays as it is). It is what every interpreter reads, so a table, a Remote read and a form work as they do for any Entity. - The row is
Domain.variants, branched on with itsmatch, never by reading which fields are null.Entity.variantsOf(Domain)holds the codec between the two (stored): a stored row, encoded, decodes to its case, and one holding another case's field is refused. - Each case says what it holds. A case with fields of its own gets an
invariant (
variant_Available: an Available holdspriceandyears;variant_Unconfirmed: an Unconfirmed holds none of them), soEntity.judgeand a form refuse a row that mixes cases, andfoldkit-remote-drizzlewrites each as a CHECK.
A key in several cases is one stored field, so it is declared once and spread
into each, as DomainName is. Refused where declared: fewer than two cases, a
case declaring _tag, an optional key, and a key with two different schemas.
Expr.is narrows a predicate to a case. Given a function, it hands it the
case's fields typed as the case holds them, never null:
const CheapDomains = Query.from(Domain).pipe(
Query.where(Expr.is(Domain, 'Available', ({ price }) => Expr.lt(price, 20))),
)It is _tag = 'Available' and the predicate, so it is false, not unknown, on
an Unconfirmed: Expr.not of it keeps every Unconfirmed, where
Expr.not(Expr.lt(Domain.fields.price, 20)) drops them, since their stored
price is null. Given a one relation to a variant Entity, it reads the
case of the row the relation reaches, each field as a path (Expr.is(
Cart.relations.domain, 'Available', …)). Several cases at once are
Expr.in(Domain.fields._tag, ['Available', 'Unconfirmed']).
A Selection that selects _tag reads the row as its case: its schema is a
tagged union whose cases hold the selected fields they have, typed as the
case holds them, and every selected relation and derived member.
const Quote = Entity.select(Domain, { _tag: true, name: true, price: true })
type Quote = Selected<typeof Quote>
// { _tag: 'Available', name: string, price: number } | { _tag: 'Unconfirmed', name: string }
const shown = (quote: Quote) =>
Quote.schema.match(quote, { Available: ({ price }) => `$${price}`, Unconfirmed: () => 'taken?' })Without _tag a Selection is the stored row's Struct, and a field only some
cases have is refused there, at the type and where it is written: nothing
would say which case a row is.