Guide
OrmAI with Drizzle
Drizzle is the ORM of choice for teams who want type safety without code generation. Here's how to wrap it with OrmAI's policy engine.
Drizzle has become the default for many TypeScript projects: zero code generation, edge-friendly, SQL-first. OrmAI’s @ormai/drizzle adapter wraps a Drizzle database with the same policy engine as the Python and Prisma versions.
Install
pnpm add @ormai/core @ormai/drizzle
Wire it up
Assume you have a Drizzle schema:
// db/schema.ts
import { pgTable, integer, varchar, timestamp, serial } from "drizzle-orm/pg-core";
export const customers = pgTable("customers", {
id: serial("id").primaryKey(),
tenantId: integer("tenant_id").notNull(),
name: varchar("name", { length: 120 }).notNull(),
email: varchar("email", { length: 120 }).notNull(),
apiSecret: varchar("api_secret", { length: 64 }),
});
export const orders = pgTable("orders", {
id: serial("id").primaryKey(),
tenantId: integer("tenant_id").notNull(),
customerId: integer("customer_id").notNull().references(() => customers.id),
totalCents: integer("total_cents").notNull(),
status: varchar("status", { length: 20 }).default("pending").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
Wire the adapter:
// lib/ormai.ts
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import { mountDrizzle, PolicyBuilder, DEFAULT_PROD } from "@ormai/core";
import * as schema from "@/db/schema";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
export const db = drizzle(pool, { schema });
export const policy = new PolicyBuilder(DEFAULT_PROD)
.registerSchema(schema, ["customers", "orders"])
.denyFields(["*secret*", "*password*", "*token*"])
.maskFields(["customers.email"])
.tenantScope("tenantId")
.enableWrites(["orders"], { requireReason: true })
.maxRows(200)
.build();
export const toolset = mountDrizzle({ db, schema, policy });
registerSchema introspects the Drizzle schema object and registers the named tables. tenantScope("tenantId") matches the camelCase JS property — OrmAI knows it maps to tenant_id in SQL.
Edge runtime
Drizzle is the easiest ORM to run on Edge. With @neondatabase/serverless:
import { drizzle } from "drizzle-orm/neon-http";
import { neon } from "@neondatabase/serverless";
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
The OrmAI adapter doesn’t care which driver is underneath. It calls Drizzle’s query builder.
Type safety
Drizzle’s types are exceptional, and OrmAI preserves them. The model parameter in db.query is constrained to the table names you registered:
toolset.execute("db.query", { model: "orders" }); // ok
toolset.execute("db.query", { model: "OrderItems" }); // ts: not in registry
Returned rows are typed against the Drizzle schema, with redacted fields typed as string (the masked value) or null (denied).
Domain tools with Drizzle
Same pattern as Prisma — register a tool() that uses the Drizzle client directly:
import { tool } from "@ormai/core";
import { db } from "@/lib/db";
import { orders } from "@/db/schema";
import { and, eq, gte, sql } from "drizzle-orm";
export const weeklyRevenue = tool({
name: "analytics.weekly_revenue",
description: "Sum of paid orders this calendar week.",
input: {},
async execute(_, ctx) {
const result = await db
.select({ cents: sql<number>`SUM(${orders.totalCents})` })
.from(orders)
.where(and(
eq(orders.tenantId, ctx.tenantId),
eq(orders.status, "paid"),
gte(orders.createdAt, weekStart()),
));
return { cents: result[0]?.cents ?? 0 };
},
});
The audit row is written automatically. Don’t forget to use ctx.tenantId — Drizzle doesn’t inject scoping for hand-written queries.
Drizzle-specific gotchas
db.execute() for raw SQL
Like Prisma’s $queryRaw, Drizzle’s db.execute(sql\…`)` is an escape hatch. Don’t expose it to the agent. Use it only in human-reviewed application code.
Column name mismatch
Drizzle separates the JS property name from the DB column name. OrmAI uses the JS name in policy declarations (tenantId), and translates to the DB name when generating SQL. If your schema has unusual aliases, double-check that policy field paths use the JS names.
Relational queries
Drizzle’s relational query builder (db.query.orders.findMany) is supported by OrmAI’s include parameter:
toolset.execute("db.query", {
model: "orders",
include: ["customer"],
where: { status: "pending" },
});
Becomes a Drizzle relational query with the joined table scoped to the same tenant.
Migration from raw Drizzle
If your existing code:
const result = await db.select().from(orders).where(
and(eq(orders.tenantId, session.tenantId), eq(orders.status, "pending")),
);
Becomes:
const result = await toolset.execute("db.query", {
model: "orders",
where: { status: "pending" },
}, ctx);
The tenant filter, the row cap, the audit row, and the field redaction all happen for free.
Related
Found a typo or want to suggest a topic? Email [email protected].