Skip to content

@casl/prisma: every should return true on empty lists to match prisma behavior #1073

@nickiepucel

Description

@nickiepucel

Describe the bug

@casl/prisma’s interpreter for the Prisma relation operator every does not match Prisma’s semantics. In Prisma, a relation filter like:

where: { posts: { every: { verified: true } } }

is vacuously true when the relation is empty (i.e., a record with zero related items still satisfies the every condition). There is further discussion about this behavior here.

However, in CASL-Prisma’s in-memory evaluator, the every handler contains an items.length > 0 check:

return Array.isArray(items)
  && items.length > 0
  && items.every(item => interpret(condition.value, item));

This causes every to return false for empty arrays, which diverges from Prisma’s actual behavior.

As a result, ability.can(...) and accessibleBy(ability) can disagree for the same rule: Prisma will return the record (empty relation passes), but CASL’s in-memory conditions will deny it. This creates an inconsistency between database-level filtering and runtime authorization checks.

To Reproduce
I see that the package already includes a unit test that covers this case.

Here's an additional small test case that illustrates the issue using casl-prisma's Prisma schema:

import { PureAbility, AbilityBuilder, subject } from "@casl/ability";
import { createPrismaAbility, PrismaQuery, Subjects } from "@casl/prisma";

type SubjectsMap = {
  User: {
    id: number;
    posts: { verified: boolean | null }[];
  };
};

type AppSubjects = Subjects<SubjectsMap>;
type AppAbility = PureAbility<[string, AppSubjects], PrismaQuery>;

describe('casl-prisma "every" on empty relation', () => {
  it("treats `every` on an empty relation as false", () => {
    const { can, build } = new AbilityBuilder<AppAbility>(createPrismaAbility);

    // Prisma-style condition on a relation:
    // where: { posts: { every: { verified: true } } }
    can("read", "User", {
      posts: {
        every: { verified: true },
      },
    });

    const ability = build();

    const userWithNoPosts = subject("User", {
      id: 1,
      posts: [],
    });

    const allowed = ability.can("read", userWithNoPosts);

    // Current behavior (due to `items.length > 0` in `every` interpreter):
    // allowed === false
    //
    // Prisma's `every` semantics treat this as true:
    // A User with zero posts satisfies "every post is verified"
    // because there is no counterexample.
    //
    // So this console.log shows the mismatch:
    console.log('ability.can("read", postWithNoComments) =', allowed);

    // This expectation documents the behavior I *would* expect
    // if casl-prisma's runtime matched Prisma's `every` semantics:
    expect(allowed).toBe(true);
  });
});

Expected behavior

The in-memory interpreter for the Prisma every operator should match Prisma’s semantics: when the related field is an empty array, the every condition should evaluate to true (vacuous truth).

In other words, this unit test check should be:

expect(test({ posts: [] })).toBe(true)

CASL Version

"@casl/ability": "^6.7.1",
"@casl/prisma": "^1.4.1",

Environment:

Node v20.18.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions