Skip to content

Commit

Permalink
feat(ability): adds pack/unpack rules methods
Browse files Browse the repository at this point in the history
Relates to #44
  • Loading branch information
stalniy committed Apr 19, 2018
1 parent 9199430 commit c60ab5d
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 14 deletions.
14 changes: 13 additions & 1 deletion packages/casl-ability/extra/extra.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Ability, Rule } from '@casl/ability'
import { Ability, Rule, RawRule } from '@casl/ability'

export function rulesToQuery(ability: Ability, action: string, subject: any, convert: (rule: Rule) => Object): Object | null

Expand All @@ -7,3 +7,15 @@ export interface PermittedFieldsOptions {
}

export function permittedFieldsOf(ability: Ability, action: string, subject: any, options?: PermittedFieldsOptions): string[]

export type PackedRule = [string, string]
| [string, string, number]
| [string, string, number, any]
| [string, string, number, number]
| [string, string, number, any, string]
| [string, string, number, number, string]
| [string, string, number, number, number];

export function packRules(rules: RawRule[]): PackedRule[]

export function unpackRules(rules: PackedRule[]): RawRule[]
146 changes: 146 additions & 0 deletions packages/casl-ability/spec/pack_rules.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { packRules, unpackRules } from '../src/extra'

fdescribe('Ability rules packing', () => {
describe('`packRules` function', () => {
it('converts array of rule objects to array of rule arrays', () => {
const rules = packRules([
{ actions: 'read', subject: 'Post' },
{ actions: 'delete', subject: 'Post' }
])

expect(rules.every(Array.isArray)).to.be.true
})

it('puts `actions` as 1st element of rule array', () => {
const rules = packRules([{ actions: 'read', subject: 'Post' }])

expect(rules[0][0]).to.equal('read')
expect(rules[0]).to.have.length(2)
})

it('joins `actions` with comma if its value is an array', () => {
const rules = packRules([{ actions: ['read', 'update'], subject: 'Post' }])

expect(rules[0][0]).to.equal('read,update')
expect(rules[0]).to.have.length(2)
})

it('puts `subject` as 2nd element of rule array', () => {
const rules = packRules([{ actions: 'read', subject: 'Post' }])

expect(rules[0][1]).to.equal('Post')
expect(rules[0]).to.have.length(2)
})

it('puts `subject` with comma if its value is an array', () => {
const rules = packRules([{ actions: 'read', subject: ['Post', 'Comment'] }])

expect(rules[0][1]).to.equal('Post,Comment')
expect(rules[0]).to.have.length(2)
})

it('converts `inverted` to number and puts it as 3rd element of rule array', () => {
const rules = packRules([{ actions: 'read', subject: 'Post', inverted: true }])

expect(rules[0][2]).to.equal(1)
expect(rules[0]).to.have.length(3)
})

it('puts `conditions` as 4th element of rule array', () => {
const conditions = { private: true }
const rules = packRules([{ actions: 'read', subject: 'Post', conditions }])

expect(rules[0][3]).to.equal(conditions)
expect(rules[0]).to.have.length(4)
})

it('puts `0` in place of `conditions` if they are not defined but `fields` are defined', () => {
const rules = packRules([{ actions: 'read', subject: 'Post', fields: ['title'] }])

expect(rules[0][3]).to.equal(0)
expect(rules[0]).to.have.length(5)
})

it('joins `fields` and puts it as 5th element of rule array', () => {
const fields = ['title', 'description']
const rules = packRules([{ actions: 'read', subject: 'Post', fields }])

expect(rules[0][4]).to.equal(fields.join(','))
expect(rules[0]).to.have.length(5)
})
})

describe('`unpackRules` function', () => {
it('converts array of rule arrays to array of rule objects', () => {
const rules = unpackRules([
['read', 'Post'],
['delete', 'Post']
])

expect(rules.every(rule => typeof rule === 'object')).to.be.true
})

it('puts 1st element under `actions` field and converts it to an array', () => {
const rules = unpackRules([['read,update', 'Post']])

expect(rules[0].actions).to.deep.equal(['read', 'update'])
})

it('converts even a single `action` to an array', () => {
const rules = unpackRules([['read', 'Post']])

expect(rules[0].actions).to.deep.equal(['read'])
})

it('puts 2nd element under `subject` field and converts it to an array', () => {
const rules = unpackRules([['read', 'Post,Comment']])

expect(rules[0].subject).to.deep.equal(['Post', 'Comment'])
})

it('converts even a single `subject` to an array', () => {
const rules = unpackRules([['read', 'Post']])

expect(rules[0].subject).to.deep.equal(['Post'])
})

it('puts 3rd element under `inverted` field and converts it to boolean', () => {
const rules = unpackRules([['read', 'Post,Comment', 1]])

expect(rules[0].inverted).to.be.true
})

it('converts `inverted` to boolean, even it is not specified', () => {
const rules = unpackRules([['read', 'Post,Comment']])

expect(rules[0].inverted).to.be.false
})

it('puts 4th element under `conditions` field', () => {
const conditions = { private: true }
const rules = unpackRules([['read', 'Post,Comment', 0, conditions]])

expect(rules[0].conditions).to.equal(conditions)
})

it('converts `conditions` to `null` if its value is `0`', () => {
const rules = unpackRules([['read', 'Post,Comment', 1, 0]])

expect(rules[0].conditions).to.be.null
})

it('puts 5th element under `fields` field and converts it to an array', () => {
const fields = ['title', 'description']
const rules = unpackRules([['read', 'Post,Comment', 1, 0, fields.join(',')]])

expect(rules[0].fields).to.deep.equal(fields)
})


it('converts `fields` to `null` if its value is `0`', () => {
const rules = unpackRules([['read', 'Post,Comment', 1, 0, 0]])

expect(rules[0].fields).to.be.null
})
})
})
7 changes: 4 additions & 3 deletions packages/casl-ability/src/ability.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ForbiddenError } from './error';
import { Rule } from './rule';
import { wrapArray } from './utils';

