Entity / Operators

A sixty-second path: a rule named once

A catalog lists a product once it has a publication date. That rule is asked in the public list, in a visitor's Read rule and in reports. Name it:

ts
import { Schema } from 'effect'
import { Collation, Entity, Expr, Query } from 'foldkit-entity'

const Product = Entity.define(
  'Product',
  Schema.Struct({
    id: Schema.String,
    upc: Schema.String,
    label: Schema.String,
    cents: Schema.Int,
    publishedAt: Schema.NullOr(Schema.String),
  }),
).pipe(Entity.annotateMembers({ upc: Collation.of(Collation.binary) }))
const { fields } = Product

const listed = Expr.operator('acme.listed', {
  input: [Schema.NullOr(Schema.String)],
  output: Schema.Boolean,
  definedAs: publishedAt => Expr.isNotNull(publishedAt),
})

const Listed = Query.from(Product).pipe(Query.where(listed(fields.publishedAt)))
Query.show(Listed)
// FROM Product
// WHERE acme.listed(Product.publishedAt)
Query.operators(Listed) // ['acme.listed']

What the calls mean

  • Expr.operator declares; it does nothing. No SQL, no evaluator, no registration. The id is namespaced (acme.; foldkit. is reserved) and has a version, 1 by default, which changes when what it means does.
  • definedAs builds an expression from the call's arguments, once per call. Every interpreter runs that expression, so Listed compiles to the same SQL as Expr.isNotNull(fields.publishedAt) written out, and the client judges it the same way. A boolean output makes a call a predicate, usable in and, or, not, a guard or an invariant.
  • It reads only its arguments. A definedAs that closed over a field instead of taking it is refused when called, since nothing asking what a body reads would see the field.
  • What it adds is a name. Query.show prints the call, and Query.operators lists what a body calls, so a reviewer, a log or a description says "listed" where it would have said "publishedAt is not null".

foldkit-cms ships one: Cms.published(entity), over its published role.