-
-
Notifications
You must be signed in to change notification settings - Fork 301
CASL's $exists behaves inconsistently with Mongo's $exists #1072
Copy link
Copy link
Open
Labels
Description
Describe the bug
CASL's $exists behaves inconsistently with Mongo's $exists
My usecase is that as a developer, I want to share ability definitions from backend to frontend, so that frontend can reuse the definition.
The frontend are not aware of MongoDB schemas/models. It only uses plain objects.
To Reproduce
- Define tests:
// exists.test.js
const { AbilityBuilder, createMongoAbility } = require('@casl/ability');
const { subject } = require('@casl/ability');
const mongoose = require('mongoose');
const MODEL_NAME = 'MyModel';
const MyModelSchema = new mongoose.Schema({
myProp: String,
});
const MyModel = mongoose.model(MODEL_NAME, MyModelSchema);
describe('CASL $exists', () => {
let ability;
beforeEach(() => {
const { can, build } = new AbilityBuilder(createMongoAbility);
can(
'update',
MODEL_NAME,
// Solution 1 - $exists: behaves inconsistently with Mongo's `$exists` for plain objects
{ myProp: { $exists: false } }
// Solution 2 - $in: works consistently but not convenient to be shared with frontend because the condition is jsonified to `{ "$in": [null, null] }`
// { myProp: { $in: [null, undefined] } }
// Solution 3 - $or: doesn't work
// {
// $or: [
// { myProp: { $exists: false } },
// { myProp: null }
// ]
// }
);
ability = build();
});
it('No specified instance: should be true', () => {
expect(ability.can('update', MODEL_NAME)).toBe(true);
});
it('Doc & myProp is absent: should be true', () => {
const doc = new MyModel({});
expect(ability.can('update', subject(MODEL_NAME, doc))).toBe(true);
});
it('Obj & myProp is absent: should be true', () => {
const doc = new MyModel({});
expect(ability.can('update', subject(MODEL_NAME, doc.toObject()))).toBe(true);
});
it('Doc & myProp is null: should be true', () => {
const doc = new MyModel({ myProp: null });
expect(ability.can('update', subject(MODEL_NAME, doc))).toBe(true);
});
it('Obj & myProp is null: should be true', () => {
const doc = new MyModel({ myProp: null });
expect(ability.can('update', subject(MODEL_NAME, doc.toObject()))).toBe(true);
});
it('Doc & myProp is truthy: should be false', () => {
const doc = new MyModel({ myProp: 'something' });
expect(ability.can('update', subject(MODEL_NAME, doc))).toBe(false);
});
it('Obj & myProp is truthy: should be false', () => {
const doc = new MyModel({ myProp: 'something' });
expect(ability.can('update', subject(MODEL_NAME, doc.toObject()))).toBe(false);
});
});- Execute tests
npx jest --colors tests/unit/route/exists.test.js
Output:
CASL $exists
✓ No specified instance: should be true (1 ms)
✓ Doc & myProp is absent: should be true (1 ms)
✓ Obj & myProp is absent: should be true
✓ Doc & myProp is null: should be true
✕ Obj & myProp is null: should be true (1 ms)
✕ Doc & myProp is truthy: should be false (1 ms)
✓ Obj & myProp is truthy: should be false
Expected behavior
Expect all tests to be successful. The concept $exists should reflex consistent logic between:
- Mongo's
$exists:{$exists: false}matches docs that have null or absent property - CALS
$existsfor a Mongoose doc:{$exists: false}matches if the doc has null or undefined or absent property - CALS
$existsfor a plain object:{$exists: false}matches if the doc has null or undefined or absent property
CASL Version
@casl/ability - 6.7.3
Environment:
Nodejs v24.11.0
Reactions are currently unavailable