npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@cerbos/orm-prisma

v4.0.0

Published

Prisma adapter for Cerbos query plans

Readme

Cerbos + Prisma ORM Adapter

An adapter library that takes a Cerbos Query Plan (PlanResources API) response and converts it into a Prisma where clause object. This is designed to work alongside a project using the Cerbos Javascript SDK.

Features

Supported Operators

Basic Operators

  • Logical operators: and, or, not
  • Comparison operators: eq, ne, lt, gt, lte, gte, in
  • String operations: startsWith, endsWith, contains, isSet

Relation Operators

  • One-to-one: is, isNot
  • One-to-many/Many-to-many: some, none, every
  • Collection operators: exists, all, except (exists_one requires counting matches, which Prisma where-filters cannot express — it throws rather than silently degrading to exists; filter only appears inside other expressions)
  • Set operations: hasIntersection

Arithmetic

  • add, sub, mult, div with a constant side are solved algebraically to a plain column comparison: R.attr.aNumber + 1 > 2{ aNumber: { gt: 1 } } (Prisma where-filters cannot express column arithmetic). Multiplying/dividing by a negative constant mirrors directional operators. Arithmetic on both sides of a comparison, division BY a column, and equality or inequality over fractional addition throw. The latter cannot be solved reversibly in IEEE-754 arithmetic.
  • String concatenation solving: P.attr.context == "projects:" + R.attr.id{ id: { equals: "123" } }

Field-to-field comparisons

Comparisons between two columns of the same model compile to Prisma field references. Pass the Prisma model name via the model option (root columns) or relation.model in the mapper (columns of a related model inside a collection expression). Comparisons across models throw — Prisma only supports references between fields of the same model.

Hierarchy Operators

  • hierarchy(string), hierarchy(string, delimiter), hierarchy([segments])
  • overlaps: segment-wise prefix comparison between two hierarchies
  • ancestorOf / descendentOf: strict prefix relationship between hierarchies

Advanced Features

  • Deep nested relations support
  • Automatic field inference
  • Collection mapping and filtering
  • Complex condition combinations
  • Type-safe field mappings
  • Timestamp comparisons against Prisma DateTime columns. Mark the field mapping with valueType: "dateTime"; applying timestamp() to an untyped/string mapping throws. Literals must be strict RFC 3339 instants in CEL's supported year 0001–9999 range and exactly representable at millisecond precision (fractional digits after the third may only be zero). The mapped column/database must preserve that precision.
  • Outer-column references inside collection expressions (e.g. R.attr.tags.exists(t, t.name == "x" && R.attr.aBool)) are hoisted or case-split so every filter lands on the model it belongs to
  • Three-valued-logic guards for nullable element columns: mark a relation field as nullable: true in the mapper and collection macros (all, negated exists, hasIntersection over map) exclude rows whose elements hold NULL in that column, matching Cerbos's treatment of a missing attribute as a deny

