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

@noxify/casl-drizzle

v0.3.1

Published

Drizzle-ORM adapter for CASL - generate Drizzle where inputs from CASL abilities

Downloads

381

Readme

@noxify/casl-drizzle

CASL v7 integration for Drizzle ORM ( 1.0.0-rc.4 ) - Add type-safe authorization to your database queries

Features

  • 🔒 Type-safe - Full TypeScript support with Drizzle types
  • 🎯 Relation support - Filter by related table conditions
  • 🔁 Many-to-many support - Filter across join-table relations
  • 🔗 Query operators - All Drizzle operators (eq, gt, like, etc.)
  • 💡 IDE autocomplete - Subject-specific field suggestions

Install

npm install @noxify/casl-drizzle @casl/ability [email protected]
pnpm add @noxify/casl-drizzle @casl/ability [email protected]

Setup

Define your Drizzle schema and relations:

import { defineRelations, pgTable } from "drizzle-orm"
import { integer, text } from "drizzle-orm/pg-core"

const users = pgTable("users", {
  id: integer().primaryKey(),
  name: text().notNull(),
})

const posts = pgTable("posts", {
  id: integer().primaryKey(),
  title: text().notNull(),
  authorId: integer().notNull(),
})

export const relations = defineRelations({ users, posts }, (r) => ({
  users: { posts: r.many.posts() },
  posts: { author: r.one.users({ from: r.posts.authorId, to: r.users.id }) },
}))

Usage

Create type-safe abilities with Drizzle query conditions:

import type { QueryInput } from "@noxify/casl-drizzle"
import { accessibleBy, createDrizzleAbility, some } from "@noxify/casl-drizzle"
import { sql } from "drizzle-orm"

type PostQuery = QueryInput<typeof relations, "posts">
type UserQuery = QueryInput<typeof relations, "users">

const currentUserId = 1

const ability = createDrizzleAbility<
  { posts: PostQuery; users: UserQuery },
  "read" | "create" | "update" | "delete"
>((can) => {
  // Simple field filtering
  can("read", "posts", { published: true })

  // Filter by related table (author)
  can("read", "posts", { author: { id: currentUserId } })

  // Many-to-many filtering (example: users by groups)
  can("read", "users", { groups: some(sql`name = 'Admins'`) })

  // Complex conditions with operators
  can("update", "posts", {
    author: { id: currentUserId },
    createdAt: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) },
  })

  // Raw SQL for complex queries
  can("delete", "posts", {
    RAW: sql`author_id = ${currentUserId} AND published = false`,
  })
})

// Convert abilities to database filters (property access)
const filters = accessibleBy(ability, "read")
const posts = await db.query.posts.findMany({ where: filters.posts })
const users = await db.query.users.findMany({ where: filters.users })

// Alternative: use ofType() for CASL/Prisma-compatible API
const postWhere = accessibleBy(ability, "read").ofType("posts")
const userWhere = accessibleBy(ability, "read").ofType("users")

Behavior Notes

every() - All Related Records Must Match

every() filters records where all related records satisfy the condition. Important: it currently requires that related records exist:

// ✅ Returns users who have AT LEAST ONE post, and ALL their posts have views > 100
can("read", "users", {
  posts: every({ views: { gt: 100 } }),
})

// ❌ Returns no users if they have NO posts at all
// (even though "all zero posts have >100 views" is technically true)

Use when you need to enforce a condition across all related records that exist. For "users with no posts" scenarios, use none() instead.

none() - No Related Records Match

none() filters records where no related records satisfy the condition. Works reliably for simple cases but may behave unexpectedly in complex relation chains.

✅ Simple case (recommended):

can("read", "posts", { comments: none() })

⚠️ Complex paths - Known Issue: When filtering through nested relations, none() semantics can be inverted:

// ❌ Behavior may be unexpected:
can("read", "posts", {
  comments: none({ author: { id: adminId } }),
})

✅ Solution - Use Drizzle's type-safe subquery:

import { notExists, eq, and } from "drizzle-orm"

can("read", "posts", {
  RAW: notExists(
    db
      .select()
      .from(comments)
      .where(and(eq(comments.postId, posts.id), eq(comments.authorId, adminId)))
  ),
})

⚠️ Current limitation: Due to Drizzle's alias handling in subqueries, both notExists() and raw SQL with outer table references currently fail. For now, the most reliable approach is to avoid complex none() filters and use simpler patterns or application-level filtering for edge cases.

For simple cases without outer table references:

// This works: Simple static condition without referencing outer table
can("read", "users", {
  posts: none(), // All users with no posts
})

For critical authorization rules involving complex relation filters, always use explicit RAW SQL to ensure predictable behavior.

Differences to @casl/prisma

This library follows the same accessibleBy API pattern as @casl/prisma but differs in how forbidden access is handled:

| Scenario | @casl/prisma | @noxify/casl-drizzle | | --- | --- | --- | | No matching rules | Returns { OR: [] } (empty result set) | Throws ForbiddenError |

Why? Throwing immediately gives you a clear error message (It's not allowed to run "read" on "posts") instead of silently returning zero results. This makes debugging permission issues easier and avoids unexpected empty queries going unnoticed.

If you need to handle the forbidden case gracefully, wrap the call in a try/catch:

import { ForbiddenError } from "@casl/ability"

try {
  const where = accessibleBy(ability, "read").ofType("posts")
  const posts = await db.query.posts.findMany({ where })
} catch (error) {
  if (error instanceof ForbiddenError) {
    // No access to this subject — return empty or respond with 403
    return []
  }
  throw error
}

Acknowledgements

This project was heavily inspired by ucastle by Guilherme Araujo and evolved through substantial refactoring and extension for Drizzle relation support.

If you are looking for the original foundation and ideas, please also check the ucastle repository.