Skip to content

Commit

Permalink
docs: adds examples of how to create Ability using factory functions
Browse files Browse the repository at this point in the history
Fixes #685
  • Loading branch information
stalniy committed Sep 26, 2022
1 parent f4a466c commit c3e9aa7
Show file tree
Hide file tree
Showing 16 changed files with 156 additions and 152 deletions.
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,14 @@ Lets define `Ability` for a blog website where visitors:
* cannot delete a post if it was created more than a day ago

```ts
import { AbilityBuilder, Ability } from '@casl/ability'
import { AbilityBuilder, createMongoAbility } from '@casl/ability'
import { User } from '../models'; // application specific interfaces

/**
* @param user contains details about logged in user: its id, name, email, etc
*/
function defineAbilitiesFor(user: User) {
const { can, cannot, rules } = new AbilityBuilder(Ability);
const { can, cannot, build } = new AbilityBuilder(createMongoAbility);

// can read blog posts
can('read', 'BlogPost');
Expand All @@ -116,7 +116,7 @@ function defineAbilitiesFor(user: User) {
createdAt: { $lt: Date.now() - 24 * 60 * 60 * 1000 }
});

return new Ability(rules);
return build();
});
```

Expand Down Expand Up @@ -171,7 +171,6 @@ Of course, you are not restricted to use only class instances in order to check
CASL has a complementary package [@casl/mongoose] which provides easy integration with MongoDB and [mongoose].

```ts
import { AbilityBuilder } from '@casl/ability';
import { accessibleRecordsPlugin } from '@casl/mongoose';
import mongoose from 'mongoose';

Expand Down
6 changes: 3 additions & 3 deletions docs-src/src/content/app/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,17 @@ buttons:

exampleCode: !md |
```js
import { Ability, AbilityBuilder } from '@casl/ability';
import { createMongoAbility, AbilityBuilder } from '@casl/ability';
// define abilities
const { can, cannot, rules } = new AbilityBuilder();
const { can, cannot, build } = new AbilityBuilder(createMongoAbility);
can('read', ['Post', 'Comment']);
can('manage', 'Post', { author: 'me' });
can('create', 'Comment');
// check abilities
const ability = new Ability(rules);
const ability = build();
ability.can('read', 'Post') // true
```
Expand Down
27 changes: 12 additions & 15 deletions docs-src/src/content/pages/advanced/customize-ability/en.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Let's see an example of how to add `$nor` operator. To do this, we will use `bui

```ts
import {
Ability,
createMongoAbility,
AbilityBuilder,
Abilities,
buildMongoQueryMatcher,
Expand All @@ -31,7 +31,7 @@ import { $nor, nor } from '@ucast/mongo2js';
const conditionsMatcher = buildMongoQueryMatcher({ $nor }, { nor });

export default function defineAbilityFor(user: any) {
const { can, build } = new AbilityBuilder(Ability);
const { can, build } = new AbilityBuilder(createMongoAbility);

can('read', 'Article', {
$nor: [{ private: true }, { authorId: user.id }]
Expand All @@ -47,22 +47,21 @@ export default function defineAbilityFor(user: any) {

```ts
import {
Ability,
createMongoAbility,
MongoAbility,
AbilityBuilder,
Abilities,
MongoQueryFieldOperators,
ConditionsMatcher,
AbilityClass
} from '@casl/ability';
import { $in, within, $eq, eq, createFactory, BuildMongoQuery } from '@ucast/mongo2js';

type RestrictedMongoQuery<T> = BuildMongoQuery<T, Pick<MongoQueryFieldOperators, '$eq' | '$in'>>;
const conditionsMatcher: ConditionsMatcher<RestrictedMongoQuery> = createFactory({ $in, $eq }, { in: within, eq });
type AppAbility = Ability<Abilities, RestrictedMongoQuery>;
const AppAbility = Ability as AbilityClass<AppAbility>;
type AppAbility = MongoAbility<Abilities, RestrictedMongoQuery>;

export default function defineAbilityFor(user: any) {
const { can, build } = new AbilityBuilder(AppAbility);
const { can, build } = new AbilityBuilder(createMongoAbility);

can('read', 'Article', { authorId: user.id } });
can('read', 'Article', { status: { $in: ['draft', 'published'] } });
Expand All @@ -77,7 +76,7 @@ By restricting operators, you not only disallow other developers to use more com

## Custom conditions matcher implementation

If you want to implement custom conditions matcher, you should use `PureAbility` class instead of `Ability`. `PureAbility` is a parent class for `Ability`, the only difference between them is that `Ability` has restriction on `Conditions` generic parameter and has default values for `conditionsMatcher` and `fieldMatcher` options.
If you want to implement custom conditions matcher, you should use `PureAbility` class instead of `createMongoAbility` factory function. `createMongoAbility` is a factory function that creates `PureAbility` instance with default values for `conditionsMatcher` and `fieldMatcher` options (i.e, mongo conditions matcher and field pattern matcher).

> The prefix "Pure" has nothing to do with functional programming. It just means this class has no predefined configuration.
Expand All @@ -91,15 +90,13 @@ import {
AbilityBuilder,
AbilityTuple,
MatchConditions,
AbilityClass
} from '@casl/ability';

