Skip to content

Commit

Permalink
perf: Prevent application of middleware to introspection types.
Browse files Browse the repository at this point in the history
BREAKING CHANGE: This version no longer wraps introspection types in middleware.

Fixes maticzav#75

I believe this is a meaningful step because we shouldn't rely on changing introspection query to gain additional functionality. From my experience, this has been extensively used in libraries as `graphql-shield` which prevented the introspection of the schema. Nevertheless, I have a strong belief that robust systems shouldn't rely on hiding fields and types, but instead create a meaningfully rounded permission system.

I suggest you use prior versions up to `3.0.0` (`2.x.x`) in case your application heavily relies on the wrapping of introspection types.

I have also added integration tests for `ApolloServer` and `GraphQL Yoga`.
  • Loading branch information
maticzav committed Dec 18, 2018
1 parent 5c247cd commit 142934f
Show file tree
Hide file tree
Showing 8 changed files with 1,420 additions and 110 deletions.
8 changes: 7 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,11 @@
"**/.git/subtree-cache/**": true,
"**/node_modules/**": true
},
"typescript.tsdk": "./node_modules/typescript/lib"
"typescript.tsdk": "./node_modules/typescript/lib",
"spellright.language": [
"en"
],
"spellright.documentTypes": [
"plaintext"
]
}
34 changes: 18 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ Split up your GraphQL resolvers in middleware functions.

GraphQL Middleware is a schema wrapper which allows you to manage additional functionality across multiple resolvers efficiently.

* **Easiest way to handle GraphQL middleware:** An intuitive, yet familiar API that you will pick up in a second.
* **Powerful:** Allows complete control over your resolvers (Before, After).
* **Compatible:** Works with any GraphQL Schema.
* **Remote:** Accepts `fragments` in resolvers to connect with remote schemas.
- **Easiest way to handle GraphQL middleware:** An intuitive, yet familiar API that you will pick up in a second.
- **Powerful:** Allows complete control over your resolvers (Before, After).
- **Compatible:** Works with any GraphQL Schema.
- **Remote:** Accepts `fragments` in resolvers to connect with remote schemas.

> **NOTE:** As of 3.0.0 `graphql-middleware` no longer wraps introspection queries.
## Install

Expand Down Expand Up @@ -149,11 +151,11 @@ server.start(() => console.log('Server is running on localhost:4000'))

## Awesome Middlewares [![Awesome](https://awesome.re/badge.svg)](https://awesome.re)

