Skip to content

Commit

Permalink
feat(ability): adds generic types to Ability and related types [skip ci]
Browse files Browse the repository at this point in the history
deprecates `actions` field in `Rule` types use `action` instead
deprecates `AbilityBuilder.define` use `AbilityBuilder.extract` instead
removes default `crud` alias. If you want it use `Ability.addAlias('crud', ['create', 'read', 'update', 'delete'])`
removes possibility to pass `RuleType` option in `Ability` constructor. Use `conditionsMatcher` and `fieldMatcher` options instead.
`packRules` and `unpackRules` now accepts optional second argument which packs/unpacks subject type. This is useful when you want to use Classes as subject types

Fixes #256 BREAKING CHANGES
  • Loading branch information
stalniy committed Feb 13, 2020
1 parent d8cd5bc commit 9db0a25
Show file tree
Hide file tree
Showing 16 changed files with 470 additions and 387 deletions.
2 changes: 1 addition & 1 deletion packages/casl-ability/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"scripts": {
"build.core": "rollup -c ../../tools/rollup.config.js -i src/index.ts -g sift:sift -n casl",
"build.extra": "rollup -c ../../tools/rollup.config.js -i src/extra.ts -n casl.extra",
"build.types": "tsc",
"build.types": "rm -rf ./dist/types/* && tsc",
"build": "npm run build.core && npm run build.extra && npm run build.types",
"lint": "eslint --ext .js,.ts src/ spec/",
"test": "NODE_ENV=test jest --config ../../tools/jest.config.js --env node",
Expand Down
48 changes: 14 additions & 34 deletions packages/casl-ability/spec/ability.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AbilityBuilder, Ability } from '../src'
import { Post } from './spec_helper'
import { Post, ruleToObject } from './spec_helper'

describe('Ability', () => {
let ability
Expand Down Expand Up @@ -32,17 +32,6 @@ describe('Ability', () => {
expect(() => Ability.addAlias('sort', ['order', 'sort'])).to.throw(Error)
})

it('provides predefined `crud` alias for `create`, `read`, `update` and `delete` actions', () => {
ability = AbilityBuilder.define(can => can('crud', 'Post'))

expect(ability).to.allow('crud', 'Post')
expect(ability).to.allow('create', 'Post')
expect(ability).to.allow('read', 'Post')
expect(ability).to.allow('update', 'Post')
expect(ability).to.allow('delete', 'Post')
expect(ability).not.to.allow('any other action', 'Post')
})

it('provides `can` and `cannot` methods to check abilities', () => {
ability = AbilityBuilder.define(can => can('read', 'Post'))

Expand All @@ -59,12 +48,12 @@ describe('Ability', () => {
cannot('preview', 'Array')
})

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

Expand Down Expand Up @@ -232,7 +221,7 @@ describe('Ability', () => {

it('shadows rule with conditions by the same rule without conditions', () => {
ability = AbilityBuilder.define((can) => {
can('crud', 'Post')
can('delete', 'Post')
can('delete', 'Post', { creator: 'me' })
})

Expand All @@ -254,7 +243,7 @@ describe('Ability', () => {
it('shadows inverted rule by regular one', () => {
ability = AbilityBuilder.define((can, cannot) => {
cannot('delete', 'Post', { creator: 'me' })
can('crud', 'Post', { creator: 'me' })
can('delete', 'Post', { creator: 'me' })
})

expect(ability).to.allow('delete', new Post({ creator: 'me' }))
Expand Down Expand Up @@ -600,8 +589,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 },
{ action: 'read', subject: 'Post', inverted: true, conditions: { private: true } },
{ action: 'read', subject: 'Post' },
])
})

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

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

Expand All @@ -627,18 +616,9 @@ 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 }
{ action: 'read', subject: 'Post', inverted: true, fields: ['title'] },
{ action: 'read', subject: 'Post' }
])
})

function ruleToObject(rule) {
return ['actions', 'subject', 'conditions', 'fields', 'inverted'].reduce((object, field) => {
if (typeof rule[field] !== 'undefined') {
object[field] = rule[field]
}
return object
}, {})
}
})
})
39 changes: 21 additions & 18 deletions packages/casl-ability/spec/builder.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { AbilityBuilder, Ability } from '../src'
import { Post } from './spec_helper'
import { Post, ruleToObject } from './spec_helper'

