Drizzle adapter
| This documentation is for an as-yet unreleased version of Cerbos. Choose 0.50.0 from the version picker at the top right or navigate to https://docs.cerbos.dev for the latest version. |
The @cerbos/orm-drizzle package converts a Cerbos PlanResources response into a Drizzle ORM SQL expression. The resulting filter can be composed with your existing query builder chain.
Requirements
-
Cerbos >= v0.16
-
@cerbos/httpor@cerbos/grpcclient -
Drizzle ORM (SQLite, PostgreSQL, MySQL, PlanetScale)
-
Node.js >= 20.0.0
Supported operators
-
Logical:
and,or,not -
Comparison:
eq,ne,lt,gt,le,ge,in -
String:
contains,startsWith,endsWith -
Existence:
isSet -
Collection:
hasIntersection,exists,exists_one,all,filter -
Relation-aware mappings with
EXISTSsubqueries
Usage
import { queryPlanToDrizzle, PlanKind } from "@cerbos/orm-drizzle";
import { eq, and } from "drizzle-orm";
import { resources } from "./schema";
const plan = await cerbos.planResources({
principal,
resource: { kind: "document" },
action: "view",
});
const result = queryPlanToDrizzle({
queryPlan: plan,
mapper: {
"request.resource.attr.status": resources.status,
"request.resource.attr.owner": resources.ownerId,
},
});
switch (result.kind) {
case PlanKind.ALWAYS_DENIED:
return [];
case PlanKind.ALWAYS_ALLOWED:
return await db.select().from(resources);
case PlanKind.CONDITIONAL:
return await db
.select()
.from(resources)
.where(result.filter);
}
Field mapper
The mapper associates Cerbos attribute references with Drizzle columns. It accepts:
-
A plain object with keys as Cerbos attribute references and values as Drizzle columns or SQL expressions
-
A function receiving the attribute reference and returning the column or expression
-
An object with a
columnproperty and an optionaltransformfunction for custom SQL translation
const result = queryPlanToDrizzle({
queryPlan,
mapper: {
"request.resource.attr.custom": {
column: sql`lower(${resources.title})`,
transform: ({ operator, value }) => {
if (operator !== "eq") throw new Error("Unsupported");
return eq(sql`lower(${resources.title})`, value.toLowerCase());
},
},
},
});
Relation mapping
The adapter wraps relation comparisons in EXISTS subqueries, automatically inferring relation fields when they match column names on the related table.
const result = queryPlanToDrizzle({
queryPlan,
mapper: {
"request.resource.attr.owner": {
relation: {
type: "one",
table: owners,
sourceColumn: resources.ownerId,
targetColumn: owners.id,
fields: {
email: owners.email,
},
},
},
"request.resource.attr.tags": {
relation: {
type: "many",
table: resourceTags,
sourceColumn: resources.id,
targetColumn: resourceTags.resourceId,
fields: {
name: {
relation: {
type: "one",
table: tags,
sourceColumn: resourceTags.tagId,
targetColumn: tags.id,
field: tags.name,
},
},
},
},
},
},
});
Collection operators
-
hasIntersection— translates tocolumn IN (…)for multi-valued attributes mapped as nested relations -
exists,exists_one,all— generate correlatedEXISTSsubqueries with the lambda variable scoped to the relation -
filter— Cerbos uses this during plan construction; the adapter discards the lambda since the filter is applied in Drizzle