Skip to content

Commit

Permalink
Merge branch 'master' into feat/graphql-transport-ws-subscription-def…
Browse files Browse the repository at this point in the history
…ault
  • Loading branch information
mcollina committed Dec 20, 2021
2 parents dc23be3 + 86da408 commit 342f18b
Show file tree
Hide file tree
Showing 56 changed files with 5,809 additions and 286 deletions.
4 changes: 4 additions & 0 deletions .github/dependabot.yml
Expand Up @@ -2,6 +2,10 @@ version: 2
updates:
- package-ecosystem: github-actions
directory: "/"
ignore:
- dependency-name: "actions/*"
update-types:
["version-update:semver-minor", "version-update:semver-patch"]
schedule:
interval: daily
open-pull-requests-limit: 10
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Expand Up @@ -8,9 +8,9 @@ jobs:
node-version: [12.x, 14.x, 16.x]
os: [ubuntu-latest, windows-latest, macOS-latest]
steps:
- uses: actions/checkout@v2.3.4
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2.4.1
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- name: Install Dependencies
Expand All @@ -21,6 +21,6 @@ jobs:
needs: test
runs-on: ubuntu-latest
steps:
- uses: fastify/github-action-merge-dependabot@v2.5.0
- uses: fastify/github-action-merge-dependabot@v2.7.1
with:
github-token: ${{secrets.GITHUB_TOKEN}}
4 changes: 4 additions & 0 deletions README.md
@@ -1,3 +1,6 @@

