Remote / foldkit-remote-drizzle

Binding a foldkit-entity domain

entity(name, table, …) makes the table the declaration. When the domain is declared with foldkit-entity, the Entity already says what each relation is, and bind says only how the database stores it:

Which to reach for. entity is the smaller of the two and exists so a field is not declared twice — once in the table and once in the domain. Reach for it when the database is the whole truth about a domain. Reach for bind when the domain is its own thing: relations and derived members it declares rather than infers, metadata other packages attach, and a query body. Only a foldkit-entity Entity has the addressable fields an Expr is built from, so Query.define cannot be written over an entity(…) binding — the same boundary foldkit-remote's own Entity.make has, for the same reason.

ts
import { Derived, Entity, Relation } from 'foldkit-entity'
import { bind, source } from 'foldkit-remote-drizzle'

const User = Entity.define('User', Schema.Struct({ id: Schema.String, name: Schema.String }))
const Post = Entity.define('Post', Schema.Struct({ id: Schema.String, title: Schema.String })).pipe(
  Entity.derived({ fanCount: Derived.make(Schema.Number) }),
)
const Blog = Entity.relate(
  { User, Post },
  { Post: { author: Relation.one(User), fans: Relation.many(User) }, User: {} },
)

const Db = bind(Blog, {
  User: { table: users },
  Post: {
    table: posts,
    relations: { author: { field: posts.authorId }, fans: { foreignKey: users.id } },
    derived: { fanCount: { relation: 'fans' } },
  },
})

source(Db.Post) // an ordinary binding
Entity memberStorage
Fieldthe column of the same name, or fields: { name: posts.title }
one relation{ field }: the foreign key on the owner's table
many relation{ foreignKey, localKey? } on the target's table (localKey defaults to the owner's id), or { through, localColumn, foreignColumn }
Derived{ relation, where? }: a count of a many relation

orderBy and where go on a many storage as they do on many(…).

A derived member is a count, { relation, where? }, the one kind this package computes. One it cannot compute is { supplied: true }: bind registers the member and reads nothing for it, and you wrap the binding's source to answer for that field yourself, as foldkit-cms-drizzle does for an entry's state.

A one may also be read from the other side, where the foreign key is a column of the target's table, as a member's profile points at its member:

ts
Member: { table: members, relations: { profile: { foreignKey: profiles.memberId } } },

It reads as the one ref, or null when no row points back, so the relation must be { optional: true }. One-to-one only holds if profiles.memberId is unique, so bind refuses a column that is neither .unique() nor the primary key. A unique index declared on the table is not visible on the column; vouch for it with assumeUnique: true. Such a relation cannot be windowed or counted.

Target and cardinality are never repeated, so they cannot disagree with the Entity. bind takes the whole Entity.relate result in one step, which lets two bindings point at each other; entity(…, { relations }) cannot, because a relation there needs its target binding to exist first.

Each result is the same EntityBinding entity returns, and the same Remote descriptor Entity.from gives the client, so register Db.Post (or Entity.from(Blog.Post)) in Remote.define and pass Db.Post to source and query.

The types and bind itself reject: a field with no column, a relation or derived member with no storage, a one stored as a many or the reverse, a count over a one relation, a required one over a nullable column (declare the relation { optional: true }), and a one read from the target's table whose foreign key is not unique.

bind also refuses a column that plainly cannot hold its field: text under a number field (a Postgres numeric reads as text), a number under a flag, and a nullable column under a field whose schema admits neither null nor undefined. It compares only what both sides state plainly. A schema that transforms (Schema.NumberFromString), a mixed union, a struct, and a custom, JSON or date column all pass unchecked, so the check never refuses a mapping that could work.