Visibility
Three flags. They do not change the SQL type.
Where this fits
The SQL type stays. Hidden, sensitive, and guarded change the row the client sees and the fields a write may set. Validation rejects a value the type still accepts.
None of the three flags is stored in the catalog, so none of them changes a migration.
Hidden
.hidden() takes one option, filterable. Omit it and the column is off the row. true opens where and orderBy in the type and at runtime. The column stays off the row either way. Insert and update can still set it. A value bound for the column is [redacted] in inspect() and explain(), on reads and writes.
.hidden()
select takes keys of the row, so a typed call cannot name the field there. At runtime where and orderBy refuse it. The type refuses that key on find, one, and page too, including beside a valid key, inside or(), and in an include's where or orderBy. page also refuses an unknown option. A root select list that names it returns it. An include drops it silently, including when that list names it. filters() refuses it in allow, sort, and relations. groupBy, bucket, sum, avg, min, and max refuse it. Search refuses it and says to call hidden({ filterable: true }).
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
const users = table("users", {
id: t.identity(),
email: t.text(),
passwordHash: t.text().hidden(),
});
const app = schema({ tables: [users] });
export async function register(url: string, email: string, passwordHash: string) {
await using db = connect(url, { schema: app });
const row = await db.users.insert({ email, passwordHash });
return row.email;
}OKM1120
where and orderBy refuse the column. The fix says to call hidden({ filterable: true }). groupBy, bucket, sum, avg, min, max, and search refuse it too.
OKM1123
filters() refuses the field in allow, sort, or relations. See filters.
Roles
The column stays out of a reporting role's select unless roles.columns names it.
filterable
{ filterable: true } is the only value that changes the flag. Omit it and where and orderBy stay refused. where, orderBy (including relevance() on find and one), and search accept the column, in the type and at runtime. page() refuses relevance() because the cursor cannot encode a rank. It stays off the row, so select still cannot name it. filters(), groupBy, bucket, sum, avg, min, and max still refuse it. The reporting role still omits it.
.nullable() in the sample lets insert omit the field. filterable does not.
page() does not put this column's value in the cursor. The cursor carries the primary key, and the next page reads the sort value back under the same tenant and archive predicates. A row that is no longer visible is The cursor is no longer valid.
import { table, t } from "okmodel/pg";
export const users = table("users", {
id: t.identity(),
resetCode: t.text().nullable().hidden({ filterable: true }),
});Sensitive
.sensitive() takes no argument. The column stays on the row, and a read returns the stored value. inspect() on a read or a write, explain(), logs, errors, and OpenTelemetry spans replace that value with [redacted] or omit it. explain() sends the real parameter so Postgres picks the same plan, then replaces the value in params and in the plan text. An ordinary column stays as it is. onQuery with params: true and errors.includeValues stay opt-in. A page() cursor does not carry the value either. When orderBy names the column, the cursor carries the primary key and the next page reads the sort value back. A unique, foreign-key, check, generic driver, or aborted-connection failure on a table that has a sensitive column keeps the value out of error.cause as well: the cause carries only safe fields (code, sqlstate, constraint, table, column names), never detail, parameters, args, where, or internalQuery. That includes postgres.js failures that omit table on the error while still attaching parameters / args on the cause. Bound parameters stay off error.cause for every table unless connect({ errors: { includeValues: true } }). A sensitive column stays redacted even with that opt-in. A codec rejection says Field token was rejected. The same column can also be hidden.
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
const sessions = table("sessions", {
id: t.identity(),
token: t.text().sensitive(),
});
const app = schema({ tables: [sessions] });
export async function token(url: string, id: string) {
await using db = connect(url, { schema: app });
const row = await db.sessions.one({ where: { id } });
return row?.token;
}Guarded
.guarded() takes no argument. The write option is allow, on that call.
Closed
Insert and update omit the field. A value that arrives is OKM1190.
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
const accounts = table("accounts", {
id: t.identity(),
email: t.text(),
role: t.text().guarded(),
});
const app = schema({ tables: [accounts] });
export async function register(url: string, email: string) {
await using db = connect(url, { schema: app });
return await db.accounts.insert({ email });
}OKM1190
Input cannot set a guarded field. Pass { allow } with the field name, or leave it out.
allow
{ allow: ["role"] } opens role for that call. allow is a list of field names on the table. A value that is not a list, or a name the table does not have, is OKM1120.
import { connect } from "okmodel/pg/postgresjs";
import { schema, table, t } from "okmodel/pg";
const accounts = table("accounts", {
id: t.identity(),
email: t.text(),
role: t.text().guarded(),
});
const app = schema({ tables: [accounts] });
export async function promote(url: string, email: string) {
await using db = connect(url, { schema: app });
return await db.accounts.insert({ email, role: "owner" }, { allow: ["role"] });
}OKM1120
allow must be a list of field names on the table.
Stays closed
A sealed trait field stays OKM1190. The message says the trait sets it. Insert of the tenant key is OKM1190. An update or a where on that key is OKM1704. allow does not open either.
Filled elsewhere
timestamps() and archivable() mark their columns guarded and sealed. Input cannot set those columns.