Skip to content

Commit

Permalink
feat: Support custom hash function
Browse files Browse the repository at this point in the history
Ability to specify your own hash function
  • Loading branch information
maticzav authored Jun 19, 2019
2 parents c491937 + f24adb9 commit 2371ea6
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 22 deletions.
16 changes: 10 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,13 +231,16 @@ interface IRuleTypeMap {

type IRules = ShieldRule | IRuleTypeMap

type IHashFunction = (arg: { parent: any; args: any }) => string

// Generator Options

interface IOptions {
debug?: boolean
allowExternalErrors?: boolean
fallbackRule?: ShieldRule
fallbackError?: string | Error
hashFunction?: IHashFunction
}

declare function shield(
Expand Down Expand Up @@ -358,12 +361,13 @@ const server = GraphQLServer({

#### `options`

| Property | Required | Default | Description |
| ------------------- | -------- | ------------------------ | -------------------------------------------------- |
| allowExternalErrors | false | false | Toggle catching internal errors. |
| debug | false | false | Toggle debug mode. |
| fallbackRule | false | allow | The default rule for every "rule-undefined" field. |
| fallbackError | false | Error('Not Authorised!') | Error Permission system fallbacks to. |
| Property | Required | Default | Description |
| ------------------- | -------- | ---------------------------------------------------- | -------------------------------------------------- |
| allowExternalErrors | false | false | Toggle catching internal errors. |
| debug | false | false | Toggle debug mode. |
| fallbackRule | false | allow | The default rule for every "rule-undefined" field. |
| fallbackError | false | Error('Not Authorised!') | Error Permission system fallbacks to. |
| hashFunction | false | [object-hash](https://github.com/puleos/object-hash) | Hashing function to use for `strict` cache |

By default `shield` ensures no internal data is exposed to client if it was not meant to be. Therefore, all thrown errors during execution resolve in `Not Authorised!` error message if not otherwise specified using `error` wrapper. This can be turned off by setting `allowExternalErrors` option to true.

Expand Down
21 changes: 13 additions & 8 deletions src/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ import {
isIntrospectionType,
GraphQLResolveInfo,
} from 'graphql'
import { IRules, IOptions, ShieldRule, IRuleFieldMap } from './types'
import {
IRules,
IOptions,
ShieldRule,
IRuleFieldMap,
IShieldContext,
} from './types'
import {
isRuleFunction,
isRuleFieldMap,
Expand All @@ -36,20 +42,19 @@ function generateFieldMiddlewareFromRule(
resolve: (parent, args, ctx, info) => any,
parent: { [key: string]: any },
args: { [key: string]: any },
ctx: any,
ctx: IShieldContext,
info: GraphQLResolveInfo,
) {
// Cache
if (!ctx) {
ctx = {}
ctx = {} as IShieldContext
}

if (!ctx._shield) {
ctx._shield = {}
}

if (!ctx._shield.cache) {
ctx._shield.cache = {}
ctx._shield = {
cache: {},
hashFunction: options.hashFunction,
}
}

// Execution
Expand Down
12 changes: 5 additions & 7 deletions src/rules.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as hash from 'object-hash'
import * as Yup from 'yup'
import {
IRuleFunction,
Expand All @@ -12,6 +11,7 @@ import {
ShieldRule,
IRuleResult,
IOptions,
IShieldContext,
} from './types'
import { isLogicRule } from './utils'

Expand Down Expand Up @@ -44,7 +44,7 @@ export class Rule implements IRule {
async resolve(
parent,
args,
ctx,
ctx: IShieldContext,
info,
options: IOptions,
): Promise<IRuleResult> {
Expand Down Expand Up @@ -146,13 +146,11 @@ export class Rule implements IRule {
* Generates cache key based on cache option.
*
*/
private generateCacheKey(parent, args, ctx, info): string {
private generateCacheKey(parent, args, ctx: IShieldContext, info): string {
switch (this.cache) {
case 'strict': {
const key = hash({
parent,
args,
})
const key = ctx._shield.hashFunction({ parent, args })

return `${this.name}-${key}`
}
case 'contextual': {
Expand Down
10 changes: 9 additions & 1 deletion src/shield.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import * as hash from 'object-hash'
import { middleware, IMiddlewareGenerator } from 'graphql-middleware'
import { ValidationError, validateRuleTree } from './validation'
import { IRules, IOptions, IOptionsConstructor, ShieldRule } from './types'
import {
IRules,
IOptions,
IOptionsConstructor,
ShieldRule,
IHashFunction,
} from './types'
import { generateMiddlewareGeneratorFromRuleTree } from './generator'
import { allow } from './constructors'
import { withDefault } from './utils'
Expand All @@ -25,6 +32,7 @@ function normalizeOptions(options: IOptionsConstructor): IOptions {
fallbackError: withDefault(new Error('Not Authorised!'))(
options.fallbackError,
),
hashFunction: withDefault<IHashFunction>(hash)(options.hashFunction),
}
}

Expand Down
11 changes: 11 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,23 +64,34 @@ export interface IRuleFieldMap {

export type IRules = ShieldRule | IRuleTypeMap

export type IHashFunction = (arg: { parent: any; args: any }) => string

// Generator Options

export interface IOptions {
debug: boolean
allowExternalErrors: boolean
fallbackRule: ShieldRule
fallbackError: Error
hashFunction: IHashFunction
}

export interface IOptionsConstructor {
debug?: boolean
allowExternalErrors?: boolean
fallbackRule?: ShieldRule
fallbackError?: string | Error
hashFunction?: IHashFunction
}

export declare function shield<TSource = any, TContext = any, TArgs = any>(
ruleTree: IRules,
options: IOptions,
): IMiddlewareGenerator<TSource, TContext, TArgs>

export interface IShieldContext {
_shield: {
cache: { [key: string]: IRuleResult | Promise<IRuleResult> }
hashFunction: IHashFunction
}
}
68 changes: 68 additions & 0 deletions tests/cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { graphql } from 'graphql'
import { applyMiddleware } from 'graphql-middleware'
import { makeExecutableSchema } from 'graphql-tools'
import { shield, rule } from '../src/index'
import { IHashFunction } from '../src/types'

describe('Caching works as expected', () => {
test('Strict cache - Rule is called multiple times, based on different parent.', async () => {
Expand Down Expand Up @@ -263,6 +264,73 @@ describe('Caching works as expected', () => {
})
})

test('Customize hash function', async () => {
/* Schema */
const typeDefs = `
type Query {
a(arg: String): String!
b(arg: String): String!
}
`
const resolvers = {
Query: {
a: () => 'a',
b: () => 'b',
},
}

const schema = makeExecutableSchema({
typeDefs,
resolvers,
})

/* Tests */

const allowMock = jest.fn().mockResolvedValue(true)

const hashFunction: IHashFunction = jest.fn(opts => JSON.stringify(opts))

const permissions = shield(
{
Query: rule({ cache: 'strict' })(allowMock),
},
{
hashFunction,
},
)

const schemaWithPermissions = applyMiddleware(schema, permissions)

/* Execution */

const query = `
query {
a(arg: "foo")
b(arg: "bar")
}
`
const res = await graphql(schemaWithPermissions, query, undefined, {})

/* Tests */

expect(res).toEqual({
data: {
a: 'a',
b: 'b',
},
})
expect(allowMock).toBeCalledTimes(2)
expect(hashFunction).toHaveBeenCalledTimes(2)
expect(hashFunction).toHaveBeenCalledWith({
parent: undefined,
args: { arg: 'foo' },
})
expect(hashFunction).toHaveBeenCalledWith({
parent: undefined,
args: { arg: 'bar' },
})
})

describe('Legacy cache', () => {
test('Strict cache - Rule is called multiple times, based on different parent.', async () => {
/* Schema */
Expand Down
1 change: 1 addition & 0 deletions tests/logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ describe('internal execution', () => {
debug: false,
fallbackRule: undefined,
fallbackError: new Error(),
hashFunction: () => `${Math.random()}`,
},
)

Expand Down

0 comments on commit 2371ea6

Please sign in to comment.