Skip to content

CASL's $exists behaves inconsistently with Mongo's $exists #1072

@terrynguyen255

Description

@terrynguyen255

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

  1. 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);
  });
});
  1. 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 $exists for a Mongoose doc: {$exists: false} matches if the doc has null or undefined or absent property
  • CALS $exists for 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions