Skip to content

Commit e724741

Browse files
committed
fix(builder): makes AbilityBuilder bound methods define as arrow functions
Fixes #736
1 parent a194d0a commit e724741

File tree

3 files changed

+133
-81
lines changed

3 files changed

+133
-81
lines changed

packages/casl-ability/spec/types/AbilityBuilder.spec.ts

Lines changed: 88 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,79 @@
11
import { expectTypeOf } from 'expect-type'
22
import {
33
AbilityBuilder,
4-
AbilityClass,
5-
PureAbility,
6-
SubjectType,
7-
MongoQuery,
8-
AbilityTuple,
9-
MongoAbility,
10-
createMongoAbility
4+
AbilityClass, AbilityTuple, createMongoAbility, MongoAbility, PureAbility
115
} from '../../src'
126

137
describe('AbilityBuilder types', () => {
14-
type Method<T extends any[]> = (...args: T) => any
15-
168
it('infers types from `PureAbility` default generics', () => {
179
const builder = new AbilityBuilder<PureAbility<AbilityTuple>>(PureAbility)
18-
type Can = typeof builder.can
1910

20-
expectTypeOf<Method<[string, SubjectType]>>().toMatchTypeOf<Can>()
21-
expectTypeOf<Method<[string[], SubjectType[]]>>().toMatchTypeOf<Can>()
22-
expectTypeOf<Method<[string, SubjectType, unknown]>>().toMatchTypeOf<Can>()
23-
expectTypeOf<[string, SubjectType, string | string[]]>().toMatchTypeOf<Parameters<Can>>()
24-
expectTypeOf<[string, SubjectType, string | string[], unknown]>()
25-
.toMatchTypeOf<Parameters<Can>>()
26-
expectTypeOf<[string, SubjectType, {}]>().not.toMatchTypeOf<Parameters<Can>>()
27-
expectTypeOf<[string, SubjectType, string]>().not.toEqualTypeOf<Parameters<Can>>()
28-
expectTypeOf<[string]>().not.toEqualTypeOf<Parameters<Can>>()
11+
builder.can('read', 'Subject')
12+
builder.can('read', class {})
13+
// @ts-expect-error only `string | class` can be a subject type
14+
builder.can('read', {})
15+
16+
builder.can(['read', 'update'], ['Subject1', 'Subject2'])
17+
builder.can('update', ['Subject1', 'Subject2'])
18+
builder.can(['read', 'update'], 'Subject')
19+
// @ts-expect-error only `string | string[]` can be used as action
20+
builder.can(1, 'Subject')
21+
22+
builder.can('read', 'Subject', {})
23+
builder.can('read', 'Subject', { title: 'new' })
24+
builder.can('read', 'Subject', { title: 'new2', anyOtherProperty: true })
25+
builder.can('read', 'Subject', 1)
26+
builder.can('read', 'Subject', () => {})
27+
builder.can('read', 'Subject', 'field1')
28+
builder.can('read', 'Subject', 'field1', { condition: true })
29+
builder.can('read', 'Subject', ['field1', 'field2'])
30+
builder.can('read', 'Subject', ['field1', 'field2'], () => {})
31+
// @ts-expect-error expects 3rd parameter to fields -> `string | string[]`
32+
builder.can('read', 'Subject', {}, () => {})
2933
})
3034

3135
it('infers types from `createMongoAbility` default generics', () => {
3236
const builder = new AbilityBuilder(createMongoAbility)
33-
type Can = typeof builder.can
3437

35-
expectTypeOf<Method<[string, SubjectType]>>().toMatchTypeOf<Can>()
36-
expectTypeOf<Method<[string[], SubjectType[]]>>().toMatchTypeOf<Can>()
37-
expectTypeOf<Method<[string, SubjectType, MongoQuery]>>().toMatchTypeOf<Can>()
38-
expectTypeOf<[string, SubjectType, string | string[]]>().toMatchTypeOf<Parameters<Can>>()
39-
expectTypeOf<[string, SubjectType, string | string[], MongoQuery]>()
40-
.toMatchTypeOf<Parameters<Can>>()
38+
builder.can('read', 'Subject')
39+
builder.can('read', class {})
40+
// @ts-expect-error only `string | class` can be a subject type
41+
builder.can('read', {})
42+
43+
builder.can(['read', 'update'], ['Subject1', 'Subject2'])
44+
builder.can('update', ['Subject1', 'Subject2'])
45+
builder.can(['read', 'update'], 'Subject')
46+
// @ts-expect-error only `string | string[]` can be used as action
47+
builder.can(1, 'Subject')
48+
49+
builder.can('read', 'Subject', {})
50+
builder.can('read', 'Subject', {
51+
title: {
52+
unknownOperator$: true, // TODO: change types to error this
53+
}
54+
})
55+
builder.can('read', 'Subject', {
56+
published: {
57+
$eq: true
58+
}
59+
})
60+
// @ts-expect-error conditions is expected to be a MongoQuery
61+
builder.can('read', 'Subject', () => {})
62+
// @ts-expect-error conditions is expected to be a MongoQuery
63+
builder.can('read', 'Subject', 1)
64+
65+
builder.can('read', 'Subject', 'field')
66+
builder.can('read', 'Subject', 'field', {
67+
published: {
68+
$eq: true
69+
}
70+
})
71+
builder.can('read', 'Subject', ['field1', 'field2'], {
72+
published: {
73+
$eq: true
74+
}
75+
})
76+
builder.can('read', 'Subject', ['field1', 'field2'])
4177
})
4278

4379
it('infers single action argument type from ClaimAbility', () => {
@@ -95,6 +131,32 @@ describe('AbilityBuilder types', () => {
95131
})
96132
})
97133

134+
describe('action and subject pairs restrictions', () => {
135+
type Post = { id: number, title: string, kind: 'Post' }
136+
type User = { id: number, name: string, kind: 'User' }
137+
type AppAbility = MongoAbility<
138+
['read' | 'update' | 'delete' | 'create', 'Post' | Post] |
139+
['read' | 'update', 'User' | User]
140+
>
141+
let builder: AbilityBuilder<AppAbility>
142+
143+
beforeEach(() => {
144+
builder = new AbilityBuilder<AppAbility>(createMongoAbility)
145+
})
146+
147+
it('allows to use only specified actions for specified subjects', () => {
148+
builder.can('read', 'Post', { id: 1 })
149+
builder.can('read', 'Post', { id: 1, title: 'test' })
150+
builder.can(['update', 'delete', 'create'], 'Post', { id: 1, title: 'test' })
151+
// @ts-expect-error "manage" is not in allowed list of actions for this subject
152+
builder.can('manage', 'Post')
153+
154+
builder.can(['read', 'update'], 'User', { id: 1 })
155+
// @ts-expect-error "delete" is not in allowed list of actions for this subject
156+
builder.can('delete', 'User', { id: 1 })
157+
})
158+
})
159+
98160
describe('class type as a subject', () => {
99161
class Post {
100162
id!: number

packages/casl-ability/src/Ability.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,13 @@ export interface MongoAbility<
2828
C extends MongoQuery = MongoQuery
2929
> extends PureAbility<A, C> {}
3030

31+
export function createMongoAbility<
32+
T extends AnyMongoAbility = MongoAbility
33+
>(rules?: RawRuleOf<T>[], options?: AbilityOptionsOf<T>): T;
3134
export function createMongoAbility<
3235
A extends AbilityTuple = AbilityTuple,
3336
C extends MongoQuery = MongoQuery
3437
>(rules?: RawRuleFrom<A, C>[], options?: AbilityOptions<A, C>): MongoAbility<A, C>;
35-
export function createMongoAbility<
36-
T extends AnyMongoAbility = AnyMongoAbility
37-
>(rules?: RawRuleOf<T>[], options?: AbilityOptionsOf<T>): T;
3838
export function createMongoAbility(rules: any[] = [], options = {}): AnyMongoAbility {
3939
return new PureAbility(rules, {
4040
conditionsMatcher: mongoQueryMatcher,

packages/casl-ability/src/AbilityBuilder.ts

Lines changed: 42 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
import { AnyMongoAbility, createMongoAbility } from './Ability';
2-
import { AnyAbility, AbilityOptionsOf } from './PureAbility';
3-
import { RawRuleOf, Generics } from './RuleIndex';
2+
import { ProduceGeneric } from './hkt';
3+
import { AbilityOptionsOf, AnyAbility } from './PureAbility';
4+
import { Generics, RawRuleOf } from './RuleIndex';
45
import {
5-
ExtractSubjectType as E,
6-
AbilityTuple,
7-
SubjectType,
8-
TaggedInterface,
9-
Normalize,
10-
AnyObject,
11-
AnyClass,
6+
AbilityTuple, AnyClass, AnyObject, ExtractSubjectType as E, Normalize, SubjectType,
7+
TaggedInterface
128
} from './types';
13-
import { ProduceGeneric } from './hkt';
149

1510
function isAbilityClass(factory: AbilityFactory<any>): factory is AnyClass {
1611
return typeof factory.prototype.possibleRulesFor === 'function';
@@ -42,7 +37,7 @@ type InstanceOf<T extends AnyAbility, S extends SubjectType> = S extends AnyClas
4237
type ConditionsOf<T extends AnyAbility, I extends {}> =
4338
ProduceGeneric<Generics<T>['conditions'], I>;
4439
type ActionFrom<T extends AbilityTuple, S extends SubjectType> = T extends any
45-
? S extends T[1] ? T[0] : never
40+
? S extends Extract<T[1], SubjectType> ? T[0] : never
4641
: never;
4742
type ActionOf<T extends AnyAbility, S extends SubjectType> = ActionFrom<Generics<T>['abilities'], S>;
4843
type SubjectTypeOf<T extends AnyAbility> = E<Normalize<Generics<T>['abilities']>[1]>;
@@ -77,34 +72,56 @@ type BuilderCanParametersWithFields<
7772
: SimpleCanParams<T>;
7873
type Keys<T> = string & keyof T;
7974

75+
type AddRule<T extends AnyAbility> = {
76+
<
77+
I extends InstanceOf<T, S>,
78+
F extends string = Keys<I>,
79+
S extends SubjectTypeOf<T> = SubjectTypeOf<T>
80+
>(...args: BuilderCanParametersWithFields<S, I, F | Keys<I>, T>): RuleBuilder<T>;
81+
<
82+
I extends InstanceOf<T, S>,
83+
S extends SubjectTypeOf<T> = SubjectTypeOf<T>
84+
>(...args: BuilderCanParameters<S, I, T>): RuleBuilder<T>;
85+
};
86+
8087
export class AbilityBuilder<T extends AnyAbility> {
8188
public rules: RawRuleOf<T>[] = [];
8289
private readonly _createAbility: AbilityFactory<T>;
90+
public can: AddRule<T>;
91+
public cannot: AddRule<T>;
92+
public build: (options?: AbilityOptionsOf<T>) => T;
8393

8494
constructor(AbilityType: AbilityFactory<T>) {
8595
this._createAbility = AbilityType;
86-
this.can = this.can.bind(this as any);
87-
this.cannot = this.cannot.bind(this as any);
88-
this.build = this.build.bind(this as any);
96+
97+
this.can = (
98+
action: string | string[],
99+
subject?: SubjectType | SubjectType[],
100+
conditionsOrFields?: string | string[] | Generics<T>['conditions'],
101+
conditions?: Generics<T>['conditions']
102+
) => this._addRule(action, subject, conditionsOrFields, conditions, false);
103+
this.cannot = (
104+
action: string | string[],
105+
subject?: SubjectType | SubjectType[],
106+
conditionsOrFields?: string | string[] | Generics<T>['conditions'],
107+
conditions?: Generics<T>['conditions']
108+
) => this._addRule(action, subject, conditionsOrFields, conditions, true);
109+
110+
this.build = options => (isAbilityClass(this._createAbility)
111+
? new this._createAbility(this.rules, options)
112+
: this._createAbility(this.rules, options));
89113
}
90114

91-
can<
92-
I extends InstanceOf<T, S>,
93-
S extends SubjectTypeOf<T> = SubjectTypeOf<T>
94-
>(...args: BuilderCanParameters<S, I, T>): RuleBuilder<T>;
95-
can<
96-
I extends InstanceOf<T, S>,
97-
F extends string = Keys<I>,
98-
S extends SubjectTypeOf<T> = SubjectTypeOf<T>
99-
>(...args: BuilderCanParametersWithFields<S, I, F | Keys<I>, T>): RuleBuilder<T>;
100-
can(
115+
private _addRule(
101116
action: string | string[],
102117
subject?: SubjectType | SubjectType[],
103118
conditionsOrFields?: string | string[] | Generics<T>['conditions'],
104-
conditions?: Generics<T>['conditions']
119+
conditions?: Generics<T>['conditions'],
120+
inverted?: boolean
105121
): RuleBuilder<T> {
106122
const rule = { action } as RawRuleOf<T>;
107123

124+
if (inverted) rule.inverted = inverted;
108125
if (subject) {
109126
rule.subject = subject;
110127

@@ -120,35 +137,8 @@ export class AbilityBuilder<T extends AnyAbility> {
120137
}
121138

122139
this.rules.push(rule);
123-
124140
return new RuleBuilder(rule);
125141
}
126-
127-
cannot<
128-
I extends InstanceOf<T, S>,
129-
S extends SubjectTypeOf<T> = SubjectTypeOf<T>
130-
>(...args: BuilderCanParameters<S, I, T>): RuleBuilder<T>;
131-
cannot<
132-
I extends InstanceOf<T, S>,
133-
F extends string = Keys<I>,
134-
S extends SubjectTypeOf<T> = SubjectTypeOf<T>
135-
>(...args: BuilderCanParametersWithFields<S, I, F | Keys<I>, T>): RuleBuilder<T>;
136-
cannot(
137-
action: string | string[],
138-
subject?: SubjectType | SubjectType[],
139-
conditionsOrFields?: string | string[] | Generics<T>['conditions'],
140-
conditions?: Generics<T>['conditions'],
141-
): RuleBuilder<T> {
142-
const builder = (this as any).can(action, subject, conditionsOrFields, conditions);
143-
builder._rule.inverted = true;
144-
return builder;
145-
}
146-
147-
build(options?: AbilityOptionsOf<T>) {
148-
return isAbilityClass(this._createAbility)
149-
? new this._createAbility(this.rules, options)
150-
: this._createAbility(this.rules, options);
151-
}
152142
}
153143

154144
type DSL<T extends AnyAbility, R> = (

0 commit comments

Comments
 (0)