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.
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 member | Storage |
|---|---|
| Field | the 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:
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.