-
-
Notifications
You must be signed in to change notification settings - Fork 301
@casl/prisma: every should return true on empty lists to match prisma behavior #1073
Description
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