Skip to content

Commit

Permalink
feat(extra): adds rulesToAST that converts rules into @ucast AST
Browse files Browse the repository at this point in the history
Relates to #350
  • Loading branch information
stalniy committed Aug 20, 2020
1 parent b8c8b60 commit 55fd6ee
Show file tree
Hide file tree
Showing 6 changed files with 121 additions and 48 deletions.
2 changes: 1 addition & 1 deletion packages/casl-ability/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,6 @@
"extra"
],
"dependencies": {
"@ucast/mongo2js": "^1.1.1"
"@ucast/mongo2js": "^1.2.0"
}
}
58 changes: 58 additions & 0 deletions packages/casl-ability/spec/rulesToAST.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { defineAbility } from '../src'
import { rulesToAST } from '../src/extra'

describe('rulesToAST', () => {
it('returns empty "and" `Condition` if there are no conditions in `Ability`', () => {
const ability = defineAbility(can => can('read', 'Post'))
const ast = rulesToAST(ability, 'read', 'Post')

expect(ast.operator).to.equal('and')
expect(ast.value).to.be.an('array').that.is.empty
})

it('returns `null` if there is no ability to do an action', () => {
const ability = defineAbility(can => can('read', 'Post'))
const ast = rulesToAST(ability, 'update', 'Post')

expect(ast).to.be.null
})

it('returns only "or" `Condition` if there are no forbidden rules', () => {
const ability = defineAbility((can) => {
can('read', 'Post', { author: 1 })
can('read', 'Post', { private: false })
})
const ast = rulesToAST(ability, 'read', 'Post')

expect(ast).to.deep.equal({
operator: 'or',
value: [
{ operator: 'eq', field: 'private', value: false },
{ operator: 'eq', field: 'author', value: 1 },
]
})
})

it('returns "and" condition that includes "or" if there are forbidden and regular rules', () => {
const ability = defineAbility((can, cannot) => {
can('read', 'Post', { author: 1 })
can('read', 'Post', { sharedWith: 1 })
cannot('read', 'Post', { private: true })
})
const ast = rulesToAST(ability, 'read', 'Post')

expect(ast).to.deep.equal({
operator: 'and',
value: [
{ operator: 'eq', field: 'private', value: true },
{
operator: 'or',
value: [
{ operator: 'eq', field: 'sharedWith', value: 1 },
{ operator: 'eq', field: 'author', value: 1 },
]
}
]
})
})
})
File renamed without changes.
14 changes: 6 additions & 8 deletions packages/casl-ability/src/RuleIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,17 +55,14 @@ interface IndexTree<A extends Abilities, C> {
}
}

type RuleIterator<A extends Abilities, C> = (rule: RawRuleFrom<A, C>, index: number) => void;

export type Unsubscribe = () => void;

export class RuleIndex<A extends Abilities, Conditions, BaseEvent extends {} = {}> {
private _hasPerFieldRules: boolean = false;
private _mergedRules: Record<string, Rule<A, Conditions>[]> = Object.create(null);
private _events: Events<this, BaseEvent> = Object.create(null);
private _indexedRules: IndexTree<A, Conditions> = Object.create(null);
private _rules: this['rules'] = [];
readonly rules!: RawRuleFrom<A, Conditions>[];
private _rules: RawRuleFrom<A, Conditions>[] = [];
readonly _ruleOptions!: RuleOptions<A, Conditions>;
readonly detectSubjectType!: DetectSubjectType<Normalize<A>[1]>;
/** @private hacky property to track Abilities type */
Expand All @@ -82,13 +79,14 @@ export class RuleIndex<A extends Abilities, Conditions, BaseEvent extends {} = {
fieldMatcher: options.fieldMatcher,
resolveAction: options.resolveAction || identity,
};
Object.defineProperty(this, 'detectSubjectType', {
value: options.detectSubjectType || detectSubjectType
});
Object.defineProperty(this, 'rules', { get: () => this._rules });
this.detectSubjectType = options.detectSubjectType || detectSubjectType;
this.update(rules);
}

get rules() {
return this._rules;
}

update(rules: RawRuleFrom<A, Conditions>[]): this {
const event = {
rules,
Expand Down
65 changes: 41 additions & 24 deletions packages/casl-ability/src/extra.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,25 @@
import { Condition, buildAnd, buildOr } from '@ucast/mongo2js';
import { PureAbility, AnyAbility } from './PureAbility';
import { RuleOf, Generics } from './RuleIndex';
import { RawRule } from './RawRule';
import { Rule } from './Rule';
import { setByPath, wrapArray } from './utils';
import {
AbilityParameters,
AnyObject,
SubjectType,
AbilityTuple,
CanParameters,
Abilities
} from './types';
import { AnyObject, SubjectType, Normalize } from './types';

export type RuleToQueryConverter<T extends AnyAbility> = (rule: RuleOf<T>) => object;
export interface AbilityQuery {
$or?: object[]
$and?: object[]
export interface AbilityQuery<T = object> {
$or?: T[]
$and?: T[]
}

type RulesToQueryRestArgs<T extends Abilities, ConvertRule> = AbilityParameters<
T,
T extends AbilityTuple
? (action: T[0], subject: T[1], convert: ConvertRule) => 0
: never,
(action: never, subject: never, convert: ConvertRule) => 0
>;

export function rulesToQuery<T extends AnyAbility>(
ability: T,
...args: RulesToQueryRestArgs<Generics<T>['abilities'], RuleToQueryConverter<T>>
action: Normalize<Generics<T>['abilities']>[0],
subject: Normalize<Generics<T>['abilities']>[1],
convert: RuleToQueryConverter<T>
): AbilityQuery | null {
const [action, subject, convert] = args;
const query: AbilityQuery = {};
const rules = ability.rulesFor(action, subject) as RuleOf<T>[];
const query: AbilityQuery = Object.create(null);
const rules = ability.rulesFor(action, subject);

for (let i = 0; i < rules.length; i++) {
const rule = rules[i];
Expand All @@ -54,9 +41,39 @@ export function rulesToQuery<T extends AnyAbility>(
return query.$or ? query : null;
}

function ruleToAST(rule: RuleOf<AnyAbility>): Condition {
if (!rule.ast) {
throw new Error(`Ability rule "${JSON.stringify(rule)}" does not have "ast" property. So, cannot be used to generate AST`);
}
return rule.ast;
}

export function rulesToAST<T extends AnyAbility>(
ability: T,
action: Normalize<Generics<T>['abilities']>[0],
subject: Normalize<Generics<T>['abilities']>[1]
): Condition | null {
const query = rulesToQuery(ability, action, subject, ruleToAST) as AbilityQuery<Condition>;

if (query === null) {
return null;
}

if (!query.$and) {
return query.$or ? buildOr(query.$or) : buildAnd([]);
}

if (query.$or) {
query.$and.push(buildOr(query.$or));
}

return buildAnd(query.$and);
}

export function rulesToFields<T extends PureAbility<any, AnyObject>>(
ability: T,
...[action, subject]: CanParameters<Generics<T>['abilities'], false>
action: Normalize<Generics<T>['abilities']>[0],
subject: Normalize<Generics<T>['abilities']>[1]
): AnyObject {
return ability.rulesFor(action, subject)
.filter(rule => !rule.inverted && rule.conditions)
Expand Down
30 changes: 15 additions & 15 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 55fd6ee

Please sign in to comment.