describe('AbilityBuilder', () => {
it('defines `Ability` instance using DSL', () => {
Expand All @@ -9,9 +9,9 @@ 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 } }
expect(ability.rules.map(ruleToObject)).to.deep.equal([
{ action: 'read', subject: 'Book' },
{ inverted: true, action: 'read', subject: 'Book', conditions: { private: true } }
])
})

Expand All @@ -22,9 +22,9 @@ describe('AbilityBuilder', () => {
})

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 } }
expect(ability.rules.map(ruleToObject)).to.deep.equal([
{ action: 'read', subject: Post },
{ inverted: true, action: 'read', subject: Post, conditions: { private: true } }
])
})

Expand All @@ -35,9 +35,9 @@ 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 } }
expect(ability.rules.map(ruleToObject)).to.deep.equal([
{ action: 'read', subject: 'Book' },
{ inverted: true, action: 'read', subject: 'Book', conditions: { private: true } }
])
})

Expand All @@ -57,12 +57,15 @@ describe('AbilityBuilder', () => {
cannot('read', 'Book', { private: true }).because(reason)
})

expect(ability.rules).to.deep.eql([
{ actions: 'read', subject: ['Book'] },
expect(ability.rules.map(ruleToObject)).to.deep.eql([
{
action: 'read',
subject: 'Book'
},
{
inverted: true,
actions: 'read',
subject: ['Book'],
action: 'read',
subject: 'Book',
conditions: { private: true },
reason
}
Expand Down Expand Up @@ -112,8 +115,8 @@ describe('AbilityBuilder', () => {
can('read', 'Comment', { private: false })

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

Expand All @@ -123,8 +126,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 }
{ action: 'read', subject: 'Post' },
{ action: 'read', subject: 'Comment', conditions: { private: true }, inverted: true }
])
})
})
Expand Down
28 changes: 14 additions & 14 deletions packages/casl-ability/spec/pack_rules.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,82 +4,82 @@ describe('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' }
{ action: 'read', subject: 'Post' },
{ action: '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' }])
const rules = packRules([{ action: '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' }])
const rules = packRules([{ action: ['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' }])
const rules = packRules([{ action: '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'] }])
const rules = packRules([{ action: 'read', subject: ['Post', 'Comment'] }])

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

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

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

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'] }])
const rules = packRules([{ action: 'read', subject: 'Post', fields: ['title'] }])

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

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

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

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

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

it('puts `0` in place of `fields` when reason is provided and fields are not', () => {
const reason = 'forbidden reason'
const rules = packRules([{ actions: 'read', subject: 'Post', reason }])
const rules = packRules([{ action: 'read', subject: 'Post', reason }])

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

it('puts `reason` as 6th element of rule array', () => {
const reason = 'forbidden reason'
const rules = packRules([{ actions: 'read', subject: 'Post', reason }])
const rules = packRules([{ action: 'read', subject: 'Post', reason }])

expect(rules[0][5]).to.equal(reason)
expect(rules[0]).to.have.length(6)
Expand All @@ -99,13 +99,13 @@ describe('Ability rules packing', () => {
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'])
expect(rules[0].action).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'])
expect(rules[0].action).to.deep.equal(['read'])
})

it('puts 2nd element under `subject` field and converts it to an array', () => {
Expand Down
10 changes: 10 additions & 0 deletions packages/casl-ability/spec/spec_helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,13 @@ export class Post {
Object.assign(this, attrs)
}
}

export function ruleToObject(rule) {
const fields = ['action', 'subject', 'conditions', 'fields', 'inverted', 'reason']
return fields.reduce((object, field) => {
if (rule[field]) {
object[field] = rule[field]
}
return object
}, {})
}
Loading

0 comments on commit 9db0a25

Please sign in to comment.