Traits
Columns applied to every table that does not opt out.
traits is a list. Schema traits apply first. A table's own list applies after them. An omitted list and [] add nothing. Import okmodel/traits to load the columns. A schema that never imports a trait does not carry that code.
timestamps(), archivable(), versioned(), and sortable() are the built-ins. A trait you write uses the same list. The builders are on traits. archivable() and sortable() rewrite uniques after tenancy has widened them.
omitted
No list, or [], leaves each table's own columns.
import { schema, table, t } from "okmodel/pg";
const notes = table("notes", {
id: t.identity(),
title: t.text(),
});
export default schema({
tables: [notes],
});list
Each entry is a trait. timestamps() adds createdAt and updatedAt. Both are timestamptz not null default now(), guarded, and sealed. Insert leaves them to the default. Update sets updatedAt and leaves createdAt.
import { schema, table, t } from "okmodel/pg";
import { timestamps } from "okmodel/traits";
const notes = table("notes", {
id: t.identity(),
title: t.text(),
});
export default schema({
tables: [notes],
traits: [timestamps()],
});trigger
{ enforce: "trigger" } also writes updatedAt in the database. The catalog gains okm_touch_<column> and one before-update trigger per table, named <table>_touch. <column> and <table> are the SQL names. With casing "snake", the function is okm_touch_updated_at.
import { schema, table, t } from "okmodel/pg";
import { timestamps } from "okmodel/traits";
const notes = table("notes", {
id: t.identity(),
title: t.text(),
});
export default schema({
tables: [notes],
traits: [timestamps({ enforce: "trigger" })],
});OKM1060
Another key is timestamps() option extra is not supported. Accepted options: enforce. Another value is timestamps() enforce database must be trigger.
opt out
omitDefaults is a reason. It drops the schema list and keeps traits declared on that table. An empty reason, or one that is only spaces, is refused.
import { schema, table, t } from "okmodel/pg";
import { timestamps } from "okmodel/traits";
const notes = table("notes", {
id: t.identity(),
title: t.text(),
});
const sessions = table(
"sessions",
{ id: t.identity(), token: t.text() },
{ omitDefaults: "sessions are not stamped" },
);
export default schema({
tables: [notes, sessions],
traits: [timestamps()],
});OKM1060
A value that is not a list is schema() traits must be a list of traits. The same on a table is Table notes traits must be a list of traits. An empty reason is Table sessions omitDefaults needs a reason.
together
Schema traits apply to notes. tasks adds sortable after them, so it also has position. sessions opts out, so it has neither createdAt nor label.
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { sortable, timestamps, trait } from "okmodel/traits";
const labeled = trait("labeled", {
fields: { label: t.text().default("note") },
});
const notes = table("notes", {
id: t.identity(),
title: t.text(),
});
const tasks = table(
"tasks",
{ id: t.identity(), title: t.text(), laneId: t.text() },
{ traits: [sortable("laneId")] },
);
const sessions = table(
"sessions",
{ id: t.identity(), token: t.text() },
{ omitDefaults: "sessions are not stamped" },
);
const app = schema({
tables: [notes, tasks, sessions],
traits: [timestamps(), labeled],
});
export async function open(url: string, title: string, token: string) {
await using db = connect(url, { schema: app });
const note = await db.notes.insert({ title });
const session = await db.sessions.insert({ token });
return { createdAt: note.createdAt, token: session.token };
}OKM1012
A column the table already has is Trait labeled adds notes.label, and notes already declares it. A column another trait already added is Trait labeled adds notes.label, and trait timestamps already adds it.
Create your own
timestamps(), archivable(), versioned(), and sortable() are the built-ins above. A trait you write is trait(name, { fields }) from okmodel/traits, the same function those built-ins call. Put it on this list and it is copied onto every table that does not opt out. The field options are on traits.
Simple
One text column with a default. Insert omits it, and the insert is awaited: await using closes the client when the function returns, so a returned promise would close it before the row is written. okm generate adds "label" text not null default 'note' on notes.
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
import { trait } from "okmodel/traits";
const labeled = trait("labeled", {
fields: { label: t.text().default("note") },
});
const notes = table("notes", {
id: t.identity(),
title: t.text(),
});
export const app = schema({
tables: [notes],
traits: [labeled],
});
export async function add(url: string, title: string) {
await using db = connect(url, { schema: app });
return await db.notes.insert({ title });
}Advanced
stamped seals label, so input cannot set it, and touches labeledAt, so an update sets that column to now(). timestamps() is the same list, applied first, so the table also has createdAt and updatedAt.
import { schema, table, t } from "okmodel/pg";
import { timestamps, trait } from "okmodel/traits";
const stamped = trait("stamped", {
fields: {
label: t.text().default("note"),
labeledAt: t.timestamptz().defaultSql("now()"),
},
touch: ["labeledAt"],
sealed: ["label"],
});
const notes = table("notes", {
id: t.identity(),
title: t.text(),
});
export default schema({
tables: [notes],
traits: [timestamps(), stamped],
});OKM1012
A column the table already declares is Trait stamped adds notes.label, and notes already declares it.