Skip to content

Commit

Permalink
feat(ability): allows AbilityBuilder to accept the subject's Type (#61)
Browse files Browse the repository at this point in the history
By reusing the subjectName option passed to the Ability, we can
determine the name of the Type when it is passed into the can/cannot
method on the AbilityBuilder. This gives a nice symmetry with the
Ability checking API.

Fixes #58
  • Loading branch information
icedtoast authored and stalniy committed May 9, 2018
1 parent 37cf5b1 commit 0de1bf0
Show file tree
Hide file tree
Showing 6 changed files with 60 additions and 42 deletions.
8 changes: 4 additions & 4 deletions packages/casl-ability/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,11 @@ export class RuleBuilder {
export abstract class AbilityBuilderParts {
rules: RawRule[]

can(action: string | string[], subject: string | string[], conditions?: Object): RuleBuilder
can(action: string | string[], subject: string | string[], fields?: string[], conditions?: Object): RuleBuilder
can(action: string | string[], subject: any | any[], conditions?: Object): RuleBuilder
can(action: string | string[], subject: any | any[], fields?: string[], conditions?: Object): RuleBuilder

cannot(action: string | string[], subject: string | string[], conditions?: Object): RuleBuilder
cannot(action: string | string[], subject: string | string[], fields?: string[], conditions?: Object): RuleBuilder
cannot(action: string | string[], subject: any | any[], conditions?: Object): RuleBuilder
cannot(action: string | string[], subject: any | any[], fields?: string[], conditions?: Object): RuleBuilder
}

export class AbilityBuilder extends AbilityBuilderParts {
Expand Down
20 changes: 10 additions & 10 deletions packages/casl-ability/spec/ability.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ describe('Ability', () => {
})

expect(ability.rules).to.deep.equal([
{ actions: 'manage', subject: 'all' },
{ actions: 'learn', subject: 'Range' },
{ actions: 'read', subject: 'String', inverted: true },
{ actions: 'read', subject: 'Hash', inverted: true },
{ actions: 'preview', subject: 'Array', inverted: true },
{ actions: 'manage', subject: ['all'] },
{ actions: 'learn', subject: ['Range'] },
{ actions: 'read', subject: ['String'], inverted: true },
{ actions: 'read', subject: ['Hash'], inverted: true },
{ actions: 'preview', subject: ['Array'], inverted: true },
])
})

Expand Down Expand Up @@ -471,8 +471,8 @@ describe('Ability', () => {
const rules = ability.rulesFor('read', 'Post').map(ruleToObject)

expect(rules).to.deep.equal([
{ actions: 'read', subject: 'Post', inverted: true, conditions: { private: true } },
{ actions: 'read', subject: 'Post', inverted: false },
{ actions: 'read', subject: ['Post'], inverted: true, conditions: { private: true } },
{ actions: 'read', subject: ['Post'], inverted: false },
])
})

Expand All @@ -485,7 +485,7 @@ describe('Ability', () => {
const rules = ability.rulesFor('read', 'Post').map(ruleToObject)

expect(rules).to.deep.equal([
{ actions: 'read', subject: 'Post', inverted: false },
{ actions: 'read', subject: ['Post'], inverted: false },
])
})

Expand All @@ -498,8 +498,8 @@ describe('Ability', () => {
const rules = ability.rulesFor('read', 'Post', 'title').map(ruleToObject)

expect(rules).to.deep.equal([
{ actions: 'read', subject: 'Post', inverted: true, fields: ['title'] },
{ actions: 'read', subject: 'Post', inverted: false }
{ actions: 'read', subject: ['Post'], inverted: true, fields: ['title'] },
{ actions: 'read', subject: ['Post'], inverted: false }
])
})

Expand Down
36 changes: 25 additions & 11 deletions packages/casl-ability/spec/builder.spec.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { AbilityBuilder, Ability } from '../src'
import { Post } from './spec_helper';