Known limitations (loud failures, never silently-wrong filters)

  • LIKE wildcards: Prisma emits LIKE without an ESCAPE clause, so contains/startsWith/ endsWith with a needle containing % or _, or with a column-valued needle, throws. (A constant receiver with a column needle — "a-b".startsWith(R.attr.x) — is translated exactly by enumerating candidate needles into an in filter.)
  • Hierarchy prefixes: ancestorOf, descendentOf and overlaps narrow a column with a startsWith, so they throw when the constant hierarchy contains %, _ or [. [ is rejected as well as the two LIKE wildcards because SQL Server opens a character class on [ even when an ESCAPE clause is declared, so it cannot be matched literally at all.
  • Counting: exists_one, size() thresholds other than empty/non-empty, and string-length comparisons throw (_count is not supported inside Prisma where).
  • Cross-model column comparisons throw (Prisma field references are same-model only). This includes membership between an outer scalar column and a related collection column.

Database collation is an authorization invariant

Cerbos string comparisons are case-sensitive. Prisma delegates comparison semantics to the database collation, so a case-insensitive or accent-insensitive collation can make a generated authorization filter return rows that Cerbos would deny. Treat the database collation used by mapped authorization columns as part of the policy contract:

  • PostgreSQL: use a deterministic, case-sensitive collation and avoid citext or an insensitive Prisma query mode for mapped fields.
  • MySQL/MariaDB: choose a case-sensitive (_cs) or binary collation rather than the common case-insensitive (_ci) defaults.
  • SQL Server: use a case-sensitive (_CS_) collation rather than a case-insensitive (_CI_) collation.
  • SQLite: do not apply COLLATE NOCASE to mapped fields, and verify the exact comparison behavior of any database configuration or extension used in production.

The adapter cannot override a column's collation in a Prisma where filter. See Prisma's case-sensitivity documentation for provider-specific details.

Conformance contract

The adapter is differentially tested against Cerbos PDP 0.54.0 checkResource decisions using 20 hostile seed rows and both Prisma 6 and 7. The Spring Data adapter defines the reference semantics for this compatibility snapshot.

| Classification | Coverage | | --- | --- | | Oracle-tested | 83 reference actions | | Fail-closed | 31 reference actions plus the 3 reference-unsupported shapes (34 actions total) | | Known planner divergence | has() on a missing attribute is folded by the Cerbos planner to ALWAYS_ALLOWED, while checkResource denies the missing-attribute rows. Until the planner is fixed, use R.attr.x != null for database-backed attributes instead of has(R.attr.x) |

The fail-closed set consists of literal LIKE cases Prisma cannot escape safely, cross-model field references, arbitrary relation counts and string lengths, exists_one, unsolved column arithmetic, sub-millisecond now() thresholds, and the reference probes for regex, ordered indexing, and timestamp() over a string field. Supported timestamp plans require a mapper entry with valueType: "dateTime" and a strict, millisecond-exact RFC 3339 literal in CEL's supported instant range. These shapes throw instead of producing a broader authorization filter.

Requirements

  • Cerbos > v0.40
  • @cerbos/http or @cerbos/grpc client
  • Prisma >= v6.0 (v7 supported)

System Requirements

  • Node.js >= 22.0.
  • Prisma CLI & Client >= 6.0 (v7 supported)
  • A database supported by Prisma (SQLite/PostgreSQL/MySQL/etc.) so the Prisma client can communicate with stored data

Installation

npm install @cerbos/orm-prisma

Usage

The package exports a function:

import { queryPlanToPrisma, PlanKind } from "@cerbos/orm-prisma";

queryPlanToPrisma({
  queryPlan,                // The Cerbos query plan response
  mapper,                   // Map Cerbos field names to Prisma field names
}): {
  kind: PlanKind,
  filters?: any             // Prisma where conditions
}

Basic Example

  1. Create a basic policy file in the policies directory:
apiVersion: api.cerbos.dev/v1
resourcePolicy:
  resource: resource
  version: default
  rules:
    - actions: ["view"]
      effect: EFFECT_ALLOW
      roles: ["USER"]
      condition:
        match:
          expr: request.resource.attr.status == "active"
  1. Start Cerbos PDP:
docker run --rm -i -p 3592:3592 -v $(pwd)/policies:/policies ghcr.io/cerbos/cerbos:latest
  1. Create Prisma schema (prisma/schema.prisma):
datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Resource {
  id     Int     @id @default(autoincrement())
  title  String
  status String
}
  1. Implement the mapper
import { GRPC as Cerbos } from "@cerbos/grpc";
import { PrismaClient } from "@prisma/client";
import { queryPlanToPrisma, PlanKind } from "@cerbos/orm-prisma";

const prisma = new PrismaClient();
const cerbos = new Cerbos("localhost:3592", { tls: false });

// Fetch query plan from Cerbos
const queryPlan = await cerbos.planResources({
  principal: { id: "user1", roles: ["USER"] },
  resource: { kind: "resource" },
  action: "view",
});

// Convert query plan to Prisma filters
const result = queryPlanToPrisma({
  queryPlan,
  mapper: {
    "request.resource.attr.title": { field: "title" },
    "request.resource.attr.status": { field: "status" },
  },
});

if (result.kind === PlanKind.ALWAYS_DENIED) {
  return [];
}

// Use filters in Prisma query
const records = await prisma.resource.findMany({
  where: result.filters,
});

// Use filters in Prisma query with other conditions
const records = await prisma.resource.findMany({
  where: {
    AND: [
      {
        status: "DRAFT"
      },
      result.filters,
    ]
});

Collection Operators

The adapter understands the full Cerbos collection operator set, including except. For example, the configuration below ensures a resource’s categories do not have any sub-category named finance:

const result = queryPlanToPrisma({
  queryPlan,
  mapper: {
    "request.resource.attr.categories": {
      relation: {
        name: "categories",
        type: "many",
        fields: {
          subCategories: {
            relation: {
              name: "subCategories",
              type: "many",
              fields: {
                name: { field: "name" },
              },
            },
          },
        },
      },
    },
  },
});

queryPlanToPrisma emits the necessary nested NOT structure so Prisma receives a valid filter for the entire relation chain.

Field Name Mapping

Fields can be mapped using either an object or a function:

// Object mapping
const result = queryPlanToPrisma({
  queryPlan,
  mapper: {
    "request.resource.attr.fieldName": { field: "prismaFieldName" },
  },
});

// Function mapping
const result = queryPlanToPrisma({
  queryPlan,
  mapper: (fieldName) => ({
    field: fieldName.replace("request.resource.attr.", ""),
  }),
});

Relations Mapping

Relations are mapped with their types and optional field configurations. Fields can be automatically inferred from the path if not explicitly mapped:

const result = queryPlanToPrisma({
  queryPlan,
  mapper: {
    // Simple relation mapping - fields will be inferred
    "request.resource.attr.owner": {
      relation: {
        name: "owner",
        type: "one", // "one" for one-to-one, "many" for one-to-many
      },
    },

    // Relation with explicit field mapping
    "request.resource.attr.tags": {
      relation: {
        name: "tags",
        type: "many",
        field: "name", // Optional: specify field for direct comparisons
      },
    },

    // Relation with nested field mappings
    "request.resource.attr.nested": {
      relation: {
        name: "nested",
        type: "one",
        fields: {
          // Optional: specify mappings for nested fields
          aBool: { field: "aBool" },
          aNumber: { field: "aNumber" },
        },
      },
    },
  },
});

Field Inference Example

When using relations, fields are automatically inferred from the path unless explicitly mapped:

// These mappers are equivalent for handling: request.resource.attr.nested.aNumber
{
  "request.resource.attr.nested": {
    relation: {
      name: "nested",
      type: "one",
      fields: {
        aNumber: { field: "aNumber" }
      }
    }
  }
}

// Shorter version - aNumber will be inferred from the path
{
  "request.resource.attr.nested": {
    relation: {
      name: "nested",
      type: "one"
    }
  }
}

Handling in Operators

queryPlanToPrisma normalises Cerbos in expressions to match Prisma expectations:

  • Single values become equality comparisons ({ field: "value" }).
  • Arrays remain { field: { in: [...] } }.
  • Relation-backed fields retain their relation structure while still applying the appropriate equality or in operator at the leaf.

Complex Example with Multiple Relations and Direct Fields

const result = queryPlanToPrisma({
  queryPlan,
  mapper: {
    "request.resource.attr.status": { field: "status" },
    "request.resource.attr.owner": {
      relation: {
        name: "owner",
        type: "one",
      },
    },
    "request.resource.attr.tags": {
      relation: {
        name: "tags",
        type: "many",
        field: "name",
      },
    },
  },
});

// Results in Prisma filters like:
const result = await primsa.resource.findMany({
  where: {
    AND: [
      { status: { equals: "active" } },
      { owner: { is: { id: { equals: "user1" } } } },
      { tags: { some: { name: { in: ["tag1", "tag2"] } } } },
    ];
  }
})

Complex Examples

Lambda Expression Examples

// Using exists with lambda expressions
const result = queryPlanToPrisma({
  queryPlan,
  mapper: {
    "request.resource.attr.comments": {
      relation: {
        name: "comments",
        type: "many",
        fields: {
          author: {
            relation: {
              name: "author",
              type: "one",
            },
          },
          status: { field: "status" },
        },
      },
    },
  },
});

// This can handle complex exists queries like:
// "Does the resource have any approved comments by specific users?"
const result = await primsa.resource.findMany({
  where: {
    comments: {
      some: {
        AND: [
          { status: { equals: "approved" } },
          {
            author: {
              is: {
                id: { in: ["user1", "user2"] },
              },
            },
          },
        ],
      },
    },
  },
});

Development

Running Tests

npm test

Note: The suite seeds prisma/dev.db and invokes prisma db push --force-reset. Only run it against disposable development databases.

The tests populate Prisma with fixture data and assert query results directly against those fixtures, covering scalar and relation in operators, collection behaviour (including except), nested relations, and lambda expressions.

Types

Query Plan Response Types

The adapter is fully typed and provides clear type definitions for all responses:

import { PlanKind, QueryPlanToPrismaResult } from "@cerbos/orm-prisma";

// The result will be one of these types:
type QueryPlanToPrismaResult =
  | {
      kind: PlanKind.ALWAYS_ALLOWED | PlanKind.ALWAYS_DENIED;
    }
  | {
      kind: PlanKind.CONDITIONAL;
      filters: Record<string, any>;
    };

// Example usage with type narrowing:
const result = queryPlanToPrisma({ queryPlan });

if (result.kind === PlanKind.CONDITIONAL) {
  // TypeScript knows `filters` exists here
  const records = await prisma.resource.findMany({
    where: result.filters,
  });
} else if (result.kind === PlanKind.ALWAYS_ALLOWED) {
  // No filters needed
  const records = await prisma.resource.findMany();
} else {
  // Must be ALWAYS_DENIED
  return [];
}

Mapper Types

The mapper configuration is also fully typed:

type MapperConfig = {
  field?: string;
  valueType?: "dateTime";
  nullable?: boolean;
  relation?: {
    name: string;
    type: "one" | "many";
    model?: string;
    field?: string;
    fields?: {
      [key: string]: MapperConfig; // Recursive for nested fields
    };
  };
};

type Mapper = { [key: string]: MapperConfig } | ((key: string) => MapperConfig);

Full Example

A complete example application using this adapter can be found at https://github.com/cerbos/express-prisma-cerbos

Resources

Documentation

Examples and Tutorials

Related Projects

Community

License

Apache 2.0.