Skip to content

Commit

Permalink
feat: Support input type rules
Browse files Browse the repository at this point in the history
This version features new rule type `inputRule`. You can use `inputRule` to easily validated arguments provided to the function.

Fixes #113
Fixes #262
  • Loading branch information
maticzav committed Feb 18, 2019
1 parent da224f9 commit c89c3c7
Show file tree
Hide file tree
Showing 9 changed files with 256 additions and 25 deletions.
79 changes: 58 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ schema = applyMiddleware(schema, permissions)
### Types

```ts
// Rule
/* Rule */
function rule(
name?: string,
options?: IRuleOptions,
Expand All @@ -183,7 +183,10 @@ interface IRuleOptions {
fragment?: IFragment
}

// Logic
/* Input */
function inputRule(yup: Yup => Yup.Schema): Rule

/* Logic */
function and(...rules: IRule[]): LogicRule
function or(...rules: IRule[]): LogicRule
function not(rule: IRule): LogicRule
Expand Down Expand Up @@ -353,7 +356,18 @@ const server = GraphQLServer({

> If you wish to see errors thrown inside resolvers, you can set `allowExternalErrors` option to `true`. This way, Shield won't hide custom errors thrown during query resolving.

#### per type wildcard rule
#### `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. |

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.

### Per Type Wildcard Rule

There is an option to specify a rule that will be applied to all fields of a type (`Query`, `Mutatation`, ...) that do not specify a rule.
It is similar to the `options.fallbackRule` but allows you to specify a `fallbackRule` per type.
Expand All @@ -377,36 +391,59 @@ const permissions = shield({
})
```

#### `options`
### Basic rules

| 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. |
> `allow`, `deny` are GraphQL Shield predefined rules.

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.
`allow` and `deny` rules do exactly what their names describe.

### `allow`, `deny`
### Rules on Input Types or Arguments

> GraphQL Shield predefined rules.
> Validate arguments using [Yup](https://github.com/jquense/yup).

`allow` and `deny` rules do exactly what their names describe.
```ts
function inputRule(name?: string, yup: Yup => Yup.Schema): Rule
```

Input rule works exactly as any other rule would work. Instead of providing a complex validation rule you can simply provide a Yup validation schema which will be mached against provided arguments.
This can be especially useful when limiting optional fields such as `create` and `connect` with Prisma, for example.

**Example:**

```graphql
type Mutation {
login(email: String): LoginPayload
}
```

Note that Yup receives entire `args` object, therefore, you should start composing schema with an object.

```ts
const isEmailEmail = inputRule(yup =>
yup.object({
email: yup
.string()
.email('It has to be an email!')
.required(),
}),
)
```

### Logic Rules

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

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

#### And Rule
##### `and` rule

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

#### Or Rule
##### `or` rule

`Or` rule allows access if at least one sub rule returns `true` and no rule throws an error.

#### Not
##### not

`Not` works as usual not in code works.

Expand Down Expand Up @@ -438,7 +475,7 @@ const permissions = shield({
})
```

### `Global Fallback Error`
### Global Fallback Error

GraphQL Shield allows you to set a globally defined fallback error that is used instead of `Not Authorised!` default response. This might be particularly useful for localization. You can use `string` or even custom `Error` to define it.

Expand Down Expand Up @@ -466,7 +503,7 @@ const permissions = shield(
)
```

### `Fragments`
### Fragments

Fragments allow you to define which fields your rule requires to work correctly. This comes in extremely handy when your rules rely on data from database. You can use fragments to define which data your rule relies on.

Expand Down Expand Up @@ -529,7 +566,7 @@ const server = new GraphQLServer({
const { schema, fragmentReplacements } = applyMiddleware(schema, permissions)
```

### `Whitelisting vs Blacklisting`
### Whitelisting vs Blacklisting

