Remote / Declared writes

A sixty-second path: a precondition

A product is published only from a draft. The declaration says so; the writer checks it in the statement that writes.

ts
import { Schema } from 'effect'
import { Entity, Expr, Write } from 'foldkit-entity'
import { Mutation } from 'foldkit-remote'

const Product = Entity.define(
  'Product',
  Schema.Struct({
    id: Schema.String,
    name: Schema.String,
    status: Schema.Literals(['draft', 'published', 'archived']),
    stock: Schema.Number,
    revision: Schema.Number,
  }),
)

const Publish = Mutation.write(
  'Publish',
  Write.update(
    Entity.input(Product, Schema.Struct({ id: Schema.String, status: Schema.Literal('published') })),
    { id: 'id', when: () => Expr.eq(Product.fields.status, 'draft') },
  ),
)

On the server, the write is served with no handler:

ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
import { bind, writer } from 'foldkit-remote-drizzle'
import { RemoteServer } from 'foldkit-remote-server'

const products = sqliteTable('products', {
  id: text('id').primaryKey(),
  name: text('name').notNull(),
  status: text('status').notNull(),
  stock: integer('stock').notNull(),
  revision: integer('revision').notNull(),
})
const Db = bind({ Product }, { Product: { table: products } })

const server = RemoteServer.make({
  entities: [],
  mutations: [RemoteServer.write(Publish, writer(Db.Product))],
})

A draft is published. An archived product is refused as Unmet, and nothing is written. A published one is answered as written and does not change.

What the calls mean

  • Write.update(input, { id, when }) is a declaration. It runs nothing and reads nothing. when is built once, at definition, into a query body over the Entity; a when over another Entity is refused there.
  • Schema.Literal('published') is how a write sets a constant. The server decodes the input before anything else, so a client cannot send another value. There is no separate constant-assignment API, because this is one.
  • Mutation.write types the refusal. A write with a when may be refused as Unmet as well as Conflict; one without has no Unmet arm to match. An insert expects no revision, so it is refused as Taken (its id is a row's), never as Conflict.
  • writer(Db.Product) is where I/O happens: one update … where id = ? and <when>, then, if no row changed, one read to say why. It checks no rules; the guard, below, does.