![Mercurius Logo](https://github.com/mercurius-js/graphics/blob/main/mercurius-horizontal.svg)

# mercurius

![CI workflow](https://github.com/mercurius-js/mercurius/workflows/CI%20workflow/badge.svg)
Expand Down Expand Up @@ -34,6 +37,7 @@ Features:
- [Integrations](docs/integrations/)
- [Related Plugins](docs/plugins.md)
- [Protocol Extensions](/docs/protocol-extension.md)
- [Faq](/docs/faq.md)
- [Acknowledgements](#acknowledgements)
- [License](#license)

Expand Down
100 changes: 82 additions & 18 deletions docs/api/options.md
@@ -1,20 +1,26 @@
# mercurius

- [Plugin options](#plugin-options)
- [HTTP endpoints](#http-endpoints)
- [GET /graphql](#get-graphql)
- [POST /graphql](#post-graphql)
- [POST /graphql with Content-type: application/graphql](#post-graphql-with-content-type-applicationgraphql)
- [GET /graphiql](#get-graphiql)
- [Decorators](#decorators)
- [app.graphql(source, context, variables, operationName)](#appgraphqlsource-context-variables-operationname)
- [app.graphql.extendSchema(schema), app.graphql.defineResolvers(resolvers) and app.graphql.defineLoaders(loaders)](#appgraphqlextendschemaschema-appgraphqldefineresolversresolvers-and-appgraphqldefineloadersloaders)
- [app.graphql.replaceSchema(schema)](#appgraphqlreplaceschemaschema)
- [app.graphql.transformSchema(transforms)](#appgraphqltransformschematransforms)
- [app.graphql.schema](#appgraphqlschema)
- [reply.graphql(source, context, variables, operationName)](#replygraphqlsource-context-variables-operationname)
- [Error extensions](#use-errors-extension-to-provide-additional-information-to-query-errors)

- [mercurius](#mercurius)
- [API](#api)
- [Plugin options](#plugin-options)
- [queryDepth example](#querydepth-example)
- [HTTP endpoints](#http-endpoints)
- [GET /graphql](#get-graphql)
- [POST /graphql](#post-graphql)
- [POST /graphql with Content-type: application/graphql](#post-graphql-with-content-type-applicationgraphql)
- [GET /graphiql](#get-graphiql)
- [Decorators](#decorators)
- [app.graphql(source, context, variables, operationName)](#appgraphqlsource-context-variables-operationname)
- [app.graphql.extendSchema(schema), app.graphql.defineResolvers(resolvers) and app.graphql.defineLoaders(loaders)](#appgraphqlextendschemaschema-appgraphqldefineresolversresolvers-and-appgraphqldefineloadersloaders)
- [app.graphql.replaceSchema(schema)](#appgraphqlreplaceschemaschema)
- [app.graphql.transformSchema(transforms)](#appgraphqltransformschematransforms)
- [app.graphql.schema](#appgraphqlschema)
- [reply.graphql(source, context, variables, operationName)](#replygraphqlsource-context-variables-operationname)
- [Errors](#errors)
- [ErrorWithProps](#errorwithprops)
- [Extensions](#extensions)
- [Status code](#status-code)
- [Error formatter](#error-formatter)
## API

### Plugin options
Expand Down Expand Up @@ -43,7 +49,7 @@
- `prefix`: String. Change the route prefix of the graphql endpoint if enabled.
- `defineMutation`: Boolean. Add the empty Mutation definition if schema is not defined (Default: `false`).
- `errorHandler`: `Function`  or `boolean`. Change the default error handler (Default: `true`). _Note: If a custom error handler is defined, it should return the standardized response format according to [GraphQL spec](https://graphql.org/learn/serving-over-http/#response)._
- `errorFormatter`: `Function`. Change the default error formatter. Allows the status code of the response to be set, and a GraphQL response for the error to be defined. This can be used to format errors for batched queries, which return a successful response overall but individual errors, or to obfuscate or format internal errors. The first argument is the error object, while the second one _might_ be the context if it is available.
- `errorFormatter`: `Function`. Change the default error formatter. Allows the status code of the response to be set, and a GraphQL response for the error to be defined. This can be used to format errors for batched queries, which return a successful response overall but individual errors, or to obfuscate or format internal errors. The first argument is the error object, while the second one is the context object.
- `queryDepth`: `Integer`. The maximum depth allowed for a single query. _Note: GraphiQL IDE sends an introspection query when it starts up. This query has a depth of 7 so when the `queryDepth` value is smaller than 7 this query will fail with a `Bad Request` error_
- `validationRules`: `Function` or `Function[]`. Optional additional validation rules that the queries must satisfy in addition to those defined by the GraphQL specification. When using `Function`, arguments include additional data from graphql request and the return value must be validation rules `Function[]`.
- `subscription`: Boolean | Object. Enable subscriptions. It uses [mqemitter](https://github.com/mcollina/mqemitter) when it is true and exposes the pubsub interface to `app.graphql.pubsub`. To use a custom emitter set the value to an object containing the emitter.
Expand All @@ -58,15 +64,17 @@

- `gateway.services`: Service[] An array of GraphQL services that are part of the gateway
- `service.name`: A unique name for the service. Required.
- `service.url`: The url of the service endpoint. Required
- `service.url`: The URL of the service endpoint. It can also be an `Array` of URLs and in which case all the requests will be load balanced throughout the URLs. Required.
- `service.mandatory`: `Boolean` Marks service as mandatory. If any of the mandatory services are unavailable, gateway will exit with an error. (Default: `false`)
- `service.useSecureParse`: `Boolean` Marks if the service response needs to be parsed securely using [secure-json-parse](https://github.com/fastify/secure-json-parse). (Default: `false`)
- `service.rewriteHeaders`: `Function` A function that gets the original headers as a parameter and returns an object containing values that should be added to the headers
- `service.initHeaders`: `Function` or `Object` An object or a function that returns the headers sent to the service for the initial \_service SDL query.
- `service.connections`: The number of clients to create. (Default: `10`)
- `service.bodyTimeout`: The timeout after which a request will time out, in milliseconds. (Default: `30e3` - 30 seconds)
- `service.headersTimeout`: The amount of time the parser will wait to receive the complete HTTP headers, in milliseconds. (Default: `30e3` - 30 seconds)
- `service.keepAliveMaxTimeout`: The maximum allowed keepAliveTimeout. (Default: `5e3` - 5 seconds)
- `service.maxHeaderSize`: The maximum length of request headers in bytes. (Default: `16384` - 16KiB)
- `service.keepAlive`: The amount of time pass between the keep-alive messages sent from the gateway to the service, if `undefined`, no keep-alive messages will be sent. (Default: `undefined`)
- `service.wsUrl`: The url of the websocket endpoint
- `service.wsConnectionParams`: `Function` or `Object`
- `wsConnectionParams.connectionInitPayload`: `Function` or `Object` An object or a function that returns the `connection_init` payload sent to the service.
Expand All @@ -89,6 +97,8 @@
- `notSupportedError?: string`: An error message to return when a query matches `isPersistedQuery`, but returns no valid hash from `getHash`. Defaults to `Bad Request`.
- `allowBatchedQueries`: Boolean. Flag to control whether to allow batched queries. When `true`, the server supports recieving an array of queries and returns an array of results.

- `compilerOptions`: Object. Configurable options for the graphql-jit compiler. For more details check https://github.com/zalando-incubator/graphql-jit

#### queryDepth example

```
Expand Down Expand Up @@ -439,7 +449,33 @@ async function run() {
run()
```

### Use errors extension to provide additional information to query errors
### Errors
Mercurius help the error handling with two useful tools.

- ErrorWithProps class
- ErrorFormatter option

### ErrorWithProps

ErrorWithProps can be used to create Errors to be thrown inside the resolvers or plugins.

it takes 3 parameters:

- message
- extensions
- statusCode

```js
'use strict'

throw new ErrorWithProps('message', {
...
}, 200)
```

#### Extensions

Use errors `extensions` to provide additional information to query errors

GraphQL services may provide an additional entry to errors with the key `extensions` in the result.

Expand Down Expand Up @@ -495,3 +531,31 @@ app.register(mercurius, {

app.listen(3000)
```

#### Status code

To control the status code for the response, the third optional parameter can be used.

```js

throw new mercurius.ErrorWithProps('Invalid User ID', {moreErrorInfo})
// using de defaultErrorFormatter the response statusCode will be 500

throw new mercurius.ErrorWithProps('Invalid User ID', {moreErrorInfo}, 200)
// using de defaultErrorFormatter the response statusCode will be 200

const error = new mercurius.ErrorWithProps('Invalid User ID', {moreErrorInfo}, 500)
error.data = {foo: 'bar'}
throw error
// using de defaultErrorFormatter the response status code will be always 200 because error.data is defined


```

### Error formatter

Allows the status code of the response to be set, and a GraphQL response for the error to be defined.

By default uses the `defaultErrorFormatter`, but it can be overridden in the [mercurius options](/docs/api/options.md#plugin-options) changing the errorFormatter parameter.

**Important**: *using the default formatter, when the error has a data property the response status code will be always 200*
86 changes: 78 additions & 8 deletions docs/federation.md
@@ -1,13 +1,17 @@
# mercurius

- [Federation metadata support](#federation-metadata-support)
- [Federation with \_\_resolveReference caching](#federation-with-__resolvereference-caching)
- [Use GraphQL server as a Gateway for federated schemas](#use-graphql-server-as-a-gateway-for-federated-schemas)
- [Periodically refresh federated schemas in Gateway mode](#periodically-refresh-federated-schemas-in-gateway-mode)
- [Programmatically refresh federated schemas in Gateway mode](#programmatically-refresh-federated-schemas-in-gateway-mode)
- [Using Gateway mode with a schema registry](#using-gateway-mode-with-a-schema-registry)
- [Flag service as mandatory in Gateway mode](#flag-service-as-mandatory-in-gateway-mode)
- [Using a custom errorHandler for handling downstream service errors in Gateway mode](#using-a-custom-errorhandler-for-handling-downstream-service-errors-in-gateway-mode)
- [mercurius](#mercurius)
- [Federation](#federation)
- [Federation metadata support](#federation-metadata-support)
- [Federation with \_\_resolveReference caching](#federation-with-__resolvereference-caching)
- [Use GraphQL server as a Gateway for federated schemas](#use-graphql-server-as-a-gateway-for-federated-schemas)
- [Periodically refresh federated schemas in Gateway mode](#periodically-refresh-federated-schemas-in-gateway-mode)
- [Programmatically refresh federated schemas in Gateway mode](#programmatically-refresh-federated-schemas-in-gateway-mode)
- [Using Gateway mode with a schema registry](#using-gateway-mode-with-a-schema-registry)
- [Flag service as mandatory in Gateway mode](#flag-service-as-mandatory-in-gateway-mode)
- [Batched Queries to services](#batched-queries-to-services)
- [Using a custom errorHandler for handling downstream service errors in Gateway mode](#using-a-custom-errorhandler-for-handling-downstream-service-errors-in-gateway-mode)
- [Securely parse service responses in Gateway mode](#securely-parse-service-responses-in-gateway-mode)

## Federation

Expand Down Expand Up @@ -351,6 +355,41 @@ server.register(mercurius, {
server.listen(3002)
```

#### Batched Queries to services

To fully leverage the DataLoader pattern we can tell the Gateway which of its services support [batched queries](batched-queries.md).
In this case the service will receive a request body with an array of queries to execute.
Enabling batched queries for a service that doesn't support it will generate errors.


```js
const Fastify = require('fastify')
const mercurius = require('mercurius')

const server = Fastify()

server.register(mercurius, {
graphiql: true,
gateway: {
services: [
{
name: 'user',
url: 'http://localhost:3000/graphql'
allowBatchedQueries: true
},
{
name: 'company',
url: 'http://localhost:3001/graphql',
allowBatchedQueries: false
}
]
},
pollingInterval: 2000
})

server.listen(3002)
```

#### Using a custom errorHandler for handling downstream service errors in Gateway mode

Service which uses Gateway mode can process different types of issues that can be obtained from remote services (for example, Network Error, Downstream Error, etc.). A developer can provide a function (`gateway.errorHandler`) that can process these errors.
Expand Down Expand Up @@ -388,3 +427,34 @@ server.listen(3002)
```

_Note: The default behavior of `errorHandler` is call `errorFormatter` to send the result. When is provided an `errorHandler` make sure to **call `errorFormatter` manually if needed**._

#### Securely parse service responses in Gateway mode

Gateway service responses can be securely parsed using the `useSecureParse` flag. By default, the target service is considered trusted and thus this flag is set to `false`. If there is a need to securely parse the JSON response from a service, this flag can be set to `true` and it will use the [secure-json-parse](https://github.com/fastify/secure-json-parse) library.

```js
const Fastify = require('fastify')
const mercurius = require('mercurius')

const server = Fastify()

server.register(mercurius, {
graphiql: true,
gateway: {
services: [
{
name: 'user',
url: 'http://localhost:3000/graphql',
useSecureParse: true
},
{
name: 'company',
url: 'http://localhost:3001/graphql'
}
]
},
pollingInterval: 2000
})

server.listen(3002)
```
16 changes: 11 additions & 5 deletions docs/hooks.md
Expand Up @@ -49,7 +49,7 @@ fastify.graphql.addHook('preParsing', async (schema, source, context) => {

### preValidation

By the time the `preValidation` hook triggers, the query string has been parsed into a GraphQL Document AST.
By the time the `preValidation` hook triggers, the query string has been parsed into a GraphQL Document AST. The hook will not be triggered for cached queries, as they are not validated.

```js
fastify.graphql.addHook('preValidation', async (schema, document, context) => {
Expand All @@ -61,14 +61,18 @@ fastify.graphql.addHook('preValidation', async (schema, document, context) => {

In the `preExecution` hook, you can modify the following items by returning them in the hook definition:
- `document`
- `schema`
- `errors`

Note that if you modify the `schema` or the `document` object, the [jit](./api/options.md#plugin-options) compilation will be disabled for the request.

```js
fastify.graphql.addHook('preExecution', async (schema, document, context) => {
const { modifiedDocument, errors } = await asyncMethod(document)
const { modifiedSchema, modifiedDocument, errors } = await asyncMethod(document)

return {
document: modifiedDocument
schema: modifiedSchema, // ⚠️ changing the schema may break the query execution. Use it carefully.
document: modifiedDocument,
errors
}
})
Expand All @@ -90,14 +94,16 @@ fastify.graphql.addHook('preGatewayExecution', async (schema, document, context,
const { modifiedDocument, errors } = await asyncMethod(document)

return {
document: modifiedDocument
document: modifiedDocument,
errors
}
})
```

### onResolution

The `onResolution` hooks run after the GraphQL query execution and you can access the result via the `execution` argument.

```js
fastify.graphql.addHook('onResolution', async (execution, context) => {
await asyncMethod()
Expand Down Expand Up @@ -141,7 +147,7 @@ Note, the original query will still execute. Adding the above will result in the
```json
{
"data": {
foo: "bar"
"foo": "bar"
},
"errors": [
{
Expand Down
5 changes: 3 additions & 2 deletions docs/integrations/type-graphql.md
Expand Up @@ -13,7 +13,7 @@ Now you can define a schema using classes and decorators:

```ts
// recipe.ts
import { Field, ObjectType, Int, Float, Resolver, Query } from "type-graphql";
import { Arg, Field, ObjectType, Int, Float, Resolver, Query } from "type-graphql";

@ObjectType({ description: "Object representing cooking recipe" })
export class Recipe {
Expand Down Expand Up @@ -44,7 +44,7 @@ export class Recipe {
@Resolver()
export class RecipeResolver {
@Query((returns) => Recipe, { nullable: true })
async recipe(@Arg("title") title: string): Promise<Recipe | undefined> {
async recipe(@Arg("title") title: string): Promise<Omit<Recipe, 'specification'> | undefined> {
return {
description: "Desc 1",
title: "Recipe 1",
Expand All @@ -62,6 +62,7 @@ This can be linked to the Mercurius plugin:
import "reflect-metadata";
import fastify from "fastify";
import mercurius from "mercurius";
import { buildSchema } from 'type-graphql'

import { RecipeResolver } from "./recipe";

Expand Down

0 comments on commit 342f18b

Please sign in to comment.