* [graphql-middleware-apollo-upload-server](http://github.com/homeroom-live/graphql-middleware-apollo-upload-server) - Uploading files is hard, that's why this package manages it for you!
* [graphql-shield](https://github.com/maticzav/graphql-shield) - Permissions as another layer of abstraction.
* [graphql-middleware-sentry](https://github.com/maticzav/graphql-middleware-sentry) - Report your server errors to Sentry.
* [graphql-middleware-forward-binding](https://github.com/maticzav/graphql-middleware-forward-binding) - GraphQL Binding forwardTo plugin for GraphQL Middleware.
* [graphql-yup-middleware](https://github.com/JCMais/graphql-yup-middleware) - Use yup to validate mutation arguments
- [graphql-middleware-apollo-upload-server](http://github.com/homeroom-live/graphql-middleware-apollo-upload-server) - Uploading files is hard, that's why this package manages it for you!
- [graphql-shield](https://github.com/maticzav/graphql-shield) - Permissions as another layer of abstraction.
- [graphql-middleware-sentry](https://github.com/maticzav/graphql-middleware-sentry) - Report your server errors to Sentry.
- [graphql-middleware-forward-binding](https://github.com/maticzav/graphql-middleware-forward-binding) - GraphQL Binding forwardTo plugin for GraphQL Middleware.
- [graphql-yup-middleware](https://github.com/JCMais/graphql-yup-middleware) - Use yup to validate mutation arguments

## API

Expand Down Expand Up @@ -306,13 +308,13 @@ const { schema, fragmentReplacements } = applyMiddleware(
## GraphQL Middleware Use Cases

* Logging
* Metrics
* Input sanitisation
* Performance measurement
* Authorization
* Caching
* Tracing
- Logging
- Metrics
- Input sanitisation
- Performance measurement
- Authorization
- Caching
- Tracing

## FAQ

Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,14 @@
},
"devDependencies": {
"@types/graphql": "^14.0.3",
"@types/request-promise-native": "^1.0.15",
"apollo-server": "^2.3.1",
"ava": "^0.25.0",
"graphql": "^14.0.2",
"graphql-yoga": "^1.16.7",
"prettier": "^1.15.3",
"prettier-check": "^2.0.0",
"request-promise-native": "^1.0.5",
"rimraf": "^2.6.2",
"semantic-release": "^15.13.1",
"ts-node": "^7.0.1",
Expand Down
7 changes: 6 additions & 1 deletion src/applicator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
GraphQLField,
GraphQLSchema,
defaultFieldResolver,
isIntrospectionType,
} from 'graphql'
import {
IMiddlewareFunction,
Expand Down Expand Up @@ -141,7 +142,11 @@ function applyMiddlewareToSchema<TSource, TContext, TArgs>(
const typeMap = schema.getTypeMap()

const resolvers = Object.keys(typeMap)
.filter(type => isGraphQLObjectType(typeMap[type]))
.filter(
type =>
isGraphQLObjectType(typeMap[type]) &&
!isIntrospectionType(typeMap[type]),
)
.reduce(
(resolvers, type) => ({
...resolvers,
Expand Down
64 changes: 0 additions & 64 deletions src/test/fragments.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,38 +50,6 @@ test('Applies schema middleware with fragments correctly.', async t => {
{ field: 'name', fragment: 'schema-fragment' },
{ field: 'content', fragment: 'schema-fragment' },
{ field: 'author', fragment: 'schema-fragment' },
{ field: 'types', fragment: 'schema-fragment' },
{ field: 'queryType', fragment: 'schema-fragment' },
{ field: 'mutationType', fragment: 'schema-fragment' },
{ field: 'subscriptionType', fragment: 'schema-fragment' },
{ field: 'directives', fragment: 'schema-fragment' },
{ field: 'kind', fragment: 'schema-fragment' },
{ field: 'name', fragment: 'schema-fragment' },
{ field: 'description', fragment: 'schema-fragment' },
{ field: 'fields', fragment: 'schema-fragment' },
{ field: 'interfaces', fragment: 'schema-fragment' },
{ field: 'possibleTypes', fragment: 'schema-fragment' },
{ field: 'enumValues', fragment: 'schema-fragment' },
{ field: 'inputFields', fragment: 'schema-fragment' },
{ field: 'ofType', fragment: 'schema-fragment' },
{ field: 'name', fragment: 'schema-fragment' },
{ field: 'description', fragment: 'schema-fragment' },
{ field: 'args', fragment: 'schema-fragment' },
{ field: 'type', fragment: 'schema-fragment' },
{ field: 'isDeprecated', fragment: 'schema-fragment' },
{ field: 'deprecationReason', fragment: 'schema-fragment' },
{ field: 'name', fragment: 'schema-fragment' },
{ field: 'description', fragment: 'schema-fragment' },
{ field: 'type', fragment: 'schema-fragment' },
{ field: 'defaultValue', fragment: 'schema-fragment' },
{ field: 'name', fragment: 'schema-fragment' },
{ field: 'description', fragment: 'schema-fragment' },
{ field: 'isDeprecated', fragment: 'schema-fragment' },
{ field: 'deprecationReason', fragment: 'schema-fragment' },
{ field: 'name', fragment: 'schema-fragment' },
{ field: 'description', fragment: 'schema-fragment' },
{ field: 'locations', fragment: 'schema-fragment' },
{ field: 'args', fragment: 'schema-fragment' }
])
})

Expand Down Expand Up @@ -271,38 +239,6 @@ test('Applies schema middleware with fragments correctly on declared resolvers.'

t.deepEqual(fragmentReplacements, [
{ field: 'book', fragment: 'schema-fragment' },
{ field: 'types', fragment: 'schema-fragment' },
{ field: 'queryType', fragment: 'schema-fragment' },
{ field: 'mutationType', fragment: 'schema-fragment' },
{ field: 'subscriptionType', fragment: 'schema-fragment' },
{ field: 'directives', fragment: 'schema-fragment' },
{ field: 'kind', fragment: 'schema-fragment' },
{ field: 'name', fragment: 'schema-fragment' },
{ field: 'description', fragment: 'schema-fragment' },
{ field: 'fields', fragment: 'schema-fragment' },
{ field: 'interfaces', fragment: 'schema-fragment' },
{ field: 'possibleTypes', fragment: 'schema-fragment' },
{ field: 'enumValues', fragment: 'schema-fragment' },
{ field: 'inputFields', fragment: 'schema-fragment' },
{ field: 'ofType', fragment: 'schema-fragment' },
{ field: 'name', fragment: 'schema-fragment' },
{ field: 'description', fragment: 'schema-fragment' },
{ field: 'args', fragment: 'schema-fragment' },
{ field: 'type', fragment: 'schema-fragment' },
{ field: 'isDeprecated', fragment: 'schema-fragment' },
{ field: 'deprecationReason', fragment: 'schema-fragment' },
{ field: 'name', fragment: 'schema-fragment' },
{ field: 'description', fragment: 'schema-fragment' },
{ field: 'type', fragment: 'schema-fragment' },
{ field: 'defaultValue', fragment: 'schema-fragment' },
{ field: 'name', fragment: 'schema-fragment' },
{ field: 'description', fragment: 'schema-fragment' },
{ field: 'isDeprecated', fragment: 'schema-fragment' },
{ field: 'deprecationReason', fragment: 'schema-fragment' },
{ field: 'name', fragment: 'schema-fragment' },
{ field: 'description', fragment: 'schema-fragment' },
{ field: 'locations', fragment: 'schema-fragment' },
{ field: 'args', fragment: 'schema-fragment' }
])
})

Expand Down
106 changes: 106 additions & 0 deletions src/test/integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import test from 'ava'
import { makeExecutableSchema } from 'graphql-tools'
import { GraphQLServer as YogaServer } from 'graphql-yoga'
import { gql, ApolloServer } from 'apollo-server'
import * as request from 'request-promise-native'
import { AddressInfo } from 'ws'
import { applyMiddleware, middleware, IMiddleware } from '../'

test('Works with GraphQL Yoga', async t => {
const typeDefs = `
type Query {
test: String!
}
`

const resolvers = {
Query: {
test: () => 'test',
},
}

const schema = makeExecutableSchema({ typeDefs, resolvers })

const schemaWithMiddleware = applyMiddleware(schema, async resolve => {
const res = await resolve()
return `pass-${res}`
})

const server = new YogaServer({
schema: schemaWithMiddleware,
})

const http = await server.start({ port: 0 })
const { port } = http.address() as AddressInfo
const uri = `http://localhost:${port}/`

/* Tests */

const query = `
query {
test
}
`

const body = await request({
uri,
method: 'POST',
json: true,
body: { query },
}).promise()

t.deepEqual(body, {
data: {
test: 'pass-test',
},
})
})

test('Works with ApolloServer', async t => {
const typeDefs = `
type Query {
test: String!
}
`

const resolvers = {
Query: {
test: () => 'test',
},
}

const schema = makeExecutableSchema({ typeDefs, resolvers })

const schemaWithMiddleware = applyMiddleware(schema, async resolve => {
const res = await resolve()
return `pass-${res}`
})

const server = new ApolloServer({
schema: schemaWithMiddleware,
})

await server.listen({ port: 8008 })
const uri = `http://localhost:8008/`

/* Tests */

const query = `
query {
test
}
`

const body = await request({
uri,
method: 'POST',
json: true,
body: { query },
}).promise()

t.deepEqual(body, {
data: {
test: 'pass-test',
},
})
})
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"sourceMap": true,
"rootDir": "src",
"outDir": "dist",
"lib": ["esnext", "dom"]
"lib": ["esnext", "dom"],
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
Expand Down
Loading

0 comments on commit 142934f

Please sign in to comment.