Views
Views are read-only projections of tables with a predefined WHERE clause. They
share the same query API as tables, but reject mutations.
import {z, table, view} from "@b9g/zen";
const Users = table("users", {
id: z.string().db.primary(),
name: z.string(),
role: z.enum(["user", "admin"]),
deletedAt: z.date().nullable().db.softDelete(),
});
// Define views with explicit names
const ActiveUsers = view("active_users", Users)`
WHERE ${Users.cols.deletedAt} IS NULL
`;
const AdminUsers = view("admin_users", Users)`
WHERE ${Users.cols.role} = ${"admin"}
`;
// Query from views (same API as tables)
const admins = await db.all(AdminUsers)``;
const admin = await db.get(AdminUsers, "u1");Mutations against a view throw:
await db.insert(AdminUsers, {/* ... */}); // โ Error
await db.update(AdminUsers, {/* ... */}); // โ Error
await db.delete(AdminUsers, "u1"); // โ ErrorThe auto-generated .active view #
Tables with a .db.softDelete() field automatically get an .active view that
excludes soft-deleted rows:
// Equivalent to: view("users_active", Users)`WHERE deletedAt IS NULL`
const activeUsers = await db.all(Users.active)``;Views preserve relationships #
Views inherit references from their base table, so JOINs work identically:
const posts = await db.all([Posts, AdminUsers])`
JOIN "admin_users" ON ${AdminUsers.on(Posts)}
`;
posts[0].author?.role; // "admin"Create or refresh a view's DDL with db.ensureView(view) โ see
Migrations.