function getSubjectName(subject) {
if (!subject || typeof subject === 'string') {
Expand All @@ -18,7 +19,7 @@ function clone(object) {
const DEFAULT_ALIASES = {
manage: ['create', 'read', 'update', 'delete'],
};
const PRIVATE_FIELD = typeof Symbol !== 'undefined' ? Symbol.for('private') : `__private${Date.now()}`;
const PRIVATE_FIELD = typeof Symbol !== 'undefined' ? Symbol('private') : `__${Date.now()}`;

export class Ability {
static addAlias(alias, actions) {
Expand Down Expand Up @@ -62,7 +63,7 @@ export class Ability {
for (let i = 0; i < rules.length; i++) {
const rule = new RuleType(rules[i]);
const actions = this.expandActions(rule.actions);
const subjects = Array.isArray(rule.subject) ? rule.subject : rule.subject.split(',');
const subjects = wrapArray(rule.subject);

for (let k = 0; k < subjects.length; k++) {
const subject = subjects[k];
Expand All @@ -80,7 +81,7 @@ export class Ability {
}

expandActions(rawActions) {
const actions = Array.isArray(rawActions) ? rawActions : [rawActions];
const actions = wrapArray(rawActions);
const { aliases } = this[PRIVATE_FIELD];

return actions.reduce((expanded, action) => {
Expand Down
28 changes: 28 additions & 0 deletions packages/casl-ability/src/extra.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,31 @@ export function permittedFieldsOf(ability, action, subject, options = {}) {

return Array.from(uniqueFields);
}

const joinIfArray = value => Array.isArray(value) ? value.join(',') : value;

export function packRules(rules) {
return rules.map(({ actions, subject, inverted, conditions, fields }) => { // eslint-disable-line
const rule = [
joinIfArray(actions),
joinIfArray(subject),
inverted ? 1 : 0,
conditions || 0,
joinIfArray(fields) || 0
];

while (!rule[rule.length - 1]) rule.pop();

return rule;
});
}

export function unpackRules(rules) {
return rules.map(([actions, subject, inverted, conditions, fields]) => ({
actions: actions.split(','),
subject: subject.split(','),
inverted: !!inverted,
conditions: conditions || null,
fields: fields ? fields.split(',') : null
}));
}
19 changes: 9 additions & 10 deletions packages/casl-ability/src/rule.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import sift from 'sift';

function wrapArray(value) {
return Array.isArray(value) ? value : [value];
}
import { wrapArray } from './utils';

export class Rule {
constructor({ conditions, actions, subject, fields, inverted }) { // eslint-disable-line
this.conditions = conditions;
this.actions = actions;
this.subject = subject;
this.fields = !fields || fields.length === 0 ? undefined : wrapArray(fields);
this.inverted = !!inverted;
constructor(params) {
this.actions = params.actions;
this.subject = params.subject;
this.fields = !params.fields || params.fields.length === 0
? undefined
: wrapArray(params.fields);
this.inverted = !!params.inverted;
this.conditions = params.conditions;
this._matches = this.conditions ? sift(this.conditions) : undefined;
}

Expand Down
3 changes: 3 additions & 0 deletions packages/casl-ability/src/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function wrapArray(value) {
return Array.isArray(value) ? value : [value];
}

0 comments on commit c60ab5d

Please sign in to comment.