Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

@cerbos/orm-prisma

alexolivier16.4kApache-2.02.0.1TypeScript support: included

Prisma adapter for Cerbos query plans

cerbos, prisma, orm, authorization, permissions

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, exists_one, all, filter
  • Set operations: hasIntersection

Advanced Features

  • Deep nested relations support
  • Automatic field inference
  • Collection mapping and filtering
  • Complex condition combinations
  • Type-safe field mappings

Requirements

  • Cerbos > v0.40
  • @cerbos/http or @cerbos/grpc client
  • Prisma > v6.0

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,
    ]
});

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"
    }
  }
}

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"] },
              },
            },
          },
        ],
      },
    },
  },
});

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;
  relation?: {
    name: string;
    type: "one" | "many";
    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

Community

License

Apache 2.0 - See LICENSE for more information.