type AppAbility = PureAbility<AbilityTuple, MatchConditions>;
const AppAbility = PureAbility as AbilityClass<AppAbility>;
const lambdaMatcher = (matchConditions: MatchConditions) => matchConditions;

export default function defineAbilityFor(user: any) {
const { can, build } = new AbilityBuilder(AppAbility);
export default function defineAbilityFor(user: any): AppAbility {
const { can, build } = new AbilityBuilder<AppAbility>(PureAbility);

can('read', 'Article', ({ authorId }) => authorId === user.id);
can('read', 'Article', ({ status }) => ['draft', 'published'].includes(status));
Expand All @@ -114,19 +111,19 @@ We don't recommend to use functions for matching logic if you need to serialize

## Custom field matcher

Field matcher is responsible for matching fields passed as 3rd argument to `can` method of `Ability` instance. It is a factory function that returns a function which accepts field and returns boolean. This logic is enforced by `FieldMatcher` type from `@casl/ability`.
Field matcher is responsible for matching fields passed as 3rd argument to `can` method of `PureAbility` instance. It is a factory function that returns a function which accepts field and returns boolean. This logic is enforced by `FieldMatcher` type from `@casl/ability`.

> We cannot imagine a reasonable case to override field matching logic, [default implementation](../../guide/restricting-fields) should be more than enough.
You can use this to reduce your bundle size or enforce simpler logic. For example, let's implement simple field matcher that doesn't support field patterns:

```ts
import { Ability, AbilityBuilder, FieldMatcher } from '@casl/ability';
import { createMongoAbility, AbilityBuilder, FieldMatcher } from '@casl/ability';

export const fieldMatcher: FieldMatcher = fields => field => fields.includes(field);

export default function defineAbilityFor(user: any) {
const { can, build } = new AbilityBuilder(Ability);
const { can, build } = new AbilityBuilder(createMongoAbility);

can('read', 'Article', ['title', 'content']);

Expand Down
14 changes: 7 additions & 7 deletions docs-src/src/content/pages/advanced/debugging-testing/en.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ Sometimes it may be a bit complicated to understand why some action in the app i

## Debugging

`Ability`'s `can` and `cannot` methods return boolean result and doesn't explain the reason or which rule forbids the action. To get the rule which allows or forbids an action on a subject, you can use `relevantRuleFor` method. It accepts the same arguments as `can`:
`PureAbility`'s `can` and `cannot` methods return boolean result and doesn't explain the reason or which rule forbids the action. To get the rule which allows or forbids an action on a subject, you can use `relevantRuleFor` method. It accepts the same arguments as `can`:

```js
import { defineAbility } from '@casl/ability';
Expand Down Expand Up @@ -86,10 +86,10 @@ console.log(rule.reason); // Private content is protected by law

## Testing

`Ability` instance is pure in terms of functional programming. It means that for the same rules, its `can` method returns always the same result. That's why, there is no big profit in testing CASL permissions, instead you should test rule distribution logic. **This sounds correct, but only at the first sight**. Let's consider an example:
`PureAbility` instance is pure in terms of functional programming. It means that for the same rules, its `can` method returns always the same result. That's why, there is no big profit in testing CASL permissions, instead you should test rule distribution logic. **This sounds correct, but only at the first sight**. Let's consider an example:

```js @{data-filename="defineAbility.js"}
import { Ability, AbilityBuilder, subject } from '@casl/ability';
import { createMongoAbility, AbilityBuilder, subject } from '@casl/ability';

export const article = subject.bind(null, 'Article');

Expand All @@ -98,7 +98,7 @@ export const article = subject.bind(null, 'Article');
* And we need to test it, not ability checks!
*/
export function defineRulesFor(user) {
const { can, cannot, rules } = new AbilityBuilder(Ability);
const { can, cannot, rules } = new AbilityBuilder(createMongoAbility);

if (user.isAdmin) {
can('manage', 'all');
Expand All @@ -110,7 +110,7 @@ export function defineRulesFor(user) {
return rules;
}

export const defineAbilityFor = user => new Ability(defineRulesFor(user));
export const defineAbilityFor = user => createMongoAbility(defineRulesFor(user));
```

Now we want to ensure that admin users can do anything and other can only read non-private articles. Using [mocha] + [chai] or [jest] we can do it (we will use mocha and chai):
Expand Down Expand Up @@ -153,10 +153,10 @@ Do you see the issue? **We've just tested implementation details and this is bad
Rules logic is very expressive and you can achieve the same results using a different combination of rules. For example "user can read non private articles" can be expressed in another way:

```js
import { AbilityBuilder } from '@casl/ability';
import { AbilityBuilder, createMongoAbility } from '@casl/ability';

export function defineRulesFor(user) {
const { can, cannot, rules } = new AbilityBuilder(Ability);
const { can, cannot, rules } = new AbilityBuilder(createMongoAbility);

if (user.isAdmin) {
can('manage', 'all');
Expand Down
Loading

0 comments on commit c3e9aa7

Please sign in to comment.