describe('AbilityBuilder', () => {
it('defines `Ability` instance using DSL', () => {
Expand All @@ -9,8 +10,21 @@ describe('AbilityBuilder', () => {

expect(ability).to.be.instanceof(Ability)
expect(ability.rules).to.deep.equal([
{ actions: 'read', subject: 'Book' },
{ inverted: true, actions: 'read', subject: 'Book', conditions: { private: true } }
{ actions: 'read', subject: ['Book'] },
{ inverted: true, actions: 'read', subject: ['Book'], conditions: { private: true } }
])
})

it('defines `Ability` instance using DSL with Constructor', () => {
const ability = AbilityBuilder.define((can, cannot) => {
can('read', Post)
cannot('read', Post, { private: true })
})

expect(ability).to.be.instanceof(Ability)
expect(ability.rules).to.deep.equal([
{ actions: 'read', subject: ['Post'] },
{ inverted: true, actions: 'read', subject: ['Post'], conditions: { private: true } }
])
})

Expand All @@ -22,8 +36,8 @@ describe('AbilityBuilder', () => {

expect(ability).to.be.instanceof(Ability)
expect(ability.rules).to.deep.equal([
{ actions: 'read', subject: 'Book' },
{ inverted: true, actions: 'read', subject: 'Book', conditions: { private: true } }
{ actions: 'read', subject: ['Book'] },
{ inverted: true, actions: 'read', subject: ['Book'], conditions: { private: true } }
])
})

Expand All @@ -43,10 +57,10 @@ describe('AbilityBuilder', () => {
}).to.throw(/to be an action or array of actions/)
})

it('throws exception if the 2nd argument is not a string', () => {
it('throws exception if the 2nd argument is not a string (and no suitable getSubjectName)', () => {
expect(() => {
AbilityBuilder.define(can => can('read', {}))
}).to.throw(/to be a subject name or array of subject names/)
AbilityBuilder.define(can => can('read', null))
}).to.throw(/to be a subject name\/type or an array of subject names\/types/)
})
})

Expand All @@ -65,8 +79,8 @@ describe('AbilityBuilder', () => {
can('read', 'Comment', { private: false })

expect(rules).to.deep.equal([
{ actions: 'read', subject: 'Post' },
{ actions: 'read', subject: 'Comment', conditions: { private: false } }
{ actions: 'read', subject: ['Post'] },
{ actions: 'read', subject: ['Comment'], conditions: { private: false } }
])
})

Expand All @@ -76,8 +90,8 @@ describe('AbilityBuilder', () => {
cannot('read', 'Comment', { private: true })

expect(rules).to.deep.equal([
{ actions: 'read', subject: 'Post' },
{ actions: 'read', subject: 'Comment', conditions: { private: true }, inverted: true }
{ actions: 'read', subject: ['Post'] },
{ actions: 'read', subject: ['Comment'], conditions: { private: true }, inverted: true }
])

})
Expand Down
12 changes: 1 addition & 11 deletions packages/casl-ability/src/ability.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,6 @@
import { ForbiddenError } from './error';
import { Rule } from './rule';
import { wrapArray } from './utils';

function getSubjectName(subject) {
if (!subject || typeof subject === 'string') {
return subject;
}

const Type = typeof subject === 'object' ? subject.constructor : subject;

return Type.modelName || Type.name;
}
import { wrapArray, getSubjectName } from './utils';

function clone(object) {
return JSON.parse(JSON.stringify(object));
Expand Down
16 changes: 10 additions & 6 deletions packages/casl-ability/src/builder.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Ability } from './ability';
import { getSubjectName } from './utils';

function isStringOrNonEmptyArray(value) {
return typeof value === 'string' || Array.isArray(value) && value.length > 0;
return ![].concat(value).some(item => typeof item !== 'string');
}

function isObject(value) {
Expand All @@ -23,7 +24,7 @@ export class AbilityBuilder {
static define(params, dsl) {
const options = typeof params === 'function' ? {} : params;
const define = params === options ? dsl : params;
const builder = new this();
const builder = new this(options);
const result = define(builder.can.bind(builder), builder.cannot.bind(builder));
const buildAbility = () => new Ability(builder.rules, options);

Expand All @@ -40,20 +41,23 @@ export class AbilityBuilder {
};
}

constructor() {
constructor({ subjectName = getSubjectName } = {}) {
this.rules = [];
this.subjectName = subjectName;
}

can(actions, subject, conditionsOrFields, conditions) {
if (!isStringOrNonEmptyArray(actions)) {
throw new TypeError('AbilityBuilder#can expects the first parameter to be an action or array of actions');
}

if (!isStringOrNonEmptyArray(subject)) {
throw new TypeError('AbilityBuilder#can expects the second argument to be a subject name or array of subject names');
const subjectName = [].concat(subject).map(this.subjectName);

if (!isStringOrNonEmptyArray(subjectName)) {
throw new TypeError('AbilityBuilder#can expects the second argument to be a subject name/type or an array of subject names/types');
}

const rule = { actions, subject };
const rule = { actions, subject: subjectName };

if (Array.isArray(conditionsOrFields) || typeof conditionsOrFields === 'string') {
rule.fields = conditionsOrFields;
Expand Down
10 changes: 10 additions & 0 deletions packages/casl-ability/src/utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
export function wrapArray(value) {
return Array.isArray(value) ? value : [value];
}

export function getSubjectName(subject) {
if (!subject || typeof subject === 'string') {
return subject;
}

const Type = typeof subject === 'object' ? subject.constructor : subject;

return Type.modelName || Type.name;
}

0 comments on commit 0de1bf0

Please sign in to comment.