> Whitelisting/Blacklisting is no longer available in versions after `3.x.x`, and has been replaced in favor of `fallbackRule`.

Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@
"postinstall": "lightercollective postinstall"
},
"dependencies": {
"lightercollective": "^0.2.0",
"object-hash": "^1.3.1",
"lightercollective": "^0.2.0"
"yup": "^0.26.10"
},
"devDependencies": {
"@types/graphql": "14.0.7",
"@types/jest": "23.3.14",
"@types/node": "10.12.26",
"@types/object-hash": "1.2.0",
"@types/request-promise-native": "1.0.15",
"@types/yup": "^0.26.9",
"apollo-server": "2.4.2",
"codecov": "3.2.0",
"coveralls": "3.0.2",
Expand Down
28 changes: 27 additions & 1 deletion src/constructors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
import * as Yup from 'yup'
import { IRuleFunction, IRuleConstructorOptions, ShieldRule } from './types'
import { Rule, RuleAnd, RuleOr, RuleNot, RuleTrue, RuleFalse } from './rules'
import {
Rule,
RuleAnd,
RuleOr,
RuleNot,
RuleTrue,
RuleFalse,
InputRule,
} from './rules'

/**
*
Expand Down Expand Up @@ -49,6 +58,23 @@ export const rule = (
})
}

/**
*
* Constructs a new InputRule based on the schema.
*
* @param schema
*/
export const inputRule = <T>(
name: string | ((yup: typeof Yup) => Yup.Schema<T>),
schema?: (yup: typeof Yup) => Yup.Schema<T>,
) => {
if (typeof name === 'string') {
return new InputRule(name, schema(Yup))
} else {
return new InputRule(Math.random().toString(), name(Yup))
}
}

/**
*
* @param rules
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { IRules, IRule } from './types'
export { shield } from './shield'
export { rule, allow, deny, and, or, not } from './constructors'
export { rule, inputRule, allow, deny, and, or, not } from './constructors'
13 changes: 13 additions & 0 deletions src/rules.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as hash from 'object-hash'
import * as Yup from 'yup'
import {
IRuleFunction,
IRule,
Expand Down Expand Up @@ -164,6 +165,18 @@ export class Rule implements IRule {
}
}

export class InputRule<Schema> extends Rule {
constructor(name: string, schema: Yup.Schema<Schema>) {
const validationFunction = (parent, args) =>
schema
.validate(args)
.then(() => true)
.catch(err => err)

super(name, validationFunction, { cache: 'strict', fragment: undefined })
}
}

export class LogicRule implements ILogicRule {
private rules: ShieldRule[]

Expand Down
7 changes: 7 additions & 0 deletions tests/__snapshots__/input.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`input rule schema validation works as expected 1`] = `
Array [
[GraphQLError: It has to be an email!],
]
`;
34 changes: 33 additions & 1 deletion tests/constructors.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { rule, and, or, not, allow, deny } from '../src/constructors'
import * as Yup from 'yup'
import { rule, and, or, not, allow, deny, inputRule } from '../src/constructors'
import {
RuleAnd,
RuleOr,
RuleNot,
RuleTrue,
RuleFalse,
Rule,
InputRule,
} from '../src/rules'

describe('rule constructor', () => {
Expand Down Expand Up @@ -68,6 +70,36 @@ describe('rule constructor', () => {
})
})

describe('input rules constructor', () => {
test('correnctly constructs an input rule with name', async () => {
const name = Math.random().toString()
let schema: Yup.ObjectSchema<{}>

const rule = inputRule(name, yup => {
schema = yup.object().shape({})
return schema
})
expect(JSON.stringify(rule)).toEqual(
JSON.stringify(new InputRule(name, schema)),
)
})

test('correnctly constructs an input rule', async () => {
const n = Math.random()
jest.spyOn(Math, 'random').mockReturnValue(n)

let schema: Yup.ObjectSchema<{}>

const rule = inputRule(yup => {
schema = yup.object().shape({})
return schema
})
expect(JSON.stringify(rule)).toEqual(
JSON.stringify(new InputRule(n.toString(), schema)),
)
})
})

describe('logic rules constructors', () => {
test('and correctly constructs rule and', async () => {
const ruleA = rule()(() => true)
Expand Down
65 changes: 65 additions & 0 deletions tests/input.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { graphql } from 'graphql'
import { applyMiddleware } from 'graphql-middleware'
import { makeExecutableSchema } from 'graphql-tools'
import { shield, inputRule } from '../src'

describe('input rule', () => {
test('schema validation works as expected', async () => {
const typeDefs = `
type Query {
hello: String!
}
type Mutation {
login(email: String!): String
}
`

const resolvers = {
Query: {
hello: () => 'world',
},
Mutation: {
login: () => 'pass',
},
}

const schema = makeExecutableSchema({ typeDefs, resolvers })

// Permissions
const permissions = shield({
Mutation: {
login: inputRule(yup =>
yup.object({
email: yup
.string()
.email('It has to be an email!')
.required(),
}),
),
},
})

const schemaWithPermissions = applyMiddleware(schema, permissions)

/* Execution */

const query = `
mutation {
success: login(email: "[email protected]")
failure: login(email: "notemail")
}
`
const res = await graphql(schemaWithPermissions, query)

console.log(res)

/* Tests */

expect(res.data).toEqual({
success: 'pass',
failure: null,
})
expect(res.errors).toMatchSnapshot()
})
})
Loading

0 comments on commit c89c3c7

Please sign in to comment.