Skip to content

Commit d2f6e25

Browse files
committedApr 25, 2018
feat: validationRules as callback + query complexity example
1 parent 188d926 commit d2f6e25

File tree

6 files changed

+172
-4
lines changed

6 files changed

+172
-4
lines changed
 

‎examples/query-complexity/README.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# GraphQL Query Complexity / Cost analysis
2+
3+
This directory contains an example with query complexity analysis based on `graphql-yoga` and [`graphql-cost-analysis`](https://github.com/pa-bru/graphql-cost-analysis).
4+
5+
## Get started
6+
7+
**Clone the repository:**
8+
9+
```sh
10+
git clone https://github.com/graphcool/graphql-yoga.git
11+
cd graphql-yoga/examples/query-complexity
12+
```
13+
14+
**Install dependencies and run the app:**
15+
16+
```sh
17+
yarn install # or npm install
18+
yarn start # or npm start
19+
```
20+
21+
## Testing
22+
23+
Open your browser at [http://localhost:4000](http://localhost:4000) and start sending queries.
24+
25+
### Simple query
26+
27+
```graphql
28+
{
29+
posts(limit:1) {
30+
id
31+
title
32+
}
33+
}
34+
```
35+
36+
#### `200` Response
37+
38+
```json
39+
{
40+
"data": {
41+
"posts": [
42+
{
43+
"id": 0,
44+
"title": "My first blog post"
45+
}
46+
]
47+
}
48+
}
49+
```
50+
51+
### Too complex query
52+
53+
```graphql
54+
{
55+
posts(limit:5) {
56+
id
57+
title
58+
}
59+
}
60+
```
61+
62+
#### `400` response:
63+
64+
```json
65+
{
66+
"errors": [
67+
{
68+
"message": "The query exceeds the maximum cost of 50. Actual cost is 52"
69+
}
70+
]
71+
}
72+
```
73+
74+
## Implementation
75+
76+
The query complexity is calculated with the help of the `@cost`-directive defined in [`index.js`](./index.js).
77+
78+
## Further reading
79+
80+
81+
- [`graphql-cost-analysis` Documentation](https://github.com/pa-bru/graphql-cost-analysis)
82+
- [How to GraphQL: Security and GraphQL Tutorial](https://www.howtographql.com/advanced/4-security/)
83+
- [Apollo: Securing Your GraphQL API from Malicious Queries
84+
](https://dev-blog.apollodata.com/securing-your-graphql-api-from-malicious-queries-16130a324a6b)

‎examples/query-complexity/index.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
const { GraphQLServer } = require('../../dist/src/index')
2+
const { default: costAnalysis } = require('graphql-cost-analysis')
3+
4+
const typeDefs = `
5+
type Query {
6+
posts(limit: Int!): [Post!]!
7+
@cost(multipliers: ["limit"], complexity: 10)
8+
}
9+
10+
type Post {
11+
id: Int!
12+
title: String!
13+
}
14+
15+
directive @cost(
16+
complexity: Int
17+
useMultipliers: Boolean
18+
multipliers: [String!]
19+
) on FIELD_DEFINITION
20+
`
21+
22+
const posts = [
23+
'My first blog post',
24+
'My second',
25+
'My third',
26+
'My fourth',
27+
'My fifth',
28+
].map((title, index) => ({
29+
id: index,
30+
title,
31+
}))
32+
33+
const resolvers = {
34+
Query: {
35+
posts: (source, {limit}) => posts.slice(0, limit),
36+
},
37+
}
38+
39+
const server = new GraphQLServer({
40+
typeDefs,
41+
resolvers,
42+
})
43+
44+
server.start({
45+
validationRules: (req) => [
46+
costAnalysis({
47+
variables: req.query.variables,
48+
maximumCost: 50,
49+
defaultCost: 1,
50+
onComplete(cost) {
51+
console.log(`Cost analysis score: ${cost}`)
52+
},
53+
})
54+
]
55+
}).then(() => {
56+
console.log('Server is running on http://localhost:4000')
57+
}).catch(() => {
58+
console.error('Server start failed', err)
59+
process.exit(1)
60+
})
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"scripts": {
3+
"start": "node ."
4+
},
5+
"dependencies": {
6+
"graphql-cost-analysis": "^1.0.1"
7+
}
8+
}

‎src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,10 @@ export class GraphQLServer {
234234
formatError: this.options.formatError || defaultErrorFormatter,
235235
logFunction: this.options.logFunction,
236236
rootValue: this.options.rootValue,
237-
validationRules: this.options.validationRules,
237+
validationRules:
238+
typeof this.options.validationRules === 'function'
239+
? this.options.validationRules(request, response)
240+
: this.options.validationRules,
238241
fieldResolver: this.options.fieldResolver || customFieldResolver,
239242
formatParams: this.options.formatParams,
240243
formatResponse: this.options.formatResponse,

‎src/lambda.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { makeExecutableSchema } from 'graphql-tools'
77
import * as path from 'path'
88
import customFieldResolver from './customFieldResolver'
99

10-
import { LambdaOptions, LambdaProps } from './types'
10+
import { LambdaOptions, LambdaProps, ValidationRules } from './types'
1111

1212
export class GraphQLServerLambda {
1313
options: LambdaOptions
@@ -85,6 +85,12 @@ export class GraphQLServerLambda {
8585
throw e
8686
}
8787

88+
if (typeof this.options.validationRules === 'function') {
89+
throw new Error(
90+
'validationRules as callback is only compatible with Express',
91+
)
92+
}
93+
8894
return {
8995
schema: this.executableSchema,
9096
tracing: tracing(event),
@@ -93,7 +99,7 @@ export class GraphQLServerLambda {
9399
formatError: this.options.formatError,
94100
logFunction: this.options.logFunction,
95101
rootValue: this.options.rootValue,
96-
validationRules: this.options.validationRules,
102+
validationRules: this.options.validationRules as ValidationRules,
97103
fieldResolver: this.options.fieldResolver || customFieldResolver,
98104
formatParams: this.options.formatParams,
99105
formatResponse: this.options.formatResponse,

‎src/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,13 +52,20 @@ export interface TracingOptions {
5252
mode: 'enabled' | 'disabled' | 'http-header'
5353
}
5454

55+
export type ValidationRules = Array<(context: ValidationContext) => any>
56+
57+
export type ValidationRulesExpressCallback = (
58+
request: Request,
59+
response: Response,
60+
) => ValidationRules
61+
5562
export interface ApolloServerOptions {
5663
tracing?: boolean | TracingOptions
5764
cacheControl?: boolean
5865
formatError?: Function
5966
logFunction?: LogFunction
6067
rootValue?: any
61-
validationRules?: Array<(context: ValidationContext) => any>
68+
validationRules?: ValidationRules | ValidationRulesExpressCallback
6269
fieldResolver?: GraphQLFieldResolver<any, any>
6370
formatParams?: Function
6471
formatResponse?: Function

0 commit comments

Comments
 (0)
Please sign in to comment.