Skip to content

Commit 081f79b

Browse files
authored
feat: apollo 4 support, remove express-graphql support (#147)
fixes #128 #131 #136
1 parent 9aaf846 commit 081f79b

23 files changed

+1110
-267
lines changed

README.md

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,9 @@ app.use('/', yoga)
118118
app.listen(4000);
119119
```
120120

121-
#### Apollo Server
121+
#### Apollo 3 Server
122122

123-
As an [Apollo Server](https://www.apollographql.com/docs/apollo-server/) plugin
123+
As an [Apollo 3 Server](https://www.apollographql.com/docs/apollo-server/v3) plugin
124124

125125
```js
126126
const { createApolloQueryValidationPlugin, constraintDirectiveTypeDefs } = require('graphql-constraint-directive')
@@ -162,21 +162,68 @@ await server.start()
162162

163163
server.applyMiddleware({ app })
164164
```
165-
#### Apollo subgraph server
166165

167-
There is a small change required to make the Apollo Server quickstart work when trying to build an [Apollo Subgraph Server](https://www.apollographql.com/docs/federation/building-supergraphs/subgraphs-apollo-server/).
166+
#### Apollo 4 Server
167+
168+
As an [Apollo 4 Server](https://www.apollographql.com/docs/apollo-server/v4) plugin
169+
170+
```js
171+
const { createApollo4QueryValidationPlugin, constraintDirectiveTypeDefs } = require('graphql-constraint-directive/apollo4')
172+
const express = require('express')
173+
const { ApolloServer } = require('@apollo/server')
174+
const { makeExecutableSchema } = require('@graphql-tools/schema')
175+
const cors = require('cors')
176+
const { json } = require('body-parser')
168177

169-
Notably, we need to wrap our `typDefs` with the `gql` tag, from either the `graphql-tag` or the `apollo-server-core` packages. This converts the `typeDefs` to an `AST` or `DocumentNode` format and is required by `buildSubgraphSchema`, as mentioned in their [docs](https://www.apollographql.com/docs/federation/building-supergraphs/subgraphs-apollo-server/):
170-
>While Apollo Server can accept a string (or `DocumentNode`) for its `typeDefs`, the `buildSubgraphSchema` function below requires the schema we pass in to be a `DocumentNode`.
178+
const typeDefs = `
179+
type Query {
180+
books: [Book]
181+
}
182+
type Book {
183+
title: String
184+
}
185+
type Mutation {
186+
createBook(input: BookInput): Book
187+
}
188+
input BookInput {
189+
title: String! @constraint(minLength: 5, format: "email")
190+
}`
171191

172-
Then, we must use the `buildSubgraphSchema` function to build a schema that can be passed to an Apollo Gateway/supergraph, instead of `makeExecuteableSchema`. This uses `makeExecutableSchema` under the hood.
192+
let schema = makeExecutableSchema({
193+
typeDefs: [constraintDirectiveTypeDefs, typeDefs],
194+
})
195+
196+
const plugins = [
197+
createApollo4QueryValidationPlugin({
198+
schema
199+
})
200+
]
201+
202+
const app = express()
203+
const server = new ApolloServer({
204+
schema,
205+
plugins
206+
})
207+
208+
await server.start()
209+
210+
app.use(
211+
'/',
212+
cors(),
213+
json(),
214+
expressMiddleware(server)
215+
)
216+
```
217+
#### Apollo 4 Subgraph server
218+
219+
There is a small change required to make the Apollo Server quickstart work when trying to build an [Apollo Subgraph Server](https://www.apollographql.com/docs/federation/building-supergraphs/subgraphs-apollo-server/).
220+
We must use the `buildSubgraphSchema` function to build a schema that can be passed to an Apollo Gateway/supergraph, instead of `makeExecuteableSchema`. This uses `makeExecutableSchema` under the hood.
173221

174222
```ts
175223
import { ApolloServer } from '@apollo/server';
176224
import { startStandaloneServer } from '@apollo/server/standalone';
177-
import { gql } from 'graphql-tag'; // Or can be imported from 'apollo-server-core'
178225
import { buildSubgraphSchema } from '@apollo/subgraph';
179-
import { createApolloQueryValidationPlugin, constraintDirectiveTypeDefs } from 'graphql-constraint-directive';
226+
import { createApollo4QueryValidationPlugin, constraintDirectiveTypeDefsGql } from 'graphql-constraint-directive/apollo4';
180227

181228
const typeDefs = gql`
182229
extend schema @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key", "@shareable"])
@@ -196,11 +243,11 @@ const typeDefs = gql`
196243
`;
197244

198245
const schema = buildSubgraphSchema({
199-
typeDefs: [gql(constraintDirectiveTypeDefs), typeDefs]
246+
typeDefs: [constraintDirectiveTypeDefsGql, typeDefs]
200247
});
201248

202249
const plugins = [
203-
createApolloQueryValidationPlugin({
250+
createApollo4QueryValidationPlugin({
204251
schema
205252
})
206253
]
@@ -215,6 +262,8 @@ await startStandaloneServer(server);
215262

216263
#### Express
217264

265+
*This implementation is untested now, as [`express-graphql` module](https://github.com/graphql/express-graphql) is not maintained anymore.*
266+
218267
As a [Validation rule](https://graphql.org/graphql-js/validation/) when query `variables` are available
219268

220269
```js
@@ -358,11 +407,16 @@ app.use('/graphql', bodyParser.json(), graphqlExpress({ schema, formatError }))
358407

359408
```
360409
361-
#### Apollo Server
362-
Throws a [`UserInputError`](https://www.apollographql.com/docs/apollo-server/data/errors/#bad_user_input) for each validation error
410+
#### Apollo Server 3
411+
Throws a [`UserInputError`](https://www.apollographql.com/docs/apollo-server/data/errors/#bad_user_input) for each validation error.
412+
413+
#### Apollo Server 4
414+
Throws a prefilled `GraphQLError` with `extensions.code` set to `BAD_USER_INPUT` and http status code `400`.
415+
In case of more validation errors, top level error is generic with `Query is invalid, for details see extensions.validationErrors` message,
416+
detailed errors are stored in `extensions.validationErrors` of this error.
363417
364418
#### Envelop
365-
The Envelop plugin throws a prefilled `GraphQLError` for each validation error
419+
The Envelop plugin throws a prefilled `GraphQLError` for each validation error.
366420
367421
### uniqueTypeName
368422
```@constraint(uniqueTypeName: "Unique_Type_Name")```

apollo4.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import {GraphQLSchema} from "graphql";
2+
import {ApolloServerPlugin} from '@apollo/server';
3+
4+
export const constraintDirectiveTypeDefs: string
5+
6+
export function createApollo4QueryValidationPlugin ( options: { schema: GraphQLSchema } ) : ApolloServerPlugin;

apollo4.js

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
const {
2+
separateOperations,
3+
GraphQLError
4+
} = require('graphql')
5+
const { validateQuery } = require('./index')
6+
const { constraintDirectiveTypeDefs } = require('./lib/type-defs')
7+
const { gql } = require('graphql-tag')
8+
9+
function createApollo4QueryValidationPlugin ({ schema }) {
10+
return {
11+
async requestDidStart () {
12+
return ({
13+
async didResolveOperation (requestContext) {
14+
const { request, document } = requestContext
15+
const query = request.operationName
16+
? separateOperations(document)[request.operationName]
17+
: document
18+
19+
const errors = validateQuery(
20+
schema,
21+
query,
22+
request.variables,
23+
request.operationName
24+
)
25+
26+
if (errors.length > 0) {
27+
const te = errors.map(err => {
28+
return new GraphQLError(err.message, {
29+
extensions: {
30+
field: err.fieldName,
31+
context: err.context,
32+
code: 'BAD_USER_INPUT',
33+
http: {
34+
status: 400
35+
}
36+
}
37+
})
38+
})
39+
if (te.length === 1) {
40+
throw te[0]
41+
} else {
42+
throw new GraphQLError('Query is invalid, for details see extensions.validationErrors', {
43+
extensions: {
44+
code: 'BAD_USER_INPUT',
45+
validationErrors: te,
46+
http: {
47+
status: 400
48+
}
49+
}
50+
})
51+
}
52+
}
53+
}
54+
})
55+
}
56+
}
57+
}
58+
59+
const constraintDirectiveTypeDefsGql = gql(constraintDirectiveTypeDefs)
60+
61+
module.exports = { constraintDirectiveTypeDefs, constraintDirectiveTypeDefsGql, createApollo4QueryValidationPlugin }

index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ function createEnvelopQueryValidationPlugin () {
147147
onExecute ({ args, setResultAndStopExecution }) {
148148
const errors = validateQuery(args.schema, args.document, args.variableValues, args.operationName)
149149
if (errors.length > 0) {
150-
setResultAndStopExecution({ errors: errors.map(err => { return new GraphQLError(err.message, null, null, null, null, err, { code: err.code, field: err.fieldName, context: err.context, exception: err.originalError }) }) })
150+
setResultAndStopExecution({ errors: errors.map(err => { return new GraphQLError(err.message, err, { code: err.code, field: err.fieldName, context: err.context, exception: err.originalError }) }) })
151151
}
152152
}
153153
}

lib/query-validation-visitor.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,9 @@ module.exports = class QueryValidationVisitor {
9696
onFieldEnter (node) {
9797
// prepere current field info, so onArgumentEnter() can use it
9898
this.currentField = node
99-
this.currentrFieldDef = this.currentTypeInfo.typeDef.getFields()[node.name.value]
99+
100+
// this if handles union type correctly
101+
if (this.currentTypeInfo.typeDef.getFields) { this.currentrFieldDef = this.currentTypeInfo.typeDef.getFields()[node.name.value] }
100102

101103
if (this.currentrFieldDef) {
102104
const newTypeDef = getNamedType(this.currentrFieldDef.type)

0 commit comments

Comments
 (0)