Entities
Download .mdEntities
An entity is a kind of object you store — product, order, customer. You
design it once; Weave maps it to Postgres and exposes it as objects everywhere. One
file, one entity, a default export:
// weave/entities/product.ts
import { defineEntity, text, int4, bool } from "@mauroandre/weave-sdk";
export default defineEntity("product", {
name: text().notNull(),
price: int4(),
active: bool().default(true),
});Fields
Fields are built from catalog types and chained modifiers:
text() // a string column
int4() // a 32-bit integer
text().notNull() // required
text().unique() // unique constraint
text().index() // indexed
int4().default(0) // default valueCommon types: text · int4 · int8 · float8 · numeric · bool ·
timestamptz · uuid · jsonb. Every entity gets id, createdAt and
updatedAt for free.
Naming — camelCase in, snake_case in the database
Write field names in camelCase, the way you would in TypeScript. Weave stores the column in snake_case — the Postgres convention — automatically:
firstName → column first_name
phoneNumber → column phone_numberYou always read and query with the camelCase name ({ firstName: "Ada" }); the
snake_case column stays under the hood. Whatever style you type in the GUI (First Name, first_name) converges on the same camelCase field.
The entity name follows the exact same rule: defineEntity("backupStorages", …) →
table backup_storages in Postgres, while the SDK keeps the logical name everywhere you
touch it — accessor weave.backupStorages, generated file backupStorages.ts.
Owned objects — composition
An owned object lives inside its parent (its own child table, cascade-deleted with the parent). Use it for parts that have no life of their own:
import { defineEntity, text, int4, owned, array } from "@mauroandre/weave-sdk";
export default defineEntity("order", {
ref: text().notNull().unique(),
// a single owned object (1:1)
shipping: owned({ address: text().notNull(), city: text().notNull() }),
// a list of owned objects (1:N)
items: owned(array({ sku: text().notNull(), qty: int4().notNull() })),
});Mirror — snapshot another entity's shape
mirror(base) copies another entity's fields into an owned child. The rows are a
snapshot: an item already written keeps its values even if the base row later changes.
The shape is not — it re-resolves from the live base on every push, so adding a field
to the base adds it to the mirror too. Add local fields alongside it. Like reference,
it takes the entity, not its name:
import { defineEntity, text, int4, owned, array, mirror } from "@mauroandre/weave-sdk";
import product from "./product.js";
export default defineEntity("order", {
code: text().notNull(),
// each item snapshots `product` (name, price, …) and adds a local `quantity`:
items: owned(array(mirror(product, { quantity: int4().notNull() }))),
});owned(mirror(base)) for 1:1, owned(array(mirror(base))) for a 1:N set. The item's
type is the base's fields plus your extras (on a name collision, your local field
wins), so create({ items: [{ name, price, quantity }] }) is fully typed. Use it for
line-item snapshots whose values must stay frozen even if the source product later
changes.
References — association
A reference points at another independent entity — shared, never owned:
import { defineEntity, reference, array } from "@mauroandre/weave-sdk";
import category from "./category.js";
import tag from "./tag.js";
export default defineEntity("product", {
// N:1 — reads `categoryId`, and `category` when expanded
category: reference(category),
// N:N — writes via `tagsIds`, reads `tags` when expanded
tags: reference(array(tag)),
});The difference in one line: owned is part of you and cascades; a reference is someone else you point at.
References to self, or in a cycle
An entity can point at itself, and two entities can point at each other. Both
need the target resolved lazily — the plain reference(x) form needs x to already
exist when the file loads, which neither a self-reference nor a cycle can guarantee.
Self-reference — use self():
import { defineEntity, reference, array, self, text } from "@mauroandre/weave-sdk";
export default defineEntity("employee", {
name: text().notNull(),
manager: reference(self()), // N:1 — a manager, who is another employee
reports: reference(array(self())), // N:N — this employee's direct reports
});Circular reference between two entities — defer the target with a thunk, () => other:
// company.ts
import users from "./users.js";
export default defineEntity("company", {
name: text().notNull(),
lead: reference(() => users), // company → users
});
// users.ts
import company from "./company.js";
export default defineEntity("users", {
email: text().notNull(),
company: reference(() => company).notNull(), // users → company
});The thunk defers resolving the target until push time, so the circular import between
the two files is harmless. A plain reference(users) here would read undefined for
whichever file loads second.
The rule: reference(x) by default; self() only for self-references; () => x only
for cycles. weave gen writes these forms for you, and the visual
designer offers a self option in the reference picker — so a schema built either way
round-trips.
One trade-off, TypeScript only: when you expand a self() or () => x reference, the
nested object comes back loosely typed (a precise type would form a cycle the compiler
can't resolve). The data is exactly the same — expand, where, and the …Id fields
all work; only the static shape of that one expanded field is wider. Plain
reference(x) keeps its precise expand type.
Composite unique & index
text().unique() covers a single column. For a multi-column constraint — the kind
a rollup key or a natural key needs — pass a third argument to defineEntity:
import { defineEntity, text, reference } from "@mauroandre/weave-sdk";
import stack from "./stack.js";
export default defineEntity(
"registryEntry",
{
slugName: text().notNull(),
stack: reference(stack),
host: text().notNull(),
},
{
unique: [["slugName", "stack"]], // the pair is unique together
index: [["host", "slugName"]], // a composite (non-unique) index
},
);Each group lists field names: a column maps to its column; a to-one reference
maps to its foreign key (stack → stack_id). Adding a unique group to an entity
that already has duplicate rows is a blocked change — resolve the duplicates first
(the same review gate as any migration).
Scalar arrays
Wrap a column in array() for a text[] / int4[] column:
import { defineEntity, text, array } from "@mauroandre/weave-sdk";
export default defineEntity("article", {
title: text().notNull(),
keywords: array(text()), // text[] — defaults to []
});Once your entities exist, learn to query them, or push them to the server with the CLI.