Skip to content

Commit

Permalink
feat: Add support for chain rule
Browse files Browse the repository at this point in the history
Add support for chain logic rule
  • Loading branch information
maticzav authored Jun 19, 2019
2 parents 3363af5 + 181571a commit eddf97a
Show file tree
Hide file tree
Showing 6 changed files with 188 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .github/stale.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ daysUntilClose: 10
# Issues with these labels will never be considered stale
exemptLabels:
- kind/feature
- reproduction-available
- kind/bug
- help-wanted
# Label to use when marking an issue as stale
staleLabel: stale
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -435,14 +435,20 @@ const isEmailEmail = inputRule(yup =>

### Logic Rules

#### `and`, `or`, `not`
#### `and`, `or`, `not`, `chain`

> `and`, `or` and `not` allow you to nest rules in logic operations.

##### `and` rule

`And` rule allows access only if all sub rules used return `true`.

##### `chain` rule

`Chain` rule allows you to chain the rules, meaning that rules won't be executed all at once, but one by one until one fails or all pass.

> The left-most rule is executed first.

##### `or` rule

`Or` rule allows access if at least one sub rule returns `true` and no rule throws an error.
Expand Down
12 changes: 12 additions & 0 deletions src/constructors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
RuleTrue,
RuleFalse,
InputRule,
RuleChain,
} from './rules'

/**
Expand Down Expand Up @@ -86,6 +87,17 @@ export const and = (...rules: ShieldRule[]): RuleAnd => {
return new RuleAnd(rules)
}

/**
*
* @param rules
*
* Logical operator and serves as a wrapper for and operation.
*
*/
export const chain = (...rules: ShieldRule[]): RuleChain => {
return new RuleChain(rules)
}

/**
*
* @param rules
Expand Down
68 changes: 68 additions & 0 deletions src/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,74 @@ export class RuleAnd extends LogicRule {
}
}

export class RuleChain extends LogicRule {
constructor(rules: ShieldRule[]) {
super(rules)
}

/**
*
* @param parent
* @param args
* @param ctx
* @param info
*
* Makes sure that all of them have resolved to true.
*
*/
async resolve(
parent,
args,
ctx,
info,
options: IOptions,
): Promise<IRuleResult> {
const result = await this.evaluate(parent, args, ctx, info, options)

if (result.some(res => res !== true)) {
const customError = result.find(res => res instanceof Error)
return customError || false
} else {
return true
}
}

/**
*
* @param parent
* @param args
* @param ctx
* @param info
*
* Evaluates all the rules.
*
*/
async evaluate(
parent,
args,
ctx,
info,
options: IOptions,
): Promise<IRuleResult[]> {
const rules = this.getRules()
const tasks = rules.reduce<Promise<IRuleResult[]>>(
(acc, rule) =>
acc.then(res => {
if (res.some(r => r !== true)) {
return res
} else {
return rule
.resolve(parent, args, ctx, info, options)
.then(task => res.concat(task))
}
}),
Promise.resolve([]),
)

return tasks
}
}

export class RuleNot extends LogicRule {
constructor(rule: ShieldRule) {
super([rule])
Expand Down
18 changes: 17 additions & 1 deletion tests/constructors.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import * as Yup from 'yup'
import { rule, and, or, not, allow, deny, inputRule } from '../src/constructors'
import {
rule,
and,
or,
not,
allow,
deny,
inputRule,
chain,
} from '../src/constructors'
import {
RuleAnd,
RuleOr,
Expand All @@ -8,6 +17,7 @@ import {
RuleFalse,
Rule,
InputRule,
RuleChain,
} from '../src/rules'

describe('rule constructor', () => {
Expand Down Expand Up @@ -107,6 +117,12 @@ describe('logic rules constructors', () => {
expect(and(ruleA, ruleB)).toEqual(new RuleAnd([ruleA, ruleB]))
})

test('chain correctly constructs rule chain', async () => {
const ruleA = rule()(() => true)
const ruleB = rule()(() => true)
expect(chain(ruleA, ruleB)).toEqual(new RuleChain([ruleA, ruleB]))
})

test('or correctly constructs rule or', async () => {
const ruleA = rule()(() => true)
const ruleB = rule()(() => true)
Expand Down
84 changes: 83 additions & 1 deletion tests/logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { applyMiddleware } from 'graphql-middleware'
import { makeExecutableSchema } from 'graphql-tools'
import { shield, rule, allow, deny, and, or, not } from '../src'
import { LogicRule } from '../src/rules'
import { chain } from '../src/constructors'

describe('logic rules', () => {
test('allow, deny work as expeted', async () => {
Expand Down Expand Up @@ -92,6 +93,7 @@ describe('logic rules', () => {
query {
allow
deny
ruleError
}
`
const res = await graphql(schemaWithPermissions, query)
Expand All @@ -101,8 +103,88 @@ describe('logic rules', () => {
expect(res.data).toEqual({
allow: 'allow',
deny: null,
ruleError: null,
})
expect(res.errors.length).toBe(1)
expect(res.errors.length).toBe(2)
})

test('chain works as expected', async () => {
const typeDefs = `
type Query {
allow: String
deny: String
ruleError: String
}
`

const resolvers = {
Query: {
allow: () => 'allow',
deny: () => 'deny',
ruleError: () => 'error',
},
}

const schema = makeExecutableSchema({ typeDefs, resolvers })

/* Permissions */

let allowRuleSequence = []
const allowRuleA = rule()(() => {
allowRuleSequence.push('A')
return true
})
const allowRuleB = rule()(() => {
allowRuleSequence.push('B')
return true
})
const allowRuleC = rule()(() => {
allowRuleSequence.push('C')
return true
})
let denyRuleCount = 0
const denyRule = rule({})(() => {
denyRuleCount += 1
return false
})
let ruleWithErrorCount = 0
const ruleWithError = rule()(() => {
ruleWithErrorCount += 1
throw new Error('error')
})

const permissions = shield({
Query: {
allow: chain(allowRuleA, allowRuleB, allowRuleC),
deny: chain(denyRule, denyRule, denyRule),
ruleError: chain(ruleWithError, ruleWithError, ruleWithError),
},
})

const schemaWithPermissions = applyMiddleware(schema, permissions)

/* Execution */

const query = `
query {
allow
deny
ruleError
}
`
const res = await graphql(schemaWithPermissions, query)

/* Tests */

expect(res.data).toEqual({
allow: 'allow',
deny: null,
ruleError: null,
})
expect(allowRuleSequence.toString()).toEqual(['A', 'B', 'C'].toString())
expect(denyRuleCount).toEqual(1)
expect(ruleWithErrorCount).toEqual(1)
expect(res.errors.length).toBe(2)
})

test('or works as expected', async () => {
Expand Down

0 comments on commit eddf97a

Please sign in to comment.