TypeORM integration for CASL — query only the records a user is allowed to access.
pnpm add @cookiemonsterdev/casl-typeorm @casl/ability typeorm@cookiemonsterdev/casl-typeorm provides two main building blocks:
createTypeOrmAbility— creates a CASLAbilitythat accepts native TypeORMFindOptionsWhereconditions, enabling both database-level filtering and instance-level permission checks.accessibleBy— converts ability rules into aFindOptionsWherearray you can pass directly to TypeORM'sfind,findOne, orfindAndCount.
Use TypeORM operators directly as conditions — the same operators you already use in find():
import { In, MoreThan, Not } from 'typeorm';
import { createTypeOrmAbility } from '@cookiemonsterdev/casl-typeorm';
// Rules are evaluated last-to-first: the last rule has the highest priority.
const ability = createTypeOrmAbility([
{ action: 'read', subject: 'Article', conditions: { published: true } },
{ action: 'read', subject: 'Article', conditions: { authorId: userId } },
// This cannot rule is last → highest priority → overrides the can rules above
{
action: 'read',
subject: 'Article',
conditions: { status: In(['banned', 'deleted']) },
inverted: true,
},
]);import { accessibleBy } from '@cookiemonsterdev/casl-typeorm';
const where = accessibleBy(ability, 'read').ofType('Article');
if (!where) {
// null means the user has no access to any records
return [];
}
const articles = await articleRepository.find({ where });where is a FindOptionsWhere<Article>[]. TypeORM treats an array as OR — each element is an AND-merged condition set. The result already encodes the full rule logic, including cannot constraints.
createTypeOrmAbility also wires up a runtime conditions matcher, so ability.can() works on individual entity instances:
import { subject } from '@casl/ability';
const article = await articleRepository.findOneBy({ id: 1 });
if (ability.can('read', subject('Article', article))) {
// user is allowed
}import { AbilityBuilder } from '@casl/ability';
import { createTypeOrmAbility, TypeOrmAbility } from '@cookiemonsterdev/casl-typeorm';
import { Not } from 'typeorm';
type Actions = 'create' | 'read' | 'update' | 'delete';
type Subjects = 'Article' | 'Comment';
function defineAbilityFor(user: User): TypeOrmAbility<[Actions, Subjects]> {
const { can, cannot, build } = new AbilityBuilder(createTypeOrmAbility);
if (user.isAdmin) {
can('manage', 'all');
} else {
can('read', 'Article', { published: true });
can('read', 'Article', { authorId: user.id });
cannot('read', 'Article', { secret: Not(false) });
can('create', 'Comment');
can('update', 'Comment', { authorId: user.id });
}
return build();
}
AbilityBuilderfollows the same last-wins priority:cannotdefined aftercanoverrides it.
Creates a CASL Ability configured to use TypeORM FindOptionsWhere conditions.
| Parameter | Type | Description |
|---|---|---|
rules |
RawRuleFrom<A, FindOptionsWhere>[] |
Initial rules array |
options |
AbilityOptions |
Standard CASL ability options (excluding conditionsMatcher and fieldMatcher) |
Returns a TypeOrmAbility<A> instance.
Converts ability rules into an AccessibleRecords builder.
| Parameter | Type | Default |
|---|---|---|
ability |
AnyAbility |
— |
action |
string |
'read' |
Returns FindOptionsWhere<Entity>[] | null.
- Array — use directly as the
whereoption infind(),findOne(), etc. null— user has no access; handle by returning an empty result or throwingForbiddenError.
const where = accessibleBy(ability).ofType(Article);
// or with a string subject:
const where = accessibleBy(ability).ofType('Article');The runtime conditions matcher used internally by createTypeOrmAbility. You can use it directly if you create a custom Ability:
import { Ability, fieldPatternMatcher } from '@casl/ability';
import { typeormQueryMatcher } from '@cookiemonsterdev/casl-typeorm';
const ability = new Ability(rules, {
conditionsMatcher: typeormQueryMatcher,
fieldMatcher: fieldPatternMatcher,
});Supports: plain equality, Not, In, MoreThan, MoreThanOrEqual, LessThan, LessThanOrEqual, IsNull, Between, Like, ILike, And, Or, ArrayContains, ArrayContainedBy, ArrayOverlap.
Rawis not supported for instance-level checks — it can only be used in query conditions.
Multi-field cannot conditions are approximated at the field level. Given:
cannot('read', 'Article', { secret: true, internal: true });The generated condition is { secret: Not(true), internal: Not(true) } which evaluates as secret != true AND internal != true. The semantically correct form is NOT (secret = true AND internal = true) = secret != true OR internal != true. For single-field conditions, the behaviour is exact.
MIT © Mykhailo Toporkov