Skip to content

Commit

Permalink
refactor(ability): changes signature of rulesToQuery to be similar to…
Browse files Browse the repository at this point in the history
… permittedFieldsOf

This protects user from mistakes like passing rules of different subjects/actions into rulesToQuery. Also refactores toMongoQuery to have similar signature but a bit different, action goes as the last argument, so we can specify default action to be `read` (as in majority of cases this is what users will want)

Relates to #18
  • Loading branch information
stalniy committed Mar 21, 2018
1 parent bb8d3c8 commit 3ce8b4e
Show file tree
Hide file tree
Showing 6 changed files with 73 additions and 54 deletions.
23 changes: 12 additions & 11 deletions packages/casl-ability/spec/query.spec.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
import { AbilityBuilder } from '../src'
import { rulesToQuery } from '../src/extra'

function toQuery(rules) {
return rulesToQuery(rules, rule => {
function toQuery(ability, action, subject) {
return rulesToQuery(ability, action, subject, rule => {
return rule.inverted ? { $not: rule.conditions } : rule.conditions
})
}

describe('rulesToQuery', () => {
it('returns empty object if there are no rules with conditions', () => {
const ability = AbilityBuilder.define(can => can('read', 'Post'))
const query = toQuery(ability.rulesFor('read', 'Post'))
const query = toQuery(ability, 'read', 'Post')

expect(Object.keys(query)).to.be.empty
})

it('returns `null` if empty list rules is passed', () => {
const query = toQuery([])
it('returns `null` if empty `Ability` instance is passed', () => {
const ability = AbilityBuilder.define(() => {})
const query = toQuery(ability, 'read', 'Post')

expect(query).to.be.null
})
Expand All @@ -26,7 +27,7 @@ describe('rulesToQuery', () => {
can('read', 'Post', { author: 123 })
can('read', 'Post')
})
const query = toQuery(ability.rulesFor('read', 'Post'))
const query = toQuery(ability, 'read', 'Post')

expect(Object.keys(query)).to.be.empty
})
Expand All @@ -36,7 +37,7 @@ describe('rulesToQuery', () => {
cannot('read', 'Post', { author: 123 })
cannot('read', 'Post')
})
const query = toQuery(ability.rulesFor('read', 'Post'))
const query = toQuery(ability, 'read', 'Post')

expect(query).to.be.null
})
Expand All @@ -47,7 +48,7 @@ describe('rulesToQuery', () => {
cannot('read', 'Post', { author: 321 })
cannot('read', 'Post')
})
const query = toQuery(ability.rulesFor('read', 'Post'))
const query = toQuery(ability, 'read', 'Post')

expect(query).to.be.null
})
Expand All @@ -57,7 +58,7 @@ describe('rulesToQuery', () => {
can('read', 'Post', { status: 'draft', createdBy: 'someoneelse' })
can('read', 'Post', { status: 'published', createdBy: 'me' })
})
const query = toQuery(ability.rulesFor('read', 'Post'))
const query = toQuery(ability, 'read', 'Post')

expect(query).to.deep.equal({
$or: [
Expand All @@ -72,7 +73,7 @@ describe('rulesToQuery', () => {
cannot('read', 'Post', { status: 'draft', createdBy: 'someoneelse' })
cannot('read', 'Post', { status: 'published', createdBy: 'me' })
})
const query = toQuery(ability.rulesFor('read', 'Post'))
const query = toQuery(ability, 'read', 'Post')

expect(query).to.deep.equal({
$and: [
Expand All @@ -89,7 +90,7 @@ describe('rulesToQuery', () => {
cannot('read', 'Post', { private: true })
cannot('read', 'Post', { state: 'archived' })
})
const query = toQuery(ability.rulesFor('read', 'Post'))
const query = toQuery(ability, 'read', 'Post')

expect(query).to.deep.equal({
$or: [
Expand Down
3 changes: 2 additions & 1 deletion packages/casl-ability/src/extra.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export function rulesToQuery(rules, convert) {
export function rulesToQuery(ability, action, subject, convert) {
const query = {};
const ignoreOperators = {};
const rules = ability.rulesFor(action, subject);

for (let i = 0; i < rules.length; i++) {
const rule = rules[i];
Expand Down
88 changes: 53 additions & 35 deletions packages/casl-mongoose/spec/mongo-query.spec.js
Original file line number Diff line number Diff line change
@@ -1,92 +1,110 @@
import { AbilityBuilder } from '@casl/ability'
import { toMongoQuery } from '../src'

describe('Mongo Query builder', () => {
const { can, cannot } = AbilityBuilder.extract()
describe('toMongoQuery', () => {
it('accepts ability action as third argument', () => {
const ability = AbilityBuilder.define((can, cannot) => {
can('update', 'Post', { _id: 'mega' })
})
const query = toMongoQuery(ability, 'Post', 'update')

expect(query).to.deep.equal({
$or: [{ _id: 'mega' }]
})
})

it('OR-es conditions for regular rules and AND-es for inverted ones', () => {
const query = toMongoQuery([
can('read', 'Post', { _id: 'mega' }),
can('read', 'Post', { state: 'draft' }),
cannot('read', 'Post', { private: true }),
const ability = AbilityBuilder.define((can, cannot) => {
can('read', 'Post', { _id: 'mega' })
can('read', 'Post', { state: 'draft' })
cannot('read', 'Post', { private: true })
cannot('read', 'Post', { state: 'archived' })
])
})
const query = toMongoQuery(ability, 'Post')

expect(query).to.deep.equal({
$or: [
{ _id: 'mega' },
{ state: 'draft' }
{ state: 'draft' },
{ _id: 'mega' }
],
$and: [
{ $nor: [{ private: true }] },
{ $nor: [{ state: 'archived' }] }
{ $nor: [{ state: 'archived' }] },
{ $nor: [{ private: true }] }
]
})
})

describe('can find records where property', () => {
it('is present', () => {
const query = toMongoQuery([
const ability = AbilityBuilder.define(can => {
can('read', 'Post', { isPublished: { $exists: true, $ne: null } })
])
})
const query = toMongoQuery(ability, 'Post')

expect(query).to.deep.equal({ $or: [{ isPublished: { $exists: true, $ne: null } }] })
})

it('is blank', () => {
const query = toMongoQuery([
can('read', 'Post', { isPublished: { $exists: false } }),
const ability = AbilityBuilder.define(can => {
can('read', 'Post', { isPublished: { $exists: false } })
can('read', 'Post', { isPublished: null })
])
})
const query = toMongoQuery(ability, 'Post')

expect(query).to.deep.equal({ $or: [{ isPublished: { $exists: false } }, { isPublished: null }] })
expect(query).to.deep.equal({ $or: [{ isPublished: null }, { isPublished: { $exists: false } }] })
})

it('is defined by `$in` criteria', () => {
const query = toMongoQuery([
const ability = AbilityBuilder.define(can => {
can('read', 'Post', { state: { $in: ['draft', 'archived'] } })
])
})
const query = toMongoQuery(ability, 'Post')

expect(query).to.deep.equal({ $or: [{ state: { $in: ['draft', 'archived'] } }] })
})

it('is defined by `$all` criteria', () => {
const query = toMongoQuery([
const ability = AbilityBuilder.define(can => {
can('read', 'Post', { state: { $all: ['draft', 'archived'] } })
])
})
const query = toMongoQuery(ability, 'Post')

expect(query).to.deep.equal({ $or: [{ state: { $all: ['draft', 'archived'] } }] })
})
it('is defined by `$lt` and `$lte` criteria', () => {
const query = toMongoQuery([
can('read', 'Post', { views: { $lt: 10 } }),
can('update', 'Post', { views: { $lt: 5 } })
])
const ability = AbilityBuilder.define(can => {
can('read', 'Post', { views: { $lt: 10 } })
can('read', 'Post', { views: { $lt: 5 } })
})
const query = toMongoQuery(ability, 'Post')

expect(query).to.deep.equal({ $or: [{ views: { $lt: 10 } }, { views: { $lt: 5 } }] })
expect(query).to.deep.equal({ $or: [{ views: { $lt: 5 } }, { views: { $lt: 10 } }] })
})

it('is defined by `$gt` and `$gte` criteria', () => {
const query = toMongoQuery([
can('read', 'Post', { views: { $gt: 10 } }),
can('update', 'Post', { views: { $gte: 100 } })
])
const ability = AbilityBuilder.define(can => {
can('read', 'Post', { views: { $gt: 10 } })
can('read', 'Post', { views: { $gte: 100 } })
})
const query = toMongoQuery(ability, 'Post')

expect(query).to.deep.equal({ $or: [{ views: { $gt: 10 } }, { views: { $gte: 100 } }] })
expect(query).to.deep.equal({ $or: [{ views: { $gte: 100 } }, { views: { $gt: 10 } }] })
})

it('is defined by `$ne` criteria', () => {
const query = toMongoQuery([
const ability = AbilityBuilder.define(can => {
can('read', 'Post', { creator: { $ne: 'me' } })
])
})
const query = toMongoQuery(ability, 'Post')

expect(query).to.deep.equal({ $or: [{ creator: { $ne: 'me' } }] })
})

it('is defined by dot notation fields', () => {
const query = toMongoQuery([
const ability = AbilityBuilder.define(can => {
can('read', 'Post', { 'comments.author': 'Ted' })
])
})
const query = toMongoQuery(ability, 'Post')

expect(query).to.deep.equal({ $or: [{ 'comments.author': 'Ted' }] })
})
Expand Down
2 changes: 1 addition & 1 deletion packages/casl-mongoose/spec/mongoose-plugin.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ describe('Mongoose Plugin', () => {
})

it('passes query created by `toMongoQuery` in `find` method of the query', () => {
const query = toMongoQuery(ability.rulesFor('read', 'Post'))
const query = toMongoQuery(ability, 'Post')
spy.on(schema.query, 'find')
schema.query.accessibleBy(ability)

Expand Down
6 changes: 3 additions & 3 deletions packages/casl-mongoose/src/mongo.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { rulesToQuery } from '@casl/ability/extra';

function ruleToMongoQuery(rule) {
function convertToMongoQuery(rule) {
return rule.inverted ? { $nor: [rule.conditions] } : rule.conditions;
}

export function toMongoQuery(rules) {
return rulesToQuery(rules, ruleToMongoQuery);
export function toMongoQuery(ability, subject, action = 'read') {
return rulesToQuery(ability, action, subject, convertToMongoQuery);
}
5 changes: 2 additions & 3 deletions packages/casl-mongoose/src/mongoose.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ function emptyQuery(query) {
return query;
}

function accessibleBy(ability, action = 'read') {
const rules = ability.rulesFor(action, this.modelName || this.model.modelName);
const query = toMongoQuery(rules);
function accessibleBy(ability, action) {
const query = toMongoQuery(ability, this.modelName || this.model.modelName, action);

return query === null ? emptyQuery(this.find()) : this.find(query);
}
Expand Down

0 comments on commit 3ce8b4e

Please sign in to comment.