Tenancy
Tenant keys on every table that does not opt out.
tenancy is columnTenancy(), compositeTenancy(), or rlsTenancy() from okmodel/tenancy. Omitted, no table gains a key. Importing the module is what loads the predicate.
Each key is a guarded uuid. Insert fills it from for(). Reads and writes filter on it. Input cannot set it. A unique and the primary key include every key. An index must start with the key. global("reason") and via() are the table marks. Both are on tenancy.
omitted
No tenant column, and for() is not on the client. A table that still sets tenancy is refused.
import { schema, table, t } from "okmodel/pg";
const notes = table("notes", {
id: t.identity(),
title: t.text(),
});
export default schema({
tables: [notes],
});OKM1060
Table notes sets tenancy, and the schema does not. A value that is not columnTenancy(), compositeTenancy(), or rlsTenancy() is schema() tenancy must be columnTenancy() from okmodel/tenancy.
column
columnTenancy({ key, type: "uuid" }) adds one key. type is "uuid".
import { columnTenancy } from "okmodel/tenancy";
import { schema, table, t } from "okmodel/pg";
const notes = table("notes", {
id: t.identity(),
title: t.text(),
});
export default schema({
tables: [notes],
tenancy: columnTenancy({ key: "tenantId", type: "uuid" }),
});for({ tenantId }) opens a client for one tenant. The value is a uuid. unscoped("reason") drops the predicate. Insert still needs for(). The root client keeps a global table and hides a tenant table.
import { connect } from "okmodel/pg/postgresjs";
import { columnTenancy, global } from "okmodel/tenancy";
import { schema, table, t } from "okmodel/pg";
const plans = table(
"plans",
{ id: t.identity(), code: t.text() },
{ tenancy: global("plans are shared") },
);
const notes = table("notes", {
id: t.identity(),
title: t.text(),
});
const app = schema({
tables: [plans, notes],
tenancy: columnTenancy({ key: "tenantId", type: "uuid" }),
});
export async function add(url: string, tenantId: string, title: string) {
await using db = connect(url, { schema: app });
await db.plans.find({ limit: 1 });
return db.for({ tenantId }).notes.insert({ title });
}OKM1060
columnTenancy() needs { key, type }. Another type is columnTenancy() type text must be uuid. An empty key is columnTenancy() key must be a field name. A table mark that is neither global("reason") nor via() is Table notes tenancy must be global("reason") or via(). An empty reason is global() needs a reason. A global unique with no reason is Unique notes.email sets global and needs a reason. set null on a tenant foreign key is Foreign key notes.authorId includes tenantId, so set null cannot clear it. Accepted actions: cascade, restrict, no action.
OKM1012
Table notes already declares tenantId, which tenancy adds. Rename the field or drop it from the table.
OKM1190
Insert that names the key is Field notes.tenantId is the tenant key. The scope sets it. Input cannot.
OKM1704
An update is Field notes.tenantId is the tenant key. An update cannot change it. A filter is Field notes.tenantId is the tenant key. A filter cannot set it.
OKM1701
No scope is Table notes is tenant-scoped. Call for({ tenantId }) or unscoped("reason"). Insert without one is insert on notes has no tenant. Call for({ tenantId }) so the scope can set it. for() needs { tenantId }, and the value must be a uuid: for() needs { tenantId } as a uuid. Another field is for() accepts tenantId. organizationId is not the tenant key. An empty reason is unscoped() needs a reason.
OKM1705
A global table that references a tenant table is Table plans.code is global and references tenant table notes. Drop the reference, or make plans a tenant table.
OKM1706
An index that does not lead with the key is Index on notes starts with title. Put tenantId first. With casing "snake", the name in that sentence is tenant_id.
composite
compositeTenancy({ key, type: "uuid" }) adds every name, in that order. for() takes every key. One name is columnTenancy().
import { connect } from "okmodel/pg/postgresjs";
import { compositeTenancy } from "okmodel/tenancy";
import { schema, table, t } from "okmodel/pg";
const documents = table("documents", {
id: t.identity(),
title: t.text(),
});
const app = schema({
tables: [documents],
tenancy: compositeTenancy({
key: ["organizationId", "workspaceId"],
type: "uuid",
}),
});
export async function add(
url: string,
organizationId: string,
workspaceId: string,
title: string,
) {
await using db = connect(url, { schema: app });
return db.for({ organizationId, workspaceId }).documents.insert({ title });
}OKM1060
compositeTenancy() needs { key, type }. Another type is compositeTenancy() type text must be uuid. A key that is not a list is compositeTenancy() key must be a list of field names, such as ["organizationId", "workspaceId"]. An empty name is compositeTenancy() key must be a field name. A repeated name is compositeTenancy() key repeats organizationId.
OKM1701
for() without both keys is for() needs { organizationId, workspaceId } as a uuid. Another field is for() accepts organizationId, workspaceId. extra is not the tenant key.
row level
rlsTenancy({ key, type: "uuid" }) is one key. The column predicate stays. Policies are the backstop: notes_tenant and notes_unscoped_select on a table named notes. A view that exposes the key is security_invoker. via() and a key list are refused. unscoped() can read and cannot write.
import { rlsTenancy } from "okmodel/tenancy";
import { schema, table, t } from "okmodel/pg";
const notes = table("notes", {
id: t.identity(),
title: t.text(),
});
export default schema({
tables: [notes],
tenancy: rlsTenancy({ key: "tenantId", type: "uuid" }),
});A healthy role check is cached for that endpoint until its generation changes. A failed check is not cached. The message names the endpoint.
OKM1060
rlsTenancy() needs { key, type }. A key list is rlsTenancy() takes one key. Combining it with compositeTenancy or via() is not supported yet. A path table is Table tasks uses via() with rlsTenancy(). Combining them is not supported yet.
OKM1707
The owner is The connected role owns the tenant tables, so row-level security would not apply. A superuser is The connected role is a superuser, so row-level security would not apply. BYPASSRLS is The connected role has BYPASSRLS, so row-level security would not apply. Each sentence then names the endpoint: Endpoint primary. A role pg_roles does not list is The connected role is not visible in pg_roles. Endpoint primary.
OKM1708
A policy that does not match is Table notes does not force row-level security. Endpoint primary. The words before the endpoint are does not force row-level security., is missing forced row-level security., policy notes_tenant does not match the tenant predicate., policy notes_unscoped_select does not match the unscoped predicate., or has a row-level security policy okm did not emit.
OKM1701
unscoped() cannot write under row-level security.
path
via("project.organization") follows to-one relations, at most three. The last table stores the key. This table does not. A read or a write adds exists along that path.
import { columnTenancy, via } from "okmodel/tenancy";
import { one, schema, table, t } from "okmodel/pg";
const organizations = table("organizations", {
id: t.identity(),
name: t.text(),
});
const projects = table(
"projects",
{
id: t.identity(),
organizationId: t.bigint().references("organizations"),
name: t.text(),
},
{ relations: { organization: one("organizations") } },
);
const tasks = table(
"tasks",
{
id: t.identity(),
projectId: t.bigint().references("projects"),
title: t.text(),
},
{
relations: { project: one("projects") },
tenancy: via("project.organization"),
},
);
export default schema({
tables: [organizations, projects, tasks],
tenancy: columnTenancy({ key: "tenantId", type: "uuid" }),
});OKM1060
An empty path is via() needs a relation path.
OKM1705
More than three steps is Table tasks tenancy via "a.b.c.d" is longer than 3 hops. A missing to-one relation is Table tasks tenancy via "project.organization" has no to-one relation project. A path that ends on a global table is Table tasks tenancy via "project.organization" ends at plans, which is not a tenant table.