├── .github ├── dependabot.yml ├── release-drafter.yml ├── tests_checker.yml └── workflows │ ├── backport.yml │ └── ci.yml ├── .gitignore ├── .npmrc ├── CNAME ├── LICENSE ├── README.md ├── SECURITY.md ├── _config.yml ├── bench.sh ├── bench ├── gateway-bench.js ├── gateway-service-1.js ├── gateway-service-2.js ├── gateway.js ├── standalone-bench.js ├── standalone-setup.js └── standalone.js ├── docs ├── api │ └── options.md ├── batched-queries.md ├── context.md ├── contribute.md ├── custom-directive.md ├── development.md ├── faq.md ├── federation.md ├── graphql-over-websocket.md ├── hooks.md ├── http.md ├── integrations │ ├── README.md │ ├── mercurius-integration-testing.md │ ├── nestjs.md │ ├── nexus.md │ ├── open-telemetry.md │ ├── prisma.md │ └── type-graphql.md ├── lifecycle.md ├── loaders.md ├── persisted-queries.md ├── plugins.md ├── subscriptions.md └── typescript.md ├── docsify └── sidebar.md ├── eslint.config.js ├── examples ├── basic.js ├── custom-directive.js ├── custom-http-behaviour.js ├── disable-introspection.js ├── executable-schema.js ├── full-ws-transport.js ├── gateway-subscription.js ├── gateway.js ├── graphiql-plugin │ ├── README.md │ ├── index.js │ ├── plugin-sources │ │ ├── .gitignore │ │ ├── package.json │ │ ├── rollup.config.js │ │ └── src │ │ │ ├── index.js │ │ │ ├── plugin.jsx │ │ │ ├── sampleDataManager.js │ │ │ ├── useSampleData.js │ │ │ └── utils.js │ └── plugin │ │ └── samplePlugin.js ├── hooks-gateway.js ├── hooks-subscription.js ├── hooks.js ├── loaders.js ├── persisted-queries │ ├── index.js │ └── queries.json ├── playground.js ├── schema-by-http-header.js └── subscription │ ├── memory.js │ └── mqemitter-mongodb-subscription.js ├── index.d.ts ├── index.html ├── index.js ├── lib ├── errors.js ├── handlers.js ├── hooks.js ├── persistedQueryDefaults.js ├── queryDepth.js ├── routes.js ├── subscriber.js ├── subscription-connection.js ├── subscription-protocol.js ├── subscription.js └── symbols.js ├── package.json ├── static ├── graphiql.html ├── img │ └── favicon.ico ├── main.js └── sw.js ├── tap-snapshots └── test │ ├── errors.js.test.cjs │ └── routes.js.test.cjs ├── test ├── aliases.js ├── app-decorator.js ├── batched.js ├── cache.js ├── custom-root-types.js ├── directives.js ├── disable-instrospection.js ├── errors.js ├── fix-790.js ├── graphql-option-override.js ├── hooks-with-batching.js ├── hooks.js ├── internals │ └── hooksRunner.js ├── loaders.js ├── options.js ├── persisted.js ├── plugin-definition.js ├── query-depth.js ├── reply-decorator.js ├── routes.js ├── subscriber.js ├── subscription-connection.js ├── subscription-hooks.js ├── subscription-protocol.js ├── subscription.js ├── types │ └── index.ts └── validation-rules.js └── tsconfig.json /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | open-pull-requests-limit: 10 8 | 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "weekly" 13 | open-pull-requests-limit: 10 14 | ignore: 15 | - dependency-name: "undici" 16 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | template: | 2 | ## What’s Changed 3 | 4 | $CHANGES 5 | -------------------------------------------------------------------------------- /.github/tests_checker.yml: -------------------------------------------------------------------------------- 1 | comment: 'Could you please add tests to make sure this change works as expected?', 2 | fileExtensions: ['.php', '.ts', '.js', '.c', '.cs', '.cpp', '.rb', '.java'] 3 | testDir: 'test' 4 | -------------------------------------------------------------------------------- /.github/workflows/backport.yml: -------------------------------------------------------------------------------- 1 | name: Backport 2 | on: 3 | pull_request_target: 4 | types: 5 | - closed 6 | - labeled 7 | 8 | jobs: 9 | backport: 10 | runs-on: ubuntu-latest 11 | name: Backport 12 | if: github.event.pull_request.merged 13 | steps: 14 | - name: Backport 15 | uses: tibdex/backport@v2 16 | with: 17 | github_token: ${{ secrets.GITHUB_TOKEN }} 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI workflow 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | runs-on: ${{ matrix.os }} 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | node-version: [20.x, 22.x, 24.x] 10 | os: [ubuntu-latest, windows-latest, macOS-latest] 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Use Node.js ${{ matrix.node-version }} 14 | uses: actions/setup-node@v4.4.0 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - name: Install Dependencies 18 | run: npm install --ignore-scripts 19 | - name: Test 20 | run: npm test 21 | automerge: 22 | needs: test 23 | runs-on: ubuntu-latest 24 | permissions: 25 | pull-requests: write 26 | contents: write 27 | steps: 28 | - uses: fastify/github-action-merge-dependabot@v3 29 | with: 30 | github-token: ${{secrets.GITHUB_TOKEN}} 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # clinic 64 | *clinic* 65 | 66 | # vscode 67 | .vscode/ 68 | 69 | # intellij idea, webstorm 70 | .idea/ 71 | 72 | # lockfiles 73 | package-lock.json 74 | yarn.lock 75 | pnpm-lock.yaml 76 | 77 | # tap 78 | .tap 79 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | mercurius.dev -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2025 Matteo Collina and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Mercurius Logo](https://raw.githubusercontent.com/mercurius-js/graphics/main/mercurius-horizontal.svg) 2 | 3 | # mercurius 4 | 5 | [![CI workflow](https://github.com/mercurius-js/mercurius/actions/workflows/ci.yml/badge.svg)](https://github.com/mercurius-js/mercurius/actions/workflows/ci.yml) 6 | [![NPM version](https://img.shields.io/npm/v/mercurius.svg?style=flat)](https://www.npmjs.com/package/mercurius) 7 | [![NPM downloads](https://img.shields.io/npm/dm/mercurius.svg?style=flat)](https://www.npmjs.com/package/mercurius) 8 | [![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard) 9 | 10 | Mercurius is a [**GraphQL**](https://graphql.org/) adapter for [**Fastify**](https://www.fastify.io) 11 | 12 | Features: 13 | 14 | - Caching of query parsing and validation. 15 | - Automatic loader integration to avoid 1 + N queries. 16 | - Just-In-Time compiler via [graphql-jit](http://npm.im/graphql-jit). 17 | - Subscriptions. 18 | - Federation support via [@mercuriusjs/federation](https://github.com/mercurius-js/mercurius-federation), including Subscriptions. 19 | - Gateway implementation via [@mercuriusjs/gateway](https://github.com/mercurius-js/mercurius-gateway), including Subscriptions. 20 | - Batched query support. 21 | - Customisable persisted queries. 22 | 23 | ## Docs 24 | 25 | - [Install](#install) 26 | - [Quick Start](#quick-start) 27 | - [Examples](#examples) 28 | - [API](docs/api/options.md) 29 | - [Context](docs/context.md) 30 | - [Loaders](docs/loaders.md) 31 | - [Hooks](docs/hooks.md) 32 | - [Lifecycle](docs/lifecycle.md) 33 | - [Federation](docs/federation.md) 34 | - [Subscriptions](docs/subscriptions.md) 35 | - [Batched Queries](docs/batched-queries.md) 36 | - [Persisted Queries](docs/persisted-queries.md) 37 | - [TypeScript Usage](/docs/typescript.md) 38 | - [HTTP](/docs/http.md) 39 | - [GraphQL over WebSocket](/docs/graphql-over-websocket.md) 40 | - [Integrations](docs/integrations/) 41 | - [Related Plugins](docs/plugins.md) 42 | - [Faq](/docs/faq.md) 43 | - [Acknowledgements](#acknowledgements) 44 | - [License](#license) 45 | 46 | ## Install 47 | 48 | ```bash 49 | npm i fastify mercurius graphql 50 | # or 51 | yarn add fastify mercurius graphql 52 | ``` 53 | 54 | The previous name of this module was [fastify-gql](http://npm.im/fastify-gql) (< 6.0.0). 55 | 56 | ## Quick Start 57 | 58 | ```js 59 | 'use strict' 60 | 61 | const Fastify = require('fastify') 62 | const mercurius = require('mercurius') 63 | 64 | const app = Fastify() 65 | 66 | const schema = ` 67 | type Query { 68 | add(x: Int, y: Int): Int 69 | } 70 | ` 71 | 72 | const resolvers = { 73 | Query: { 74 | add: async (_, { x, y }) => x + y 75 | } 76 | } 77 | 78 | app.register(mercurius, { 79 | schema, 80 | resolvers 81 | }) 82 | 83 | app.get('/', async function (req, reply) { 84 | const query = '{ add(x: 2, y: 2) }' 85 | return reply.graphql(query) 86 | }) 87 | 88 | app.listen({ port: 3000 }) 89 | ``` 90 | 91 | ## Examples 92 | 93 | Check [GitHub repo](https://github.com/mercurius-js/mercurius/tree/master/examples) for more examples. 94 | 95 | ## Acknowledgements 96 | 97 | The project is kindly sponsored by: 98 | 99 | - [NearForm](https://www.nearform.com) 100 | - [Platformatic](https://platformatic.dev) 101 | 102 | The Mercurius name was gracefully donated by [Marco Castelluccio](https://github.com/marco-c). 103 | The usage of that library was described in https://hacks.mozilla.org/2015/12/web-push-notifications-from-irssi/, and 104 | you can find that codebase in https://github.com/marco-c/mercurius. 105 | 106 | ## License 107 | 108 | MIT 109 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | This document describes the management of vulnerabilities for the 4 | Mercurius project and all modules within the Mercurius organization. 5 | 6 | ## Reporting vulnerabilities 7 | 8 | Individuals who find potential vulnerabilities in Mercurius are invited 9 | to report them via GitHub at https://github.com/mercurius-js/mercurius/security/advisories 10 | or via email at matteo.collina@gmail.com. 11 | 12 | ### Strict measures when reporting vulnerabilities 13 | 14 | Avoid creating new "informative" reports. Only create new 15 | report a potential vulnerability if you are absolutely sure this 16 | should be tagged as an actual vulnerability. Be careful on the maintainers time. 17 | 18 | ## Handling vulnerability reports 19 | 20 | When a potential vulnerability is reported, the following actions are taken: 21 | 22 | ### Triage 23 | 24 | **Delay:** 5 business days 25 | 26 | Within 5 business days, a member of the security team provides a first answer to the 27 | individual who submitted the potential vulnerability. The possible responses 28 | can be: 29 | 30 | * Acceptance: what was reported is considered as a new vulnerability 31 | * Rejection: what was reported is not considered as a new vulnerability 32 | * Need more information: the security team needs more information in order to evaluate what was reported. 33 | 34 | Triaging should include updating issue fields: 35 | * Asset - set/create the module affected by the report 36 | * Severity - TBD, currently left empty 37 | 38 | ### Correction follow-up 39 | 40 | **Delay:** 90 days 41 | 42 | When a vulnerability is confirmed, a member of the security team volunteers to follow 43 | up on this report. 44 | 45 | With the help of the individual who reported the vulnerability, they contact 46 | the maintainers of the vulnerable package to make them aware of the 47 | vulnerability. The maintainers can be invited as participants to the reported issue. 48 | 49 | With the package maintainer, they define a release date for the publication 50 | of the vulnerability. Ideally, this release date should not happen before 51 | the package has been patched. 52 | 53 | The report's vulnerable versions upper limit should be set to: 54 | * `*` if there is no fixed version available by the time of publishing the report. 55 | * the last vulnerable version. For example: `<=1.2.3` if a fix exists in `1.2.4` 56 | 57 | ### Publication 58 | 59 | **Delay:** 90 days 60 | 61 | Within 90 days after the triage date, the vulnerability must be made public. 62 | 63 | **Severity**: Vulnerability severity is assessed using [CVSS v.3](https://www.first.org/cvss/user-guide). 64 | 65 | If the package maintainer is actively developing a patch, an additional delay 66 | can be added with the approval of the security team and the individual who 67 | reported the vulnerability. 68 | 69 | At this point, a CVE will be requested by the team. 70 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-minimal -------------------------------------------------------------------------------- /bench.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | # from https://github.com/mercurius-js/auth/tree/main/bench 4 | 5 | echo '==============================' 6 | echo '= Normal Mode =' 7 | echo '==============================' 8 | npx concurrently --raw -k \ 9 | "node ./bench/standalone.js" \ 10 | "npx wait-on tcp:3000 && node ./bench/standalone-bench.js" 11 | 12 | echo '===============================' 13 | echo '= Gateway Mode =' 14 | echo '===============================' 15 | npx concurrently --raw -k \ 16 | "node ./bench/gateway-service-1.js" \ 17 | "node ./bench/gateway-service-2.js" \ 18 | "npx wait-on tcp:3001 tcp:3002 && node ./bench/gateway.js" \ 19 | "npx wait-on tcp:3000 && node ./bench/gateway-bench.js" 20 | -------------------------------------------------------------------------------- /bench/gateway-bench.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const autocannon = require('autocannon') 4 | 5 | const query = `query { 6 | me { 7 | id 8 | name 9 | nickname: name 10 | topPosts(count: 2) { 11 | pid 12 | author { 13 | id 14 | } 15 | } 16 | } 17 | topPosts(count: 2) { 18 | pid 19 | } 20 | }` 21 | 22 | const instance = autocannon( 23 | { 24 | url: 'http://localhost:3000/graphql', 25 | connections: 100, 26 | title: '', 27 | method: 'POST', 28 | headers: { 29 | 'content-type': 'application/json', 'x-user': 'admin' 30 | }, 31 | body: JSON.stringify({ query }) 32 | }, 33 | (err) => { 34 | if (err) { 35 | console.error(err) 36 | } 37 | } 38 | ) 39 | 40 | process.once('SIGINT', () => { 41 | instance.stop() 42 | }) 43 | 44 | autocannon.track(instance, { renderProgressBar: true }) 45 | -------------------------------------------------------------------------------- /bench/gateway-service-1.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | const mercurius = require('..') 5 | 6 | const app = Fastify() 7 | 8 | const users = { 9 | u1: { 10 | id: 'u1', 11 | name: 'John' 12 | }, 13 | u2: { 14 | id: 'u2', 15 | name: 'Jane' 16 | } 17 | } 18 | 19 | const schema = ` 20 | directive @auth( 21 | requires: Role = ADMIN, 22 | ) on OBJECT | FIELD_DEFINITION 23 | 24 | enum Role { 25 | ADMIN 26 | REVIEWER 27 | USER 28 | UNKNOWN 29 | } 30 | 31 | type Query @extends { 32 | me: User 33 | } 34 | 35 | type User @key(fields: "id") { 36 | id: ID! 37 | name: String! @auth(requires: ADMIN) 38 | }` 39 | 40 | const resolvers = { 41 | Query: { 42 | me: (root, args, context, info) => { 43 | return users.u1 44 | } 45 | }, 46 | User: { 47 | __resolveReference: (user, args, context, info) => { 48 | return users[user.id] 49 | } 50 | } 51 | } 52 | 53 | app.register(mercurius, { 54 | schema, 55 | resolvers, 56 | federationMetadata: true, 57 | graphiql: false, 58 | jit: 1 59 | }) 60 | 61 | app.listen({ port: 3001 }) 62 | -------------------------------------------------------------------------------- /bench/gateway-service-2.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | const mercurius = require('..') 5 | 6 | const app = Fastify() 7 | 8 | const posts = { 9 | p1: { 10 | pid: 'p1', 11 | title: 'Post 1', 12 | content: 'Content 1', 13 | authorId: 'u1' 14 | }, 15 | p2: { 16 | pid: 'p2', 17 | title: 'Post 2', 18 | content: 'Content 2', 19 | authorId: 'u2' 20 | }, 21 | p3: { 22 | pid: 'p3', 23 | title: 'Post 3', 24 | content: 'Content 3', 25 | authorId: 'u1' 26 | }, 27 | p4: { 28 | pid: 'p4', 29 | title: 'Post 4', 30 | content: 'Content 4', 31 | authorId: 'u1' 32 | } 33 | } 34 | 35 | const schema = ` 36 | directive @auth( 37 | requires: Role = ADMIN, 38 | ) on OBJECT | FIELD_DEFINITION 39 | 40 | enum Role { 41 | ADMIN 42 | REVIEWER 43 | USER 44 | UNKNOWN 45 | } 46 | 47 | type Post @key(fields: "pid") { 48 | pid: ID! 49 | author: User @auth(requires: ADMIN) 50 | } 51 | 52 | extend type Query { 53 | topPosts(count: Int): [Post] @auth(requires: ADMIN) 54 | } 55 | 56 | type User @key(fields: "id") @extends { 57 | id: ID! @external 58 | topPosts(count: Int!): [Post] 59 | }` 60 | 61 | const resolvers = { 62 | Post: { 63 | __resolveReference: (post, args, context, info) => { 64 | return posts[post.pid] 65 | }, 66 | author: (post, args, context, info) => { 67 | return { 68 | __typename: 'User', 69 | id: post.authorId 70 | } 71 | } 72 | }, 73 | User: { 74 | topPosts: (user, { count }, context, info) => { 75 | return Object.values(posts).filter(p => p.authorId === user.id).slice(0, count) 76 | } 77 | }, 78 | Query: { 79 | topPosts: (root, { count = 2 }) => Object.values(posts).slice(0, count) 80 | } 81 | } 82 | 83 | app.register(mercurius, { 84 | schema, 85 | resolvers, 86 | federationMetadata: true, 87 | graphiql: false, 88 | jit: 1 89 | }) 90 | 91 | app.listen({ port: 3002 }) 92 | -------------------------------------------------------------------------------- /bench/gateway.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | const mercurius = require('..') 5 | 6 | const app = Fastify() 7 | 8 | app.register(mercurius, { 9 | gateway: { 10 | services: [{ 11 | name: 'user', 12 | url: 'http://localhost:3001/graphql' 13 | }, { 14 | name: 'post', 15 | url: 'http://localhost:3002/graphql' 16 | }] 17 | }, 18 | graphiql: false, 19 | jit: 1 20 | }) 21 | 22 | app.listen({ port: 3000 }) 23 | -------------------------------------------------------------------------------- /bench/standalone-bench.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const autocannon = require('autocannon') 4 | 5 | const query = `query { 6 | four: add(x: 2, y: 2) 7 | six: add(x: 3, y: 3) 8 | subtract(x: 3, y: 3) 9 | messages { 10 | title 11 | public 12 | private 13 | } 14 | adminMessages { 15 | title 16 | public 17 | private 18 | } 19 | }` 20 | 21 | const instance = autocannon( 22 | { 23 | url: 'http://localhost:3000/graphql', 24 | connections: 100, 25 | title: '', 26 | method: 'POST', 27 | headers: { 28 | 'content-type': 'application/json', 'x-user': 'admin' 29 | }, 30 | body: JSON.stringify({ query }) 31 | }, 32 | (err) => { 33 | if (err) { 34 | console.error(err) 35 | } 36 | } 37 | ) 38 | 39 | process.once('SIGINT', () => { 40 | instance.stop() 41 | }) 42 | 43 | autocannon.track(instance, { renderProgressBar: true }) 44 | -------------------------------------------------------------------------------- /bench/standalone-setup.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const schema = ` 4 | directive @auth( 5 | requires: Role = ADMIN, 6 | ) on OBJECT | FIELD_DEFINITION 7 | 8 | enum Role { 9 | ADMIN 10 | REVIEWER 11 | USER 12 | UNKNOWN 13 | } 14 | 15 | type Message { 16 | title: String! 17 | public: String! 18 | private: String! @auth(requires: ADMIN) 19 | } 20 | 21 | type Query { 22 | add(x: Int, y: Int): Int @auth(requires: ADMIN) 23 | subtract(x: Int, y: Int): Int 24 | messages: [Message!]! 25 | adminMessages: [Message!]! @auth(requires: ADMIN) 26 | } 27 | ` 28 | 29 | const resolvers = { 30 | Query: { 31 | add: async (_, obj) => { 32 | const { x, y } = obj 33 | return x + y 34 | }, 35 | subtract: async (_, obj) => { 36 | const { x, y } = obj 37 | return x - y 38 | }, 39 | messages: async () => { 40 | return [ 41 | { 42 | title: 'one', 43 | public: 'public one', 44 | private: 'private one' 45 | }, 46 | { 47 | title: 'two', 48 | public: 'public two', 49 | private: 'private two' 50 | } 51 | ] 52 | }, 53 | adminMessages: async () => { 54 | return [ 55 | { 56 | title: 'admin one', 57 | public: 'admin public one', 58 | private: 'admin private one' 59 | }, 60 | { 61 | title: 'admin two', 62 | public: 'admin public two', 63 | private: 'admin private two' 64 | } 65 | ] 66 | } 67 | } 68 | } 69 | 70 | module.exports = { 71 | schema, 72 | resolvers 73 | } 74 | -------------------------------------------------------------------------------- /bench/standalone.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | const mercurius = require('..') 5 | const { schema, resolvers } = require('./standalone-setup') 6 | 7 | const app = Fastify() 8 | 9 | app.register(mercurius, { 10 | schema, 11 | resolvers, 12 | graphiql: false, 13 | jit: 1 14 | }) 15 | 16 | app.listen({ port: 3000 }) 17 | -------------------------------------------------------------------------------- /docs/batched-queries.md: -------------------------------------------------------------------------------- 1 | # mercurius 2 | 3 | ## Batched Queries 4 | 5 | Batched queries, like those sent by `apollo-link-batch-http` are supported by enabling the `allowBatchedQueries` option. 6 | 7 | Instead a single query object, an array of queries is accepted, and the response is returned as an array of results. Errors are returned on a per query basis. Note that the response will not be returned until the slowest query has been executed. 8 | 9 | Request: 10 | 11 | ```js 12 | [ 13 | { 14 | operationName: "AddQuery", 15 | variables: { x: 1, y: 2 }, 16 | query: "query AddQuery ($x: Int!, $y: Int!) { add(x: $x, y: $y) }", 17 | }, 18 | { 19 | operationName: "DoubleQuery", 20 | variables: { x: 1 }, 21 | query: "query DoubleQuery ($x: Int!) { add(x: $x, y: $x) }", 22 | }, 23 | { 24 | operationName: "BadQuery", 25 | query: "query DoubleQuery ($x: Int!) {---", // Malformed Query 26 | }, 27 | ]; 28 | ``` 29 | 30 | Response: 31 | 32 | ```js 33 | [ 34 | { 35 | data: { add: 3 }, 36 | }, 37 | { 38 | data: { add: 2 }, 39 | }, 40 | { 41 | errors: [{ message: "Bad Request" }], 42 | }, 43 | ]; 44 | ``` 45 | -------------------------------------------------------------------------------- /docs/context.md: -------------------------------------------------------------------------------- 1 | # mercurius 2 | 3 | ## Context 4 | 5 | ### Access app context in resolver 6 | 7 | ```js 8 | ... 9 | 10 | const resolvers = { 11 | Query: { 12 | add: async (_, { x, y }, context) => { 13 | // do you need the request object? 14 | console.log(context.reply.request) 15 | return x + y 16 | } 17 | } 18 | } 19 | 20 | ... 21 | ``` 22 | 23 | ### Build a custom GraphQL context object 24 | 25 | ```js 26 | ... 27 | const resolvers = { 28 | Query: { 29 | me: async (obj, args, ctx) => { 30 | // access user_id in ctx 31 | console.log(ctx.user_id) 32 | } 33 | } 34 | } 35 | app.register(mercurius, { 36 | schema: makeExecutableSchema({ typeDefs, resolvers }), 37 | context: (request, reply) => { 38 | // Return an object that will be available in your GraphQL resolvers 39 | return { 40 | user_id: 1234 41 | } 42 | } 43 | }) 44 | ... 45 | ``` 46 | -------------------------------------------------------------------------------- /docs/contribute.md: -------------------------------------------------------------------------------- 1 | # Contribute to Mercurius 2 | 3 | Mercurius is a growing and friendly community, and would be lucky to have your contributions! 🙇‍♂️ 4 | 5 | Contributions are always welcome, we only ask you follow the Contribution Guidelines and the Code Of Conduct. 6 | 7 | If you don't know where to start you can have a look at the list of good first issues below. 8 | 9 | ## Good First Issues 10 | 11 |
12 |
13 |
14 |
15 |

Error

16 |
17 |
{{ error }}
18 |
19 |
20 |
21 |
22 |
23 | 26 |
27 |
28 |

29 | {{ issue.title }} 30 |

31 |
32 |
33 | {{ label }} 34 |
35 |
36 |
37 | {{ issue.comments }} 38 | Comments 39 |
40 |
41 |
42 |
43 |
44 |
45 | -------------------------------------------------------------------------------- /docs/development.md: -------------------------------------------------------------------------------- 1 | # Development 2 | 3 | ## Backporting 4 | 5 | The Mercurius repository supports backporting PRs that also need to be applied to older versions. 6 | 7 | ### How do we do this? 8 | 9 | As soon as one opens a PR against the default branch, and the change should be backported to `v8.x`, you should add the corresponding backport label. For example, if we need to backport something to `v8.x`, we add the following label: 10 | 11 | - `backport v8.x` 12 | 13 | And you are done! If there are no conflicts, the action will open a separate PR with the backport for review. 14 | 15 | If the PR can't be automatically backported, the GitHub bot will comment the failure on the PR. 16 | -------------------------------------------------------------------------------- /docs/faq.md: -------------------------------------------------------------------------------- 1 | # FAQ 2 | 3 | This page answers commonly asked questions about Mercurius. 4 | 5 | ## Disable Graphql introspection 6 | To disable Graphql introspection you can use `NoSchemaIntrospectionCustomRule` from graphql. We have an example on "example/disable-instrospection.js", using this approach: 7 | 8 | ```js 9 | import { NoSchemaIntrospectionCustomRule } from 'graphql'; 10 | 11 | const schema = ` 12 | type Query { 13 | add(x: Int, y: Int): Int 14 | } 15 | ` 16 | const resolvers = { 17 | Query: { 18 | add: async (_, obj) => { 19 | const { x, y } = obj 20 | return x + y 21 | } 22 | } 23 | } 24 | 25 | app.register(mercurius, { 26 | context: buildContext, 27 | schema, 28 | resolvers, 29 | validationRules: process.env.NODE_ENV === 'production' && [NoSchemaIntrospectionCustomRule], 30 | }); 31 | ``` 32 | 33 | ## Execute against different schemas based on request headers 34 | 35 | Sometimes we may face the need to present a scheme that varies depending on specific situations. 36 | To accomplish this need we can use one powerful fastify/find-my-way feature called **Custom Constraints**. 37 | 38 | https://www.fastify.io/docs/latest/Reference/Routes/#asynchronous-custom-constraints 39 | 40 | > Fastify supports constraining routes to match only certain requests based on some property of the request, like the Host header, or any other value via find-my-way constraints. 41 | 42 | We can then create two mercurius instances that expose the two different schemas and use the constraint on the header to drive the request to one or other mercurius instance. 43 | 44 | ### 1. Create the constraint and initialize the fastify instance 45 | ```js 46 | const Fastify = require('fastify') 47 | const mercurius = require('..') 48 | 49 | // Define the constraint custom strategy 50 | const schemaStrategy = { 51 | name: 'schema', 52 | storage: function () { 53 | const handlers = {} 54 | return { 55 | get: (type) => { return handlers[type] || null }, 56 | set: (type, store) => { handlers[type] = store } 57 | } 58 | }, 59 | deriveConstraint: (req, ctx) => { 60 | return req.headers.schema 61 | }, 62 | validate: () => true, 63 | mustMatchWhenDerived: true 64 | } 65 | 66 | // Initialize fastify 67 | const app = Fastify({ constraints: { schema: schemaStrategy } }) 68 | ``` 69 | ### 2. Initialize the first mercurius instance and bind it to the `/` route only if the `schema` header value is equal to `A` 70 | 71 | ```js 72 | const schema = ` 73 | type Query { 74 | add(x: Int, y: Int): Int 75 | } 76 | ` 77 | 78 | const resolvers = { 79 | Query: { 80 | add: async (_, obj) => { 81 | const { x, y } = obj 82 | return x + y 83 | } 84 | } 85 | } 86 | 87 | // Schema A registration with A constraint 88 | app.register(async childServer => { 89 | childServer.register(mercurius, { 90 | schema, 91 | resolvers, 92 | graphiql: false, 93 | routes: false 94 | }) 95 | 96 | childServer.route({ 97 | path: '/', 98 | method: 'POST', 99 | constraints: { schema: 'A' }, 100 | handler: (req, reply) => { 101 | const query = req.body 102 | return reply.graphql(query) 103 | } 104 | }) 105 | }) 106 | ``` 107 | ### 3. Initialize the second mercurius instance and bind it to the `/` route only if the `schema` header value is equal to `B` 108 | 109 | ```js 110 | const schema2 = ` 111 | type Query { 112 | subtract(x: Int, y: Int): Int 113 | } 114 | ` 115 | 116 | const resolvers2 = { 117 | Query: { 118 | subtract: async (_, obj) => { 119 | const { x, y } = obj 120 | return x - y 121 | } 122 | } 123 | } 124 | 125 | app.register(async childServer => { 126 | childServer.register(mercurius, { 127 | schema: schema2, 128 | resolvers: resolvers2, 129 | graphiql: false, 130 | routes: false 131 | }) 132 | 133 | childServer.route({ 134 | path: '/', 135 | method: 'POST', 136 | constraints: { schema: 'B' }, 137 | handler: (req, reply) => { 138 | const query = req.body 139 | return reply.graphql(query) 140 | } 141 | }) 142 | }) 143 | ``` 144 | 145 | 4. Start the fastify server 146 | 147 | ```js 148 | app.listen({ port: 3000 }) 149 | ``` 150 | 151 | ### Important notes: 152 | 153 | In order to use graphql in constrained routes we need to set mercurius `routes` parameter to `false` in order to avoid that both the mercurius instances try to expose themself at `/graphql`. 154 | -------------------------------------------------------------------------------- /docs/graphql-over-websocket.md: -------------------------------------------------------------------------------- 1 | # GraphQL over WebSocket 2 | 3 | The GraphQL specification doesn't dictates which transport must be used in order to execute operations. In fact, GraphQL is _transport agnostic_, so implementors can choose which protocol makes more sense for each use case. 4 | 5 | Generally, `query` and `mutation` are carried via HTTP, while `subscription` via WebSocket: this is the default behavior in `mercurius` and many other server implementations. However, `query` and `mutation` can also be sent through WebSocket. 6 | 7 | ## WebSocket subprotocol 8 | 9 | As WebSocket is a generic and bidirectional way to send messages, **we need to agree about what each message _means_**: this is defined by the _subprotocol_. 10 | 11 | ### Supported subprotocols 12 | 13 | The GraphQL over WebSocket Protocol (i.e. the WebSocket sub-protocol) used by default is called `graphql-transport-ws` and it's defined here: 14 | 15 | - [`graphql-transport-ws` Protocol SPEC](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) 16 | 17 | > ⚠️ The subprotocol originally defined by Apollo's `subscriptions-transport-ws` library is also supported. However, that library is **UNMAINTAINED** so it's not recommended to be used: basically **deprecated**. More info [here](https://github.com/apollographql/subscriptions-transport-ws/). 18 | 19 | ### Supported clients 20 | 21 | You should be able to use any major GraphQL client library in order to send operations via WebSocket (e.g. `graphql-ws`, `graphql-hooks`, `apollo`, `urql`…). Depending on which client you use, you have built in support or you may need to use some plugins or middleware. 22 | 23 | ## Extensions 24 | 25 | The `extensions` field is reserved for things that implementors want to add on top of the spec. 26 | 27 | ### Message structure 28 | 29 | This is the structure allowed on each WS message: 30 | 31 | ```ts 32 | export interface OperationMessage { 33 | payload?: any; 34 | id?: string; 35 | type: string; 36 | 37 | extensions?: Array; 38 | } 39 | 40 | export interface OperationExtension { 41 | type: string; 42 | payload?: any; 43 | } 44 | ``` 45 | 46 | ### Server -> Server 47 | 48 | In order to achieve _gateway-to-service_ communication and handle `connection_init` per client, an extension is used on the protocol for _server-to-server_ communication. See https://github.com/mercurius-js/mercurius/issues/268 for more details about the original issue and the actual implemented solution. 49 | 50 | #### `connectionInit` extension 51 | 52 | Gateway uses this extension to share the `connection_init` payload with a service when the connection is already established between gateway and services. 53 | 54 | ```ts 55 | export interface ConnectionInitExtension extends OperationExtension { 56 | type: string; 57 | payload?: Object; 58 | } 59 | ``` 60 | 61 | - `type: String` : 'connectionInit' 62 | - `payload: Object` : optional parameters that the client specifies in connectionParams 63 | -------------------------------------------------------------------------------- /docs/hooks.md: -------------------------------------------------------------------------------- 1 | # Hooks 2 | 3 | Hooks are registered with the `fastify.graphql.addHook` method and allow you to listen to specific events in the GraphQL request/response lifecycle. You have to register a hook before the event is triggered, otherwise the event is lost. 4 | 5 | By using hooks you can interact directly with the GraphQL lifecycle of Mercurius. There are GraphQL Request and Subscription hooks: 6 | 7 | - [GraphQL Request Hooks](#graphql-request-hooks) 8 | - [preParsing](#preparsing) 9 | - [preValidation](#prevalidation) 10 | - [preExecution](#preexecution) 11 | - [onResolution](#onresolution) 12 | - [Manage Errors from a request hook](#manage-errors-from-a-request-hook) 13 | - [Add errors to the GraphQL response from a hook](#add-errors-to-the-graphql-response-from-a-hook) 14 | - [GraphQL Subscription Hooks](#graphql-subscription-hooks) 15 | - [preSubscriptionParsing](#presubscriptionparsing) 16 | - [preSubscriptionExecution](#presubscriptionexecution) 17 | - [onSubscriptionResolution](#onsubscriptionresolution) 18 | - [onSubscriptionEnd](#onsubscriptionend) 19 | - [Manage Errors from a subscription hook](#manage-errors-from-a-subscription-hook) 20 | 21 | 22 | **Notice:** these hooks are only supported with `async`/`await` or returning a `Promise`. 23 | 24 | ## GraphQL Request Hooks 25 | 26 | It is pretty easy to understand where each hook is executed by looking at the [lifecycle page](/docs/lifecycle.md).
27 | 28 | There are five different hooks that you can use in a GraphQL Request *(in order of execution)*: 29 | 30 | When registering hooks, you must wait for Mercurius to be registered in Fastify. 31 | 32 | ```js 33 | await fastify.ready() 34 | ``` 35 | 36 | ### preParsing 37 | 38 | If you are using the `preParsing` hook, you can access the GraphQL query string before it is parsed. It receives the schema and context objects as other hooks. 39 | 40 | For instance, you can register some tracing events: 41 | 42 | ```js 43 | fastify.graphql.addHook('preParsing', async (schema, source, context) => { 44 | await registerTraceEvent() 45 | }) 46 | ``` 47 | 48 | ### preValidation 49 | 50 | 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. 51 | 52 | ```js 53 | fastify.graphql.addHook('preValidation', async (schema, document, context) => { 54 | await asyncMethod() 55 | }) 56 | ``` 57 | 58 | ### preExecution 59 | 60 | In the `preExecution` hook, you can modify the following items by returning them in the hook definition: 61 | - `document` 62 | - `schema` 63 | - `variables` 64 | - `errors` 65 | 66 | 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. 67 | 68 | ```js 69 | fastify.graphql.addHook('preExecution', async (schema, document, context, variables) => { 70 | const { 71 | modifiedSchema, 72 | modifiedDocument, 73 | modifiedVariables, 74 | errors 75 | } = await asyncMethod(document) 76 | 77 | return { 78 | schema: modifiedSchema, // ⚠️ changing the schema may break the query execution. Use it carefully. 79 | document: modifiedDocument, 80 | variables: modifiedVariables, 81 | errors 82 | } 83 | }) 84 | ``` 85 | 86 | ### onResolution 87 | 88 | The `onResolution` hooks run after the GraphQL query execution and you can access the result via the `execution` argument. 89 | 90 | ```js 91 | fastify.graphql.addHook('onResolution', async (execution, context) => { 92 | await asyncMethod() 93 | }) 94 | ``` 95 | 96 | ### Manage Errors from a request hook 97 | If you get an error during the execution of your hook, you can just throw an error and Mercurius will automatically close the GraphQL request and send the appropriate errors to the user.` 98 | 99 | ```js 100 | fastify.graphql.addHook('preParsing', async (schema, source, context) => { 101 | throw new Error('Some error') 102 | }) 103 | ``` 104 | 105 | ### Add errors to the GraphQL response from a hook 106 | 107 | The following hooks support adding errors to the GraphQL response. These are: 108 | 109 | - `preExecution` 110 | 111 | ```js 112 | fastify.graphql.addHook('preExecution', async (schema, document, context) => { 113 | return { 114 | errors: [new Error('foo')] 115 | } 116 | }) 117 | ``` 118 | 119 | Note, the original query will still execute. Adding the above will result in the following response: 120 | 121 | ```json 122 | { 123 | "data": { 124 | "foo": "bar" 125 | }, 126 | "errors": [ 127 | { 128 | "message": "foo" 129 | }, 130 | { 131 | "message": "bar" 132 | } 133 | ] 134 | } 135 | ``` 136 | 137 | ## GraphQL Subscription Hooks 138 | 139 | It is pretty easy to understand where each hook is executed by looking at the [lifecycle page](/docs/lifecycle.md).
140 | 141 | There are five different hooks that you can use in GraphQL Subscriptions *(in order of execution)*: 142 | 143 | When registering hooks, you must make sure that subscriptions are enabled and you must wait for Mercurius to be registered in Fastify. 144 | 145 | ```js 146 | await fastify.ready() 147 | ``` 148 | 149 | ### preSubscriptionParsing 150 | 151 | If you are using the `preSubscriptionParsing` hook, you can access the GraphQL subscription query string before it is parsed. It receives the schema and context objects as other hooks. 152 | 153 | For instance, you can register some tracing events: 154 | 155 | ```js 156 | fastify.graphql.addHook('preSubscriptionParsing', async (schema, source, context) => { 157 | await registerTraceEvent() 158 | }) 159 | ``` 160 | 161 | ### preSubscriptionExecution 162 | 163 | By the time the `preSubscriptionExecution` hook triggers, the subscription query string has been parsed into a GraphQL Document AST. 164 | 165 | ```js 166 | fastify.graphql.addHook('preSubscriptionExecution', async (schema, document, context) => { 167 | await asyncMethod() 168 | }) 169 | ``` 170 | 171 | ### onSubscriptionResolution 172 | 173 | ```js 174 | fastify.graphql.addHook('onSubscriptionResolution', async (execution, context) => { 175 | await asyncMethod() 176 | }) 177 | ``` 178 | 179 | ### onSubscriptionEnd 180 | 181 | This hook will be triggered when a subscription ends. 182 | 183 | ```js 184 | fastify.graphql.addHook('onSubscriptionEnd', async (context, id) => { 185 | await asyncMethod() 186 | }) 187 | ``` 188 | 189 | ### Manage Errors from a subscription hook 190 | 191 | If you get an error during the execution of your subscription hook, you can just throw an error and Mercurius will send the appropriate errors to the user along the websocket.` 192 | 193 | **Notice:** there are exceptions to this with the `onSubscriptionResolution` and `onSubscriptionEnd` hooks, which will close the subscription connection if an error occurs. 194 | 195 | ```js 196 | fastify.graphql.addHook('preSubscriptionParsing', async (schema, source, context) => { 197 | throw new Error('Some error') 198 | }) 199 | ``` 200 | 201 | ## GraphQL Application lifecycle Hooks 202 | 203 | When registering hooks, you must wait for Mercurius to be registered in Fastify. 204 | 205 | ```js 206 | await fastify.ready() 207 | ``` 208 | 209 | ### onExtendSchema 210 | 211 | This hook will be triggered when `extendSchema` is called. It receives the new schema and context object. 212 | 213 | ```js 214 | app.graphql.addHook('onExtendSchema', async (schema, context) => { 215 | await asyncMethod() 216 | }) 217 | ``` 218 | -------------------------------------------------------------------------------- /docs/http.md: -------------------------------------------------------------------------------- 1 | # mercurius 2 | 3 | - [HTTP Status Codes](#http-status-codes) 4 | - [Default behaviour](#default-behaviour) 5 | - [Response with data](#response-with-data) 6 | - [Invalid input document](#invalid-input-document) 7 | - [Multiple errors](#multiple-errors) 8 | - [Single error with `statusCode` property](#single-error-with-statuscode-property) 9 | - [Single error with no `statusCode` property](#single-error-with-no-statuscode-property) 10 | - [Custom behaviour](#custom-behaviour) 11 | - [`200 OK` on all requests](#200-ok-on-all-requests) 12 | 13 | Mercurius exhibits the following behaviour when serving GraphQL over HTTP. 14 | 15 | ## HTTP Status Codes 16 | 17 | ### Default behaviour 18 | 19 | Mercurius has the following default behaviour for HTTP Status Codes. 20 | 21 | #### Response with data 22 | 23 | When a GraphQL response contains `data` that is defined, the HTTP Status Code is `200 OK`. 24 | 25 | - **HTTP Status Code**: `200 OK` 26 | - **Data**: `!== null` 27 | - **Errors**: `N/A` 28 | 29 | #### Invalid input document 30 | 31 | When a GraphQL input document is invalid and fails GraphQL validation, the HTTP Status Code is `400 Bad Request`. 32 | 33 | - **HTTP Status Code**: `400 Bad Request` 34 | - **Data**: `null` 35 | - **Errors**: `MER_ERR_GQL_VALIDATION` 36 | 37 | #### Response with errors 38 | 39 | When a GraphQL response contains errors, the HTTP Status Code is `200 OK` as defined in the [GraphQL Over HTTP 40 | Specification](https://github.com/graphql/graphql-over-http/blob/main/spec/GraphQLOverHTTP.md#applicationjson). 41 | 42 | - **HTTP Status Code**: `200 OK` 43 | - **Data**: `null` 44 | - **Errors**: `Array` (`.length >= 1`) 45 | 46 | #### Single error with `statusCode` property 47 | 48 | When a GraphQL response contains a single error with the `statusCode` property set and no data, the HTTP Status Code is set to this value. See [ErrorWithProps](/docs/api/options.md#errorwithprops) for more details. 49 | 50 | - **HTTP Status Code**: `Error statusCode` 51 | - **Data**: `null` 52 | - **Errors**: `Array` (`.length === 1`) 53 | 54 | ### Custom behaviour 55 | 56 | If you wish to customise the default HTTP Status Code behaviour, one can do this using the [`errorFormatter`](/docs/api/options.md#plugin-options) option. 57 | -------------------------------------------------------------------------------- /docs/integrations/README.md: -------------------------------------------------------------------------------- 1 | # mercurius 2 | 3 | ## Integrations 4 | 5 | - [nexus](/docs/integrations/nexus.md) - Declarative, code-first and strongly typed GraphQL schema construction for TypeScript & JavaScript 6 | - [TypeGraphQL](/docs/integrations/type-graphql.md) - Modern framework for creating GraphQL API in Node.js, using only classes and decorators 7 | - [Prisma](/docs/integrations/prisma.md) - Prisma is an open-source ORM for Node.js and TypeScript. 8 | - [mercurius-integration-testing](/docs/integrations/mercurius-integration-testing.md) - Utility library for writing mercurius integration tests. 9 | - [@opentelemetry](/docs/integrations/open-telemetry.md) - A framework for collecting traces and metrics from applications. 10 | - [NestJS](/docs/integrations/nestjs.md) - Use Typescript classes and decorators, along with a powerful modularization system and a lot of other great tools for a very good developer experience in terms of GraphQL API automation. 11 | -------------------------------------------------------------------------------- /docs/integrations/mercurius-integration-testing.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | You can easily test your GraphQL API using `mercurius-integration-testing`. 4 | 5 | [More info here.](https://github.com/mercurius-js/mercurius-integration-testing) 6 | 7 | ## Installation 8 | 9 | ```bash 10 | npm install mercurius-integration-testing 11 | ``` 12 | 13 | ## Usage 14 | 15 | > Example using [node-tap](https://node-tap.org/) 16 | 17 | ```js 18 | // server.js 19 | const Fastify = require('fastify') 20 | const mercurius = require('mercurius') 21 | 22 | const app = Fastify() 23 | 24 | const schema = ` 25 | type Query { 26 | hello: String! 27 | } 28 | ` 29 | 30 | const resolvers = { 31 | Query: { 32 | hello: () => { 33 | return 'world' 34 | } 35 | } 36 | } 37 | 38 | app.register(mercurius, { 39 | schema, 40 | resolvers, 41 | // Only required to use .batchQueries() 42 | allowBatchedQueries: true 43 | }) 44 | 45 | exports.app = app 46 | ``` 47 | 48 | Then in your tests 49 | 50 | ```js 51 | // example.test.js 52 | 53 | const tap = require('tap') 54 | const { createMercuriusTestClient } = require('mercurius-integration-testing') 55 | const { app } = require('./server.js') 56 | 57 | tap.test('works', (t) => { 58 | t.plan(1) 59 | 60 | const client = createMercuriusTestClient(app) 61 | 62 | client 63 | .query( 64 | `query { 65 | hello 66 | }` 67 | ) 68 | .then((response) => { 69 | t.equivalent(response, { 70 | data: { 71 | hello: 'world' 72 | } 73 | }) 74 | }) 75 | }) 76 | ``` 77 | 78 | --- 79 | 80 | 🎉 81 | 82 | ``` 83 | $ npx tap 84 | 85 | PASS example.test.js 1 OK 129.664ms 86 | 87 | 88 | 🌈 SUMMARY RESULTS 🌈 89 | 90 | 91 | Suites: 1 passed, 1 of 1 completed 92 | Asserts: 1 passed, of 1 93 | Time: 2s 94 | -----------|----------|----------|----------|----------|-------------------| 95 | File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s | 96 | -----------|----------|----------|----------|----------|-------------------| 97 | All files | 100 | 100 | 100 | 100 | | 98 | server.js | 100 | 100 | 100 | 100 | | 99 | -----------|----------|----------|----------|----------|-------------------| 100 | 101 | ``` 102 | 103 | ## Docs 104 | 105 | Please check [https://github.com/mercurius-js/mercurius-integration-testing#api](https://github.com/mercurius-js/mercurius-integration-testing#api) for more documentation 106 | -------------------------------------------------------------------------------- /docs/integrations/nexus.md: -------------------------------------------------------------------------------- 1 | # Integrating Nexus with Mercurius 2 | 3 | You can easily use [Nexus](https://github.com/graphql-nexus/nexus) in combination with Mercurius. 4 | This allows you to follow a code first approach instead of the SDL first. 5 | 6 | ## Installation 7 | 8 | ```bash 9 | npm install --save nexus 10 | ``` 11 | 12 | Now you can define a schema. 13 | 14 | ```js 15 | // schema.js 16 | const { objectType, intArg, nonNull } = require("nexus"); 17 | const args = { 18 | x: nonNull( 19 | intArg({ 20 | description: 'value of x', 21 | }) 22 | ), 23 | y: nonNull( 24 | intArg({ 25 | description: 'value of y', 26 | }) 27 | ), 28 | }; 29 | exports.Query = objectType({ 30 | name: "Query", 31 | definition(t) { 32 | t.int("add", { 33 | resolve(_, { x, y }) { 34 | return x + y; 35 | }, 36 | args, 37 | }); 38 | }, 39 | }); 40 | ``` 41 | 42 | This can be linked to the Mercurius plugin: 43 | 44 | ```js 45 | // index.js 46 | 47 | const Fastify = require("fastify"); 48 | const mercurius = require("mercurius"); 49 | const path = require("path"); 50 | const { makeSchema } = require("nexus"); 51 | const types = require("./schema"); 52 | 53 | const schema = makeSchema({ 54 | types, 55 | outputs: { 56 | schema: path.join(__dirname, "./my-schema.graphql"), 57 | typegen: path.join(__dirname, "./my-generated-types.d.ts"), 58 | }, 59 | }); 60 | 61 | const app = Fastify(); 62 | 63 | app.register(mercurius, { 64 | schema, 65 | graphiql: true, 66 | }); 67 | 68 | app.get("/", async function (req, reply) { 69 | const query = "{ add(x: 2, y: 2) }"; 70 | return reply.graphql(query); 71 | }); 72 | 73 | app.listen({ port: 3000 }); 74 | ``` 75 | 76 | If you run this, you will get type definitions and a generated GraphQL based on your code: 77 | 78 | ```bash 79 | node index.js 80 | ``` 81 | -------------------------------------------------------------------------------- /docs/integrations/open-telemetry.md: -------------------------------------------------------------------------------- 1 | # mercurius 2 | 3 | ## OpenTelemetry (Tracing) 4 | 5 | Mercurius is compatible with open-telemetry (Note that, for now, jitted requests are not able to trace the graphql execution). Also make sure that registration of opentelemetry instrumentation happens before requiring `mercurius`. 6 | 7 | Here is a simple example on how to enable tracing on Mercurius with OpenTelemetry: 8 | 9 | tracer.js 10 | ```js 11 | 'use strict' 12 | 13 | const api = require('@opentelemetry/api') 14 | const { NodeTracerProvider } = require('@opentelemetry/node') 15 | const { SimpleSpanProcessor } = require('@opentelemetry/tracing') 16 | const { JaegerExporter } = require('@opentelemetry/exporter-jaeger') 17 | const { GraphQLInstrumentation } = require('@opentelemetry/instrumentation-graphql') 18 | const { W3CTraceContextPropagator } = require('@opentelemetry/core') 19 | const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin') 20 | // or 21 | // const { JaegerExporter } = require('@opentelemetry/exporter-jaeger') 22 | 23 | module.exports = serviceName => { 24 | const provider = new NodeTracerProvider() 25 | const graphQLInstrumentation = new GraphQLInstrumentation() 26 | graphQLInstrumentation.setTracerProvider(provider) 27 | graphQLInstrumentation.enable() 28 | 29 | api.propagation.setGlobalPropagator(new W3CTraceContextPropagator()) 30 | api.trace.setGlobalTracerProvider(provider) 31 | 32 | provider.addSpanProcessor( 33 | new SimpleSpanProcessor( 34 | new ZipkinExporter({ 35 | serviceName 36 | }) 37 | // or 38 | // new JaegerExporter({ 39 | // serviceName, 40 | // }) 41 | ) 42 | ) 43 | provider.register() 44 | return provider 45 | } 46 | ``` 47 | 48 | serviceAdd.js 49 | ```js 50 | 'use strict' 51 | // Register tracer 52 | const serviceName = 'service-add' 53 | const tracer = require('./tracer') 54 | tracer(serviceName) 55 | 56 | const service = require('fastify')({ logger: { level: 'debug' } }) 57 | const mercurius = require('mercurius') 58 | const opentelemetry = require('@autotelic/fastify-opentelemetry') 59 | 60 | service.register(opentelemetry, { serviceName }) 61 | service.register(mercurius, { 62 | schema: ` 63 | extend type Query { 64 | add(x: Float, y: Float): Float 65 | } 66 | `, 67 | resolvers: { 68 | Query: { 69 | add: (_, { x, y }, { reply }) => { 70 | const { activeSpan, tracer } = reply.request.openTelemetry() 71 | 72 | activeSpan.setAttribute('arg.x', x) 73 | activeSpan.setAttribute('arg.y', y) 74 | 75 | const span = tracer.startSpan('compute-add', { parent: tracer.getCurrentSpan() }) 76 | const result = x + y 77 | span.end() 78 | 79 | return result 80 | } 81 | } 82 | }, 83 | }) 84 | 85 | service.listen({ port: 4001, host: 'localhost' }, err => { 86 | if (err) { 87 | console.error(err) 88 | process.exit(1) 89 | } 90 | }) 91 | ``` 92 | 93 | gateway.js 94 | ```js 95 | 'use strict' 96 | const serviceName = 'gateway' 97 | const tracer = require('./tracer') 98 | // Register tracer 99 | tracer(serviceName) 100 | 101 | const gateway = require('fastify')({ logger: { level: 'debug' } }) 102 | const mercurius = require('mercurius') 103 | const opentelemetry = require('@autotelic/fastify-opentelemetry') 104 | 105 | // Register fastify opentelemetry 106 | gateway.register(opentelemetry, { serviceName }) 107 | gateway.register(mercurius, { 108 | gateway: { 109 | services: [ 110 | { 111 | name: 'add', 112 | url: 'http://localhost:4001/graphql' 113 | } 114 | ] 115 | } 116 | }) 117 | 118 | gateway.listen({ port: 3000, host: 'localhost' }, err => { 119 | if (err) { 120 | process.exit(1) 121 | } 122 | }) 123 | ``` 124 | 125 | Start a zipkin service: 126 | 127 | ``` 128 | $ docker run -d -p 9411:9411 openzipkin/zipkin 129 | ``` 130 | 131 | Send some request to the gateway: 132 | 133 | ```bash 134 | $ curl localhost:3000/graphql -H 'Content-Type: application/json' --data '{"query":"{ add(x: 1, y: 2) }"}' 135 | ``` 136 | 137 | You can now browse through mercurius tracing at `http://localhost:9411` 138 | -------------------------------------------------------------------------------- /docs/integrations/prisma.md: -------------------------------------------------------------------------------- 1 | # Integrating Prisma with Mercurius 2 | 3 | [Prisma](https://prisma.io) is an [open-source](https://github.com/prisma/prisma) ORM for Node.js and TypeScript. 4 | It can be used as an _alternative_ to writing plain SQL, or using another database access tool such as SQL query builders (e.g. [knex.js](https://knexjs.org/)) or ORMs (like [TypeORM](https://typeorm.io/) and [Sequelize](https://sequelize.org/)). 5 | Prisma currently supports PostgreSQL, MySQL, SQL Server, MongoDB, CockroachDB, and SQLite. 6 | 7 | You can easily combine Prisma and Mercurius to build your GraphQL server that connects to a database. Prisma is agnostic to the GraphQL tools you use when building your GraphQL server. Check out this [GitHub repo](https://github.com/2color/fastify-graphql-nexus-prisma) for a ready-to-run example project with a PosgreSQL database. 8 | 9 | Prisma can be used with plain JavaScript and it embraces TypeScript and provides a level to type-safety that goes beyond the guarantees other ORMs in the TypeScript ecosystem. You can find an in-depth comparison of Prisma against other ORMs [here](https://www.prisma.io/docs/concepts/more/comparisons) 10 | 11 | ## Installation 12 | 13 | Install [Prisma CLI](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-cli) as a development dependency in your project: 14 | 15 | ```bash 16 | npm install prisma --save-dev 17 | npm install @prisma/client 18 | ``` 19 | 20 | [Prisma Client](https://www.prisma.io/docs/reference/api-reference/prisma-client-reference) is an auto-generated database client that allows you to interact with your database in a type-safe way. 21 | 22 | Initialize Prisma in your project: 23 | ```bash 24 | npx prisma init 25 | ``` 26 | 27 | This command does the following: 28 | - Creates a new directory called `prisma` which contains a file called `schema.prisma`. This file defines your database connection and the Prisma Client generator. 29 | - Creates a `.env` file at the root of your project. This defines your environment variables (used for your database connection). 30 | 31 | ## Connect to your database 32 | 33 | To connect to your database, set the `url` field of the `datasource` block in your Prisma schema to your database connection URL. By default, it's set to `postgresql` but this guide will use SQLite database. Adjust your `datasource` block to `sqlite`: 34 | 35 | ```prisma 36 | /// prisma/schema.prisma 37 | datasource db { 38 | provider = "sqlite" 39 | url = env("DATABASE_URL") 40 | } 41 | 42 | generator client { 43 | provider = "prisma-client-js" 44 | } 45 | ``` 46 | 47 | Update the `DATABASE_URL` environment variable in the `.env` file: 48 | 49 | ``` 50 | # .env 51 | DATABASE_URL="file:./dev.db" 52 | ``` 53 | 54 | If you wish to use a different database, you can jump to [switching database providers](#switching-database-providers). 55 | 56 | ## Create database tables with Prisma Migrate 57 | 58 | Add the following model to your `prisma.schema` file: 59 | 60 | ```prisma 61 | model Post { 62 | id Int @id @default(autoincrement()) 63 | title String 64 | body String 65 | published Boolean 66 | } 67 | ``` 68 | 69 | To map your data model to the database schema, you need to use `prisma migrate` CLI commands: 70 | 71 | ```bash 72 | npx prisma migrate dev --name init 73 | ``` 74 | 75 | The above command does three things: 76 | 1. Creates a new SQL migration file for this migration 77 | 1. Creates the database if it does not exist 78 | 1. Runs the SQL migration against the database 79 | 1. Generates Prisma Client 80 | 81 | ## Set up your GraphQL server 82 | 83 | ```js 84 | // index.js 85 | 'use strict' 86 | const Fastify = require('fastify') 87 | const mercurius = require('mercurius') 88 | const { PrismaClient } = require('@prisma/client') 89 | 90 | const app = Fastify() 91 | const prisma = new PrismaClient() 92 | 93 | const schema = ` 94 | type Mutation { 95 | createDraft(body: String!, title: String!): Post 96 | publish(draftId: Int!): Post 97 | } 98 | 99 | type Post { 100 | body: String 101 | id: Int 102 | published: Boolean 103 | title: String 104 | } 105 | 106 | type Query { 107 | drafts: [Post] 108 | posts: [Post] 109 | } 110 | ` 111 | 112 | const resolvers = { 113 | Query: { 114 | posts: async (_parent, args, ctx) => { 115 | return ctx.prisma.post.findMany({ 116 | where: { 117 | published: true 118 | } 119 | }) 120 | }, 121 | drafts: async (_parent, args, ctx) => { 122 | return ctx.prisma.post.findMany({ 123 | where: { 124 | published: false 125 | } 126 | }) 127 | }, 128 | }, 129 | Mutation: { 130 | createDraft: async (_parent, args, ctx) => { 131 | return ctx.prisma.post.create({ 132 | data: { 133 | title: args.title, 134 | body: args.body, 135 | } 136 | }) 137 | }, 138 | publish: async (_parent, args, ctx) => { 139 | return ctx.prisma.post.update({ 140 | where: { id: args.draftId }, 141 | data: { published: true } 142 | }) 143 | }, 144 | } 145 | } 146 | 147 | app.register(mercurius, { 148 | schema, 149 | resolvers, 150 | context: (request, reply) => { 151 | return { prisma } 152 | }, 153 | graphiql: true 154 | }) 155 | 156 | app.listen({ port: 3000 }) 157 | .then(() => console.log(`🚀 Server ready at http://localhost:3000/graphiql`)) 158 | 159 | ``` 160 | 161 | Start your application: 162 | ```bash 163 | node index.js 164 | ``` 165 | 166 | ## Switching database providers 167 | 168 | If you want to switch to a different database other than SQLite, you can adjust the database connection in `prisma/prisma.schema` by reconfiguring the `datasource` block. 169 | 170 | Learn more about the different connection configurations in the [docs](https://www.prisma.io/docs/reference/database-reference/connection-urls). 171 | 172 | Here's an overview of an example configuration with different databases: 173 | 174 | ### PostgreSQL 175 | 176 | Here is an example connection string with a local PostgreSQL database: 177 | 178 | ```prisma 179 | datasource db { 180 | provider = "postgresql" 181 | url = env("DATABASE_URL") 182 | } 183 | ``` 184 | 185 | ### MySQL 186 | 187 | Here is an example connection string with a local MySQL database: 188 | 189 | ```prisma 190 | datasource db { 191 | provider = "mysql" 192 | url = env("DATABASE_URL") 193 | } 194 | ``` 195 | 196 | ### SQL Server 197 | 198 | Here is an example connection string with a local Microsoft SQL Server database: 199 | 200 | ```prisma 201 | datasource db { 202 | provider = "sqlserver" 203 | url = env("DATABASE_URL") 204 | } 205 | ``` 206 | 207 | ### CockroachDB 208 | 209 | Here is an example connection string with a local CockroachDB database: 210 | 211 | ```prisma 212 | datasource db { 213 | provider = "cockroachdb" 214 | url = env("DATABASE_URL") 215 | } 216 | ``` 217 | 218 | ### MongoDB 219 | 220 | Here is an example connection string with a local MongoDB database: 221 | 222 | ```prisma 223 | datasource db { 224 | provider = "mongodb" 225 | url = env("DATABASE_URL") 226 | } 227 | ``` 228 | -------------------------------------------------------------------------------- /docs/integrations/type-graphql.md: -------------------------------------------------------------------------------- 1 | # Integrating TypeGraphQL with Mercurius 2 | 3 | You can easily use [TypeGraphQL](https://github.com/MichalLytek/type-graphql) in combination with Mercurius. 4 | This allows you to follow a code first approach instead of the SDL first. 5 | 6 | ## Installation 7 | 8 | ```bash 9 | npm install --save type-graphql graphql reflect-metadata 10 | ``` 11 | 12 | Now you can define a schema using classes and decorators: 13 | 14 | ```ts 15 | // recipe.ts 16 | import { Arg, Field, ObjectType, Int, Float, Resolver, Query } from "type-graphql"; 17 | 18 | @ObjectType({ description: "Object representing cooking recipe" }) 19 | export class Recipe { 20 | @Field() 21 | title: string; 22 | 23 | @Field((type) => String, { 24 | nullable: true, 25 | deprecationReason: "Use `description` field instead", 26 | }) 27 | get specification(): string | undefined { 28 | return this.description; 29 | } 30 | 31 | @Field({ 32 | nullable: true, 33 | description: "The recipe description with preparation info", 34 | }) 35 | description?: string; 36 | 37 | @Field((type) => [Int]) 38 | ratings: number[]; 39 | 40 | @Field() 41 | creationDate: Date; 42 | } 43 | 44 | @Resolver() 45 | export class RecipeResolver { 46 | @Query((returns) => Recipe, { nullable: true }) 47 | async recipe(@Arg("title") title: string): Promise | undefined> { 48 | return { 49 | description: "Desc 1", 50 | title: title, 51 | ratings: [0, 3, 1], 52 | creationDate: new Date("2018-04-11"), 53 | }; 54 | } 55 | } 56 | ``` 57 | 58 | This can be linked to the Mercurius plugin: 59 | 60 | ```ts 61 | // index.ts 62 | import "reflect-metadata"; 63 | import fastify, {FastifyRegisterOptions} from "fastify"; 64 | import mercurius, {MercuriusOptions} from "mercurius"; 65 | import { buildSchema } from 'type-graphql' 66 | 67 | import { RecipeResolver } from "./recipe"; 68 | 69 | async function main() { 70 | // build TypeGraphQL executable schema 71 | const schema = await buildSchema({ 72 | resolvers: [RecipeResolver], 73 | }); 74 | 75 | const app = fastify(); 76 | 77 | const opts: FastifyRegisterOptions = { 78 | schema, 79 | graphiql: true 80 | } 81 | app.register(mercurius, opts); 82 | 83 | app.get("/", async (req, reply) => { 84 | const query = `{ 85 | recipe(title: "Recipe 1") { 86 | title 87 | description 88 | ratings 89 | creationDate 90 | } 91 | }`; 92 | return reply.graphql(query); 93 | }); 94 | 95 | app.listen({ port: 3000 }); 96 | } 97 | 98 | main().catch(console.error); 99 | ``` 100 | 101 | If you run this, you will get a GraphQL API based on your code: 102 | 103 | ```bash 104 | ts-node index.ts 105 | ``` 106 | 107 | ## Class validators 108 | 109 | One of the features of `type-graphql` is ability to add validation rules using decorators. Let's say we want to add 110 | a mutation with some simple validation rules for its input. First we need to define the class for the input: 111 | 112 | ```ts 113 | @InputType() 114 | export class RecipeInput { 115 | @Field() 116 | @MaxLength(30) 117 | title: string; 118 | 119 | @Field({ nullable: true }) 120 | @Length(30, 255) 121 | description?: string; 122 | } 123 | ``` 124 | 125 | Then add a method in the `RecipeResolver` that would serve as a mutation implementation: 126 | 127 | ```ts 128 | @Mutation(returns => Recipe) 129 | async addRecipe(@Arg("input") recipeInput: RecipeInput): Promise { 130 | const recipe = new Recipe(); 131 | recipe.description = recipeInput.description; 132 | recipe.title = recipeInput.title; 133 | recipe.creationDate = new Date(); 134 | return recipe; 135 | } 136 | ``` 137 | 138 | Now, here we can run into a problem. Getting the details of validation errors can get confusing. Normally, the default 139 | error formatter of `mercurius` will handle the error, log them and carry over the details to the response of API call. 140 | The problem is that validation errors coming from `type-graphql` are stored in `originalError` field (in contrast to 141 | the `extensions` field, which was designed to be carrying such data) of `GraphQLError` object, which is a non-enumerable 142 | property (meaning it won't get serialized/logged). 143 | 144 | An easy workaround would be to copy the validation details from `originalError` to `extensions` field using custom error 145 | formatter. The problem is that in GraphQLError's constructor method, if the extensions are empty initially, then this 146 | field is being marked as a non-enumerable as well. To work this problem around you could do something like this: 147 | 148 | ```ts 149 | const app = fastify({ logger: { level: 'info' } }); 150 | const opts: FastifyRegisterOptions = { 151 | schema, 152 | graphiql: true, 153 | errorFormatter: (executionResult, context) => { 154 | const log = context.reply ? context.reply.log : context.app.log; 155 | const errors = executionResult.errors.map((error) => { 156 | error.extensions.exception = error.originalError; 157 | Object.defineProperty(error, 'extensions', {enumerable: true}); 158 | return error; 159 | }); 160 | log.info({ err: executionResult.errors }, 'Argument Validation Error'); 161 | return { 162 | statusCode: 201, 163 | response: { 164 | data: executionResult.data, 165 | errors 166 | } 167 | } 168 | } 169 | } 170 | app.register(mercurius, opts); 171 | ``` -------------------------------------------------------------------------------- /docs/lifecycle.md: -------------------------------------------------------------------------------- 1 | # Lifecycle 2 | 3 | The schema of the internal lifecycle of Mercurius.
4 | 5 | On the right branch of every section there is the next phase of the lifecycle, on the left branch there is the corresponding GraphQL error(s) that will be generated if the parent throws an error *(note that all the errors are automatically handled by Mercurius)*. 6 | 7 | ## Normal lifecycle 8 | 9 | ``` 10 | Incoming GraphQL Request 11 | │ 12 | └─▶ Routing 13 | │ 14 | errors ◀─┴─▶ preParsing Hook 15 | │ 16 | errors ◀─┴─▶ Parsing 17 | │ 18 | errors ◀─┴─▶ preValidation Hook 19 | │ 20 | errors ◀─┴─▶ Validation 21 | │ 22 | errors ◀─┴─▶ preExecution Hook 23 | │ 24 | errors ◀─┴─▶ Execution 25 | │ 26 | errors ◀─┴─▶ Resolution 27 | │ 28 | └─▶ onResolution Hook 29 | ``` 30 | 31 | 32 | ## Subscription lifecycle 33 | 34 | ``` 35 | Incoming GraphQL Websocket subscription data 36 | │ 37 | └─▶ Routing 38 | │ 39 | errors ◀─┴─▶ preSubscriptionParsing Hook 40 | │ 41 | errors ◀─┴─▶ Subscription Parsing 42 | │ 43 | errors ◀─┴─▶ preSubscriptionExecution Hook 44 | │ 45 | errors ◀─┴─▶ Subscription Execution 46 | │ 47 | wait for subscription data 48 | │ 49 | subscription closed on error ◀─┴─▶ Subscription Resolution (when subscription data is received) 50 | │ 51 | └─▶ onSubscriptionResolution Hook 52 | │ 53 | keeping processing until subscription ended 54 | │ 55 | subscription closed on error ◀─┴─▶ Subscription End (when subscription stop is received) 56 | │ 57 | └─▶ onSubscriptionEnd Hook 58 | ``` 59 | -------------------------------------------------------------------------------- /docs/loaders.md: -------------------------------------------------------------------------------- 1 | # mercurius 2 | 3 | ## Loaders 4 | 5 | A loader is an utility to avoid the 1 + N query problem of GraphQL. 6 | Each defined loader will register a resolver that coalesces each of the 7 | request and combines them into a single, bulk query. Moreover, it can 8 | also cache the results, so that other parts of the GraphQL do not have 9 | to fetch the same data. 10 | 11 | Each loader function has the signature `loader(queries, context)`. 12 | `queries` is an array of objects defined as `{ obj, params, info }` where 13 | `obj` is the current object, `params` are the GraphQL params (those 14 | are the first two parameters of a normal resolver) and `info` contains 15 | additional information about the query and execution. `info` object is 16 | only available in the loader if the cache is set to `false`. The `context` 17 | is the GraphQL context, and it includes a `reply` object. 18 | 19 | Example: 20 | 21 | ```js 22 | const loaders = { 23 | Dog: { 24 | async owner (queries, { reply }) { 25 | return queries.map(({ obj, params }) => owners[obj.name]) 26 | } 27 | } 28 | } 29 | 30 | app.register(mercurius, { 31 | schema, 32 | resolvers, 33 | loaders 34 | }) 35 | ``` 36 | 37 | It is also possible disable caching with: 38 | 39 | ```js 40 | const loaders = { 41 | Dog: { 42 | owner: { 43 | async loader (queries, { reply }) { 44 | return queries.map(({ obj, params, info }) => { 45 | // info is available only if the loader is not cached 46 | owners[obj.name] 47 | }) 48 | }, 49 | opts: { 50 | cache: false 51 | } 52 | } 53 | } 54 | } 55 | 56 | app.register(mercurius, { 57 | schema, 58 | resolvers, 59 | loaders 60 | }) 61 | ``` 62 | 63 | Alternatively, globally disabling caching also disable the Loader cache: 64 | 65 | ```js 66 | const loaders = { 67 | Dog: { 68 | async owner (queries, { reply }) { 69 | return queries.map(({ obj, params, info }) => { 70 | // info is available only if the loader is not cached 71 | owners[obj.name] 72 | }) 73 | } 74 | } 75 | } 76 | 77 | app.register(mercurius, { 78 | schema, 79 | resolvers, 80 | loaders, 81 | cache: false 82 | }) 83 | ``` 84 | 85 | Disabling caching has the advantage to avoid the serialization at 86 | the cost of more objects to fetch in the resolvers. 87 | 88 | Internally, it uses 89 | [single-user-cache](http://npm.im/single-user-cache). 90 | -------------------------------------------------------------------------------- /docs/persisted-queries.md: -------------------------------------------------------------------------------- 1 | # mercurius 2 | 3 | ## Persisted Queries 4 | 5 | GraphQL query strings are often larger than the URLs used in REST requests, sometimes by many kilobytes. 6 | 7 | Depending on the client, this can be a significant overhead for each request, especially given that upload speed is typically the most bandwidth-constrained part of the request lifecycle. Large queries can add significant performance overheads. 8 | 9 | Persisted Queries solve this problem by having the client send a generated ID, instead of the full query string, resulting in a smaller request. The server can use an internal lookup to turn this back into a full query and return the result. 10 | 11 | The `persistedQueryProvider` option lets you configure this for Fastify mercurius. There are a few default options available, included in `mercurius.persistedQueryDefaults`. 12 | 13 | ### Prepared 14 | 15 | Prepared queries give the best performance in all use cases, at the expense of tooling complexity. Queries must be hashed ahead of time, and a matching set of hashes must be available for both the client and the server. Additionally, version control of query hashes must be considered, e.g. queries used by old clients may need to be kept such that hashes can be calculated at build time. This can be very useful for non-public APIs, but is impractical for public APIs. 16 | 17 | Clients can provide a full query string, or set the `persisted` flag to true and provide a hash instead of the query in the request: 18 | 19 | ```js 20 | { 21 | query: '', 22 | persisted: true 23 | } 24 | ``` 25 | 26 | A map of hashes to queries must be provided to the server at startup: 27 | 28 | ```js 29 | const mercurius = require('mercurius') 30 | 31 | app.register(mercurius, { 32 | ... 33 | persistedQueryProvider: mercurius.persistedQueryDefaults.prepared({ 34 | '': '{ add(x: 1, y: 1) }' 35 | }) 36 | }) 37 | ``` 38 | 39 | Alternatively the `peristedQueries` option may be used directly, which will be internally mapped to the `prepared` default: 40 | 41 | ```js 42 | const mercurius = require('mercurius') 43 | 44 | app.register(mercurius, { 45 | ... 46 | persistedQueries: { 47 | '': '{ add(x: 1, y: 1) }' 48 | } 49 | }) 50 | ``` 51 | 52 | ### Prepared Only 53 | 54 | This offers similar performance and considerations to the `prepared` queries, but only allows persisted queries. This provides additional secuirity benefits, but means that the server **must** know all queries ahead of time or will reject the request. 55 | 56 | The API is the same as the `prepared` default. 57 | 58 | Alternatively the `peristedQueries` and `onlyPersisted` options may be used directly, which will be internally mapped to the `preparedOnly` default: 59 | 60 | ```js 61 | const mercurius = require('mercurius') 62 | 63 | app.register(mercurius, { 64 | ... 65 | persistedQueries: { 66 | '': '{ add(x: 1, y: 1) }' 67 | }, 68 | onlyPersisted: true 69 | }) 70 | ``` 71 | 72 | ### Automatic 73 | 74 | This default is compatible with `apollo-client`, and requires no additional tooling to set up at the cost of some performance. In order for this mode to be effective, you must have long lived server instances (i.e _not_ cloud functions). This mode is also appropriate for public APIs where queries are not known ahead of time. 75 | 76 | When an unrecognised hash is recieved by the server instance, an error is thrown informing the client that the persisted query has not been seen before. The client then re-sends the full query string. When a full query string is recieved, the server caches the hash of the query string and returns the response. _Note that sticky sessions should be used to ensure optimal performance here by making sure the follow up request is sent to the same server instance._ 77 | 78 | The next request for that query (from the same or a different client) will already have been cached and will then be looked up accordingly. 79 | 80 | When the server initially starts, no queries will be cached and additional latency will be added to the first requests recieved (due to the client re-sending the full query). However, the most common queries will rapidly be cached by the server. After a warmup (length dependent on the number of queries clients might send and how frequent they are) performance will match that of the `prepared` query option. 81 | 82 | Additional documentation on Apollo's automatic persisted queries implementation can be found [here](https://www.apollographql.com/docs/apollo-server/performance/apq/). 83 | 84 | Example: 85 | 86 | ```js 87 | const mercurius = require('mercurius') 88 | 89 | app.register(mercurius, { 90 | ... 91 | persistedQueryProvider: mercurius.persistedQueryDefaults.automatic() 92 | }) 93 | ``` 94 | 95 | An LRU cache is used to prevent DoS attacks on the storage of hashes & queries. The maximum size of this cache (maximum number of cached queries) can be adjusted by passing a value to the constructor, for example: 96 | 97 | ```js 98 | const mercurius = require('mercurius') 99 | 100 | app.register(mercurius, { 101 | ... 102 | persistedQueryProvider: mercurius.persistedQueryDefaults.automatic(5000) 103 | }) 104 | ``` 105 | 106 | ### Custom Persisted Queries 107 | 108 | It is also possible to extend or modify these persisted query implementations for custom cases, such as automatic Persisted Queries, but with a shared cache between servers. 109 | 110 | This would enable all persisted queries to be shared between all server instances in a cache which is dynamically populated. The lookup time from the cache is an additional overhead for each request, but a higher rate of persisted query matches would be achieved. This may be beneficial, for example, in a public facing API which supports persisted queries and uses cloud functions (short lived server instances). _Note the performance impacts of this need to be considered thoroughly: the latency added to each request must be less than the savings from smaller requests._ 111 | 112 | A example of using this with Redis would be: 113 | 114 | ```js 115 | const mercurius = require('mercurius') 116 | 117 | const persistedQueryProvider = { 118 | ...mercurius.persistedQueryDefaults.automatic(), 119 | getQueryFromHash: async (hash) => redis.get(hash), 120 | saveQuery: async (hash, query) => redis.set(hash, query) 121 | } 122 | 123 | app.register(mercurius, { 124 | ...persistedQueryProvider 125 | }) 126 | ``` 127 | -------------------------------------------------------------------------------- /docs/plugins.md: -------------------------------------------------------------------------------- 1 | # Plugins 2 | 3 | Related plugins for mercurius 4 | 5 | - [mercurius-auth](#mercurius-auth) 6 | - [mercurius-cache](#mercurius-cache) 7 | - [mercurius-validation](#mercurius-validation) 8 | - [mercurius-upload](#mercurius-upload) 9 | - [altair-fastify-plugin](#altair-fastify-plugin) 10 | - [mercurius-apollo-registry](#mercurius-apollo-registry) 11 | - [mercurius-apollo-tracing](#mercurius-apollo-tracing) 12 | - [mercurius-postgraphile](#mercurius-postgraphile) 13 | - [mercurius-logging](#mercurius-logging) 14 | - [mercurius-fetch](#mercurius-fetch) 15 | - [mercurius-hit-map](#mercurius-hit-map) 16 | 17 | ## mercurius-auth 18 | 19 | Mercurius Auth is a plugin for [Mercurius](https://mercurius.dev) that adds configurable Authentication and Authorization support. 20 | 21 | Check the [`mercurius-auth` documentation](https://github.com/mercurius-js/auth) for detailed usage. 22 | 23 | ## mercurius-cache 24 | 25 | Mercurius Cache is a plugin for [Mercurius](https://mercurius.dev) that caches the results of your GraphQL resolvers, for Mercurius. 26 | 27 | Check the [`mercurius-cache` documentation](https://github.com/mercurius-js/cache) for detailed usage. 28 | 29 | ## mercurius-validation 30 | 31 | Mercurius Validation is a plugin for [Mercurius](https://mercurius.dev) that adds configurable validation support. 32 | 33 | Check the [`mercurius-validation` documentation](https://github.com/mercurius-js/validation) for detailed usage. 34 | 35 | ## mercurius-upload 36 | 37 | Implementation of [graphql-upload](https://github.com/jaydenseric/graphql-upload) for File upload support. 38 | 39 | Check [https://github.com/mercurius-js/mercurius-upload](https://github.com/mercurius-js/mercurius-upload) for detailed usage. 40 | 41 | ## altair-fastify-plugin 42 | 43 | [**Altair**](https://altairgraphql.dev/) plugin. Fully featured GraphQL Client IDE, good alternative of `graphiql`. 44 | 45 | ```bash 46 | npm install altair-fastify-plugin 47 | ``` 48 | 49 | ```js 50 | const AltairFastify = require('altair-fastify-plugin') 51 | // ... 52 | const app = Fastify() 53 | 54 | app.register(mercurius, { 55 | // ... 56 | graphiql: false, 57 | ide: false, 58 | path: '/graphql' 59 | }) 60 | // ... 61 | app.register(AltairFastify, { 62 | path: '/altair', 63 | baseURL: '/altair/', 64 | // 'endpointURL' should be the same as the mercurius 'path' 65 | endpointURL: '/graphql' 66 | }) 67 | 68 | app.listen({ port: 3000 }) 69 | ``` 70 | 71 | And it will be available at `http://localhost:3000/altair` 🎉 72 | 73 | Check [here](https://github.com/imolorhe/altair/tree/staging/packages/altair-fastify-plugin) for more information. 74 | 75 | ## mercurius-apollo-registry 76 | 77 | A Mercurius plugin for schema reporting to Apollo Studio. 78 | 79 | Check [https://github.com/nearform/mercurius-apollo-registry](https://github.com/nearform/mercurius-apollo-registry) for usage and readme. 80 | 81 | ```bash 82 | npm install mercurius-apollo-registry 83 | ``` 84 | 85 | ```js 86 | const app = Fastify() 87 | const mercurius = require('mercurius') 88 | const mercuriusApolloRegistry = require('mercurius-apollo-registry') 89 | 90 | const schema = `define schema here` 91 | const resolvers = { 92 | // ... 93 | } 94 | 95 | app.register(mercurius, { 96 | schema, 97 | resolvers, 98 | graphiql: true 99 | }) 100 | 101 | app.register(mercuriusApolloRegistry, { 102 | schema, 103 | apiKey: 'REPLACE-THIS-VALUE-WITH-APOLLO-API-KEY' 104 | }) 105 | 106 | app.listen({ port: 3000 }) 107 | ``` 108 | 109 | ## mercurius-apollo-tracing 110 | 111 | A Mercurius plugin for reporting performance metrics and errors to Apollo Studio. 112 | 113 | ```bash 114 | npm install mercurius-apollo-tracing 115 | ``` 116 | 117 | ```js 118 | const mercuriusTracing = require('mercurius-apollo-tracing') 119 | 120 | app.register(mercuriusTracing, { 121 | apiKey: 'REPLACE-THIS-VALUE-WITH-APOLLO-API-KEY', // replace with the one from apollo studio 122 | graphRef: 'yourGraph@ref' // replace 'yourGraph@ref'' with the one from apollo studio 123 | }) 124 | ``` 125 | 126 | ## mercurius-postgraphile 127 | A Mercurius plugin for integrating PostGraphile schemas with Mercurius 128 | 129 | Check [https://github.com/autotelic/mercurius-postgraphile](https://github.com/autotelic/mercurius-postgraphile) for usage and readme. 130 | 131 | ## mercurius-logging 132 | A Mercurius plugin to enhance the GQL request logging adding useful insights: 133 | 134 | ```json 135 | { 136 | "level": 30, 137 | "time": 1660395516406, 138 | "hostname": "eomm", 139 | "reqId": "req-1", 140 | "graphql": { 141 | "queries": [ 142 | "firstQuery:myTeam", 143 | "secondQuery:myTeam" 144 | ] 145 | } 146 | } 147 | ``` 148 | 149 | Check the [`mercurius-logging`](https://github.com/Eomm/mercurius-logging) documentation for usage and settings. 150 | 151 | ## mercurius-fetch 152 | Mercurius Fetch is a plugin for [Mercurius](https://mercurius.dev) that adds fetch to a rest api directly on query or properties of query. 153 | 154 | Check the [`mercurius-fetch` documentation](https://github.com/rbonillajr/mercurius-fetch) for detailed usage. 155 | 156 | ## mercurius-hit-map 157 | A Mercurius plugin to count how many times the application's resolvers are executed by the clients. 158 | 159 | ```js 160 | const app = Fastify() 161 | app.register(mercurius, { 162 | schema, 163 | resolvers 164 | }) 165 | 166 | app.register(require('mercurius-hit-map')) 167 | 168 | app.get('/hit', async () => { 169 | const hitMap = await app.getHitMap() 170 | return hitMap 171 | }) 172 | ``` 173 | 174 | Check the [`mercurius-hit-map`](https://github.com/Eomm/mercurius-hit-map) documentation for usage and settings. 175 | -------------------------------------------------------------------------------- /docs/typescript.md: -------------------------------------------------------------------------------- 1 | # TypeScript usage 2 | 3 | > Complete example are available in [https://github.com/mercurius-js/mercurius-typescript](https://github.com/mercurius-js/mercurius-typescript). 4 | 5 | Mercurius has included type definitions, that you can use in your projects manually if you wish, but you can also use [mercurius-codegen](https://github.com/mercurius-js/mercurius-typescript/tree/master/packages/mercurius-codegen), which is designed to improve the TypeScript experience using [GraphQL Code Generator](https://graphql-code-generator.com/) seamlessly while you code, but this documentation will show you how to use both. 6 | 7 | ## Codegen 8 | 9 | Install [mercurius-codegen](https://github.com/mercurius-js/mercurius-typescript/tree/master/packages/mercurius-codegen): 10 | 11 | ```bash 12 | npm install mercurius-codegen 13 | # or your preferred package manager 14 | ``` 15 | 16 | Then in your code 17 | 18 | ```ts 19 | import Fastify, { FastifyRequest, FastifyReply } from 'fastify' 20 | import mercurius, { IResolvers } from 'mercurius' 21 | import mercuriusCodegen, { gql } from 'mercurius-codegen' 22 | 23 | const app = Fastify() 24 | 25 | const buildContext = async (req: FastifyRequest, _reply: FastifyReply) => { 26 | return { 27 | authorization: req.headers.authorization 28 | } 29 | } 30 | 31 | type PromiseType = T extends PromiseLike ? U : T 32 | 33 | declare module 'mercurius' { 34 | interface MercuriusContext extends PromiseType> {} 35 | } 36 | 37 | // Using the fake "gql" from mercurius-codegen gives tooling support for 38 | // "prettier formatting" and "IDE syntax highlighting". 39 | // It's optional 40 | const schema = gql` 41 | type Query { 42 | hello(name: String!): String! 43 | } 44 | ` 45 | 46 | const resolvers: IResolvers = { 47 | Query: { 48 | hello(root, { name }, ctx, info) { 49 | // root ~ {} 50 | // name ~ string 51 | // ctx.authorization ~ string | undefined 52 | // info ~ GraphQLResolveInfo 53 | return 'hello ' + name 54 | } 55 | } 56 | } 57 | 58 | app.register(mercurius, { 59 | schema, 60 | resolvers, 61 | context: buildContext 62 | }) 63 | 64 | mercuriusCodegen(app, { 65 | // Commonly relative to your root package.json 66 | targetPath: './src/graphql/generated.ts' 67 | }).catch(console.error) 68 | ``` 69 | 70 | Then automatically while you code the types are going to be generated and give you type-safety and auto-completion. 71 | 72 | You can check the more detailed documentation [here](https://github.com/mercurius-js/mercurius-typescript/tree/master/packages/mercurius-codegen) and two complete examples using GraphQL Operations, [Loaders](/docs/loaders.md), [Subscriptions](/docs/subscriptions.md), and [Full integration testing](/docs/integrations/mercurius-integration-testing.md) in [mercurius-typescript/examples/codegen](https://github.com/mercurius-js/mercurius-typescript/tree/master/examples/codegen), and an even further example that uses `.gql` files to make your GraphQL Schema in [**mercurius-typescript/examples/codegen-gql-files**](https://github.com/mercurius-js/mercurius-typescript/tree/master/examples/codegen-gql-files). 73 | 74 | ## Manually typing 75 | 76 | You can also use the included types with mercurius in your API 77 | 78 | ```ts 79 | import Fastify, { FastifyReply, FastifyRequest } from 'fastify' 80 | import mercurius, { 81 | IFieldResolver, 82 | IResolvers, 83 | MercuriusContext, 84 | MercuriusLoaders 85 | } from 'mercurius' 86 | 87 | export const app = Fastify() 88 | 89 | const buildContext = async (req: FastifyRequest, _reply: FastifyReply) => { 90 | return { 91 | authorization: req.headers.authorization 92 | } 93 | } 94 | 95 | type PromiseType = T extends PromiseLike ? U : T 96 | 97 | declare module 'mercurius' { 98 | interface MercuriusContext extends PromiseType> {} 99 | } 100 | 101 | const schema = ` 102 | type Query { 103 | helloTyped: String! 104 | helloInline: String! 105 | } 106 | ` 107 | 108 | const helloTyped: IFieldResolver< 109 | {} /** Root */, 110 | MercuriusContext /** Context */, 111 | {} /** Args */ 112 | > = (root, args, ctx, info) => { 113 | // root ~ {} 114 | root 115 | // args ~ {} 116 | args 117 | // ctx.authorization ~ string | undefined 118 | ctx.authorization 119 | // info ~ GraphQLResolveInfo 120 | info 121 | 122 | return 'world' 123 | } 124 | 125 | const resolvers: IResolvers = { 126 | Query: { 127 | helloTyped, 128 | helloInline: (root: {}, args: {}, ctx, info) => { 129 | // root ~ {} 130 | root 131 | // args ~ {} 132 | args 133 | // ctx.authorization ~ string | undefined 134 | ctx.authorization 135 | // info ~ GraphQLResolveInfo 136 | info 137 | 138 | return 'world' 139 | } 140 | } 141 | } 142 | 143 | app.register(mercurius, { 144 | schema, 145 | resolvers, 146 | context: buildContext 147 | }) 148 | ``` 149 | 150 | You can check [**mercurius-typescript/examples/manual**](https://github.com/mercurius-js/mercurius-typescript/tree/master/examples/manual) for more detailed usage, using [Loaders](/docs/loaders.md), [Subscriptions](/docs/subscriptions.md) and [Full integration testing](/docs/integrations/mercurius-integration-testing.md) 151 | -------------------------------------------------------------------------------- /docsify/sidebar.md: -------------------------------------------------------------------------------- 1 | - [**Home**](/) 2 | - [Install](/#install) 3 | - [Quick Start](/#quick-start) 4 | - [Examples](/#examples) 5 | - [Acknowledgements](/#acknowledgements) 6 | - [License](/#license) 7 | - [API](/docs/api/options) 8 | - [Plugin Options](/docs/api/options#plugin-options) 9 | - [HTTP Endpoints](/docs/api/options#http-endpoints) 10 | - [Decorators](/docs/api/options#decorators) 11 | - [Error Extensions](/docs/api/options#errors) 12 | - [Context](/docs/context) 13 | - [Loaders](/docs/loaders) 14 | - [Hooks](/docs/hooks) 15 | - [Lifecycle](/docs/lifecycle) 16 | - [Federation](/docs/federation) 17 | - [Subscriptions](/docs/subscriptions) 18 | - [Custom directives](/docs/custom-directive.md) 19 | - [Batched Queries](/docs/batched-queries) 20 | - [Persisted Queries](/docs/persisted-queries) 21 | - [TypeScript Usage](/docs/typescript) 22 | - [HTTP](/docs/http) 23 | - [GraphQL over WebSocket](/docs/graphql-over-websocket.md) 24 | - [Integrations](/docs/integrations/) 25 | - [nexus](/docs/integrations/nexus) 26 | - [TypeGraphQL](/docs/integrations/type-graphql) 27 | - [Prisma](/docs/integrations/prisma) 28 | - [Testing](/docs/integrations/mercurius-integration-testing) 29 | - [Tracing - OpenTelemetry](/docs/integrations/open-telemetry) 30 | - [NestJS](/docs/integrations/nestjs.md) 31 | - [Related Plugins](/docs/plugins) 32 | - [mercurius-auth](/docs/plugins#mercurius-auth) 33 | - [mercurius-cache](/docs/plugins#mercurius-cache) 34 | - [mercurius-validation](/docs/plugins#mercurius-validation) 35 | - [mercurius-upload](/docs/plugins#mercurius-upload) 36 | - [altair-fastify-plugin](/docs/plugins#altair-fastify-plugin) 37 | - [mercurius-apollo-registry](/docs/plugins#mercurius-apollo-registry) 38 | - [mercurius-apollo-tracing](/docs/plugins#mercurius-apollo-tracing) 39 | - [Development](/docs/development) 40 | - [Faq](/docs/faq) 41 | - [Contribute](/docs/contribute) 42 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = require('neostandard')({ 4 | ts: true, 5 | }) 6 | -------------------------------------------------------------------------------- /examples/basic.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | const mercurius = require('..') 5 | 6 | const app = Fastify() 7 | 8 | const schema = ` 9 | type Query { 10 | add(x: Int, y: Int): Int 11 | } 12 | ` 13 | 14 | const resolvers = { 15 | Query: { 16 | add: async (_, obj) => { 17 | const { x, y } = obj 18 | return x + y 19 | } 20 | } 21 | } 22 | 23 | app.register(mercurius, { 24 | schema, 25 | resolvers, 26 | graphiql: true 27 | }) 28 | 29 | app.get('/', async function (req, reply) { 30 | const query = '{ add(x: 2, y: 2) }' 31 | return reply.graphql(query) 32 | }) 33 | 34 | app.listen({ port: 3000 }) 35 | -------------------------------------------------------------------------------- /examples/custom-directive.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | const mercurius = require('..') 5 | const { makeExecutableSchema } = require('@graphql-tools/schema') 6 | const { mapSchema, getDirective, MapperKind } = require('@graphql-tools/utils') 7 | 8 | const app = Fastify() 9 | 10 | const resolvers = { 11 | Query: { 12 | documents: async (_, _obj, _ctx) => { 13 | return [{ 14 | excerpt: 'Proin info@mercurius.dev rutrum pulvinar lectus sed placerat.', 15 | text: 'Proin 33 222-33355 rutrum pulvinar lectus sed placerat.' 16 | }] 17 | } 18 | } 19 | } 20 | 21 | // Define the executable schema 22 | const schema = makeExecutableSchema({ 23 | typeDefs: ` 24 | # Define the directive schema 25 | directive @redact(find: String) on FIELD_DEFINITION 26 | 27 | type Document { 28 | excerpt: String! @redact(find: "email") 29 | text: String! @redact(find: "phone") 30 | } 31 | 32 | type Query { 33 | documents: [Document] 34 | } 35 | `, 36 | resolvers 37 | }) 38 | 39 | const PHONE_REGEXP = /(?:\+?\d{2}[ -]?\d{3}[ -]?\d{5}|\d{4})/g 40 | const EMAIL_REGEXP = /([^\s@])+@[^\s@]+\.[^\s@]+/g 41 | 42 | const redactionSchemaTransformer = (schema) => mapSchema(schema, { 43 | [MapperKind.OBJECT_FIELD]: fieldConfig => { 44 | const redactDirective = getDirective(schema, fieldConfig, 'redact')?.[0] 45 | 46 | if (redactDirective) { 47 | const { find } = redactDirective 48 | 49 | fieldConfig.resolve = async (obj, _args, ctx, info) => { 50 | const value = obj[info.fieldName] 51 | 52 | if (!ctx.redact) { 53 | return document 54 | } 55 | 56 | switch (find) { 57 | case 'email': 58 | return value.replace(EMAIL_REGEXP, '****@*****.***') 59 | case 'phone': 60 | return value.replace(PHONE_REGEXP, m => '*'.repeat(m.length)) 61 | default: 62 | return value 63 | } 64 | } 65 | } 66 | } 67 | }) 68 | 69 | // Register mercurius and run it 70 | app.register(mercurius, { 71 | schema: redactionSchemaTransformer(schema), 72 | context: (request, reply) => { 73 | return { 74 | redact: true 75 | } 76 | }, 77 | graphiql: true 78 | }) 79 | 80 | app.listen({ port: 3000 }) 81 | -------------------------------------------------------------------------------- /examples/custom-http-behaviour.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | const mercurius = require('..') 5 | 6 | const app = Fastify() 7 | 8 | const schema = ` 9 | type Query { 10 | add(x: Int, y: Int): Int 11 | } 12 | ` 13 | 14 | const resolvers = { 15 | Query: { 16 | add: async (_, obj) => { 17 | const { x, y } = obj 18 | return x + y 19 | } 20 | } 21 | } 22 | 23 | /** 24 | * Define error formatter so we always return 200 OK 25 | */ 26 | function errorFormatter (err, ctx) { 27 | const response = mercurius.defaultErrorFormatter(err, ctx) 28 | response.statusCode = 200 29 | return response 30 | } 31 | 32 | app.register(mercurius, { 33 | schema, 34 | resolvers, 35 | errorFormatter 36 | }) 37 | 38 | app.listen({ port: 3000 }) 39 | -------------------------------------------------------------------------------- /examples/disable-introspection.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | const mercurius = require('..') 5 | const graphql = require('graphql') 6 | 7 | const app = Fastify() 8 | 9 | const schema = ` 10 | type Query { 11 | add(x: Int, y: Int): Int 12 | } 13 | ` 14 | 15 | const resolvers = { 16 | Query: { 17 | add: async (_, obj) => { 18 | const { x, y } = obj 19 | return x + y 20 | } 21 | } 22 | } 23 | 24 | app.register(mercurius, { 25 | schema, 26 | resolvers, 27 | graphiql: true, 28 | validationRules: [graphql.NoSchemaIntrospectionCustomRule] 29 | }) 30 | 31 | app.get('/', async function (req, reply) { 32 | const query = '{ add(x: 2, y: 2) }' 33 | return reply.graphql(query) 34 | }) 35 | 36 | app.listen({ port: 3000 }) 37 | -------------------------------------------------------------------------------- /examples/executable-schema.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | const mercurius = require('mercurius') 5 | const { makeExecutableSchema } = require('@graphql-tools/schema') 6 | 7 | const app = Fastify() 8 | 9 | const typeDefs = ` 10 | type Query { 11 | add(x: Int, y: Int): Int 12 | } 13 | ` 14 | 15 | const resolvers = { 16 | Query: { 17 | add: async (_, { x, y }) => x + y 18 | } 19 | } 20 | 21 | app.register(mercurius, { 22 | schema: makeExecutableSchema({ typeDefs, resolvers }) 23 | }) 24 | 25 | app.get('/', async function (req, reply) { 26 | const query = '{ add(x: 2, y: 2) }' 27 | return reply.graphql(query) 28 | }) 29 | 30 | app.listen({ port: 3000 }) 31 | -------------------------------------------------------------------------------- /examples/full-ws-transport.js: -------------------------------------------------------------------------------- 1 | import Fastify from 'fastify' 2 | import mercurius from 'mercurius' 3 | 4 | const app = Fastify({ logger: true }) 5 | 6 | let x = 0 7 | 8 | const schema = ` 9 | type ResultChange { 10 | operation: String 11 | prev: Int 12 | current: Int 13 | } 14 | type Query { 15 | result: Int 16 | } 17 | type Mutation { 18 | add(num: Int): Int 19 | subtract(num: Int): Int 20 | } 21 | type Subscription { 22 | onResultChange: ResultChange 23 | } 24 | ` 25 | 26 | const resolvers = { 27 | Query: { 28 | result: async () => { 29 | return x 30 | } 31 | }, 32 | Mutation: { 33 | add: async (_, args, { pubsub }) => { 34 | const prev = x 35 | const { num } = args 36 | 37 | x = prev + num 38 | 39 | pubsub.publish({ 40 | topic: 'RESULT_TOPIC', 41 | payload: { 42 | onResultChange: { 43 | operation: 'add', 44 | prev, 45 | current: x 46 | } 47 | } 48 | }) 49 | 50 | return x 51 | }, 52 | subtract: async (_, args, { pubsub }) => { 53 | const prev = x 54 | const { num } = args 55 | 56 | x = prev - num 57 | 58 | pubsub.publish({ 59 | topic: 'RESULT_TOPIC', 60 | payload: { 61 | onResultChange: { 62 | operation: 'subtract', 63 | prev, 64 | current: x 65 | } 66 | } 67 | }) 68 | 69 | return x 70 | } 71 | }, 72 | Subscription: { 73 | onResultChange: { 74 | subscribe: async (_, __, { pubsub }) => { 75 | return await pubsub.subscribe('RESULT_TOPIC') 76 | } 77 | } 78 | } 79 | } 80 | 81 | app.register(mercurius, { 82 | schema, 83 | resolvers, 84 | graphiql: true, 85 | subscription: { 86 | fullWsTransport: true 87 | } 88 | }) 89 | 90 | app.listen({ port: 4000 }) 91 | -------------------------------------------------------------------------------- /examples/gateway-subscription.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const Fastify = require('fastify') 3 | const mercuriusWithFederation = require('@mercuriusjs/federation') 4 | const mercuriusWithGateway = require('@mercuriusjs/gateway') 5 | 6 | async function createService (port, schema, resolvers = {}) { 7 | const service = Fastify() 8 | 9 | service.register(mercuriusWithFederation, { 10 | schema, 11 | resolvers, 12 | ide: true, 13 | routes: true, 14 | jit: 1, 15 | subscription: true 16 | }) 17 | await service.listen(port) 18 | } 19 | 20 | const users = { 21 | u1: { 22 | id: 'u1', 23 | name: 'John' 24 | }, 25 | u2: { 26 | id: 'u2', 27 | name: 'Jane' 28 | }, 29 | u3: { 30 | id: 'u3', 31 | name: 'Jack' 32 | } 33 | } 34 | 35 | const posts = { 36 | p1: { 37 | pid: 'p1', 38 | title: 'Post 1', 39 | content: 'Content 1', 40 | authorId: 'u1' 41 | }, 42 | p2: { 43 | pid: 'p2', 44 | title: 'Post 2', 45 | content: 'Content 2', 46 | authorId: 'u2' 47 | }, 48 | p3: { 49 | pid: 'p3', 50 | title: 'Post 3', 51 | content: 'Content 3', 52 | authorId: 'u1' 53 | }, 54 | p4: { 55 | pid: 'p4', 56 | title: 'Post 4', 57 | content: 'Content 4', 58 | authorId: 'u2' 59 | } 60 | } 61 | 62 | const comments = {} 63 | 64 | async function start () { 65 | await createService(4001, ` 66 | extend type Query { 67 | me: User 68 | } 69 | 70 | type User @key(fields: "id") { 71 | id: ID! 72 | name: String! 73 | fullName: String 74 | avatar(size: AvatarSize): String 75 | friends: [User] 76 | } 77 | 78 | enum AvatarSize { 79 | small 80 | medium 81 | large 82 | } 83 | `, { 84 | Query: { 85 | me: (root, args, context, info) => { 86 | return users.u1 87 | } 88 | }, 89 | User: { 90 | __resolveReference: (user, args, context, info) => { 91 | return users[user.id] 92 | }, 93 | avatar: (user, { size }) => `avatar-${size}.jpg`, 94 | friends: (user) => Object.values(users).filter(u => u.id !== user.id), 95 | fullName: (user) => user.name + ' Doe' 96 | } 97 | }) 98 | 99 | await createService(4002, ` 100 | type Post @key(fields: "pid") { 101 | pid: ID! 102 | title: String 103 | content: String 104 | author: User @requires(fields: "pid title") 105 | } 106 | 107 | extend type Query { 108 | topPosts(count: Int): [Post] 109 | } 110 | 111 | extend type User @key(fields: "id") { 112 | id: ID! @external 113 | name: String @external 114 | posts: [Post] 115 | numberOfPosts: Int @requires(fields: "id name") 116 | } 117 | 118 | extend type Mutation { 119 | createPost(post: PostInput!): Post 120 | } 121 | 122 | input PostInput { 123 | title: String! 124 | content: String! 125 | authorId: String! 126 | } 127 | `, { 128 | Post: { 129 | __resolveReference: (post, args, context, info) => { 130 | return posts[post.pid] 131 | }, 132 | author: (post, args, context, info) => { 133 | return { 134 | __typename: 'User', 135 | id: post.authorId 136 | } 137 | } 138 | }, 139 | User: { 140 | posts: (user, args, context, info) => { 141 | return Object.values(posts).filter(p => p.authorId === user.id) 142 | }, 143 | numberOfPosts: (user) => { 144 | return Object.values(posts).filter(p => p.authorId === user.id).length 145 | } 146 | }, 147 | Query: { 148 | topPosts: (root, { count = 2 }) => Object.values(posts).slice(0, count) 149 | }, 150 | Mutation: { 151 | createPost: (root, { post }) => { 152 | const pid = `p${Object.values(posts).length + 1}` 153 | 154 | const result = { 155 | pid, 156 | ...post 157 | } 158 | posts[pid] = result 159 | 160 | return result 161 | } 162 | } 163 | }) 164 | 165 | await createService(4003, ` 166 | type Comment @key(fields: "cid") { 167 | cid: ID! 168 | text: String! 169 | author: User 170 | post: Post 171 | } 172 | 173 | extend type User @key(fields: "id") { 174 | id: ID! @external 175 | comments: [Comment] 176 | } 177 | 178 | extend type Post @key(fields: "pid") { 179 | pid: ID! @external 180 | comments: [Comment] 181 | } 182 | 183 | input CommentInput { 184 | text: String! 185 | authorId: ID! 186 | postId: ID! 187 | } 188 | 189 | extend type Mutation { 190 | addComment(comment: CommentInput): Comment 191 | } 192 | 193 | extend type Subscription { 194 | commentAdded(postId: ID!): Comment 195 | } 196 | `, { 197 | Comment: { 198 | __resolveReference: (comment) => { 199 | return comments[comment.id] 200 | }, 201 | author: (comment) => { 202 | return { 203 | __typename: 'User', 204 | id: comment.authorId 205 | } 206 | }, 207 | post: (comment) => { 208 | return { 209 | __typename: 'Post', 210 | pid: comment.postId 211 | } 212 | } 213 | }, 214 | Post: { 215 | comments: (post) => { 216 | return Object.values(comments).filter(c => post.pid === c.postId) 217 | } 218 | }, 219 | User: { 220 | comments: (user) => { 221 | return Object.values(comments).filter(c => user.id === c.authorId) 222 | } 223 | }, 224 | Mutation: { 225 | async addComment (parent, { comment }, { pubsub }) { 226 | const cid = `c${Object.values(comments).length + 1}` 227 | 228 | const result = { 229 | cid, 230 | ...comment 231 | } 232 | comments[cid] = result 233 | 234 | await pubsub.publish({ 235 | topic: `COMMENT_ADDED_${comment.postId}`, 236 | payload: { 237 | commentAdded: result 238 | } 239 | }) 240 | return result 241 | } 242 | }, 243 | Subscription: { 244 | commentAdded: { 245 | subscribe: async (root, { postId }, { pubsub }) => { 246 | const subscription = await pubsub.subscribe(`COMMENT_ADDED_${postId}`) 247 | 248 | return subscription 249 | } 250 | } 251 | } 252 | }) 253 | 254 | const gateway = Fastify() 255 | gateway.register(mercuriusWithGateway, { 256 | routes: true, 257 | ide: true, 258 | subscription: true, 259 | jit: 1, 260 | gateway: { 261 | services: [{ 262 | name: 'user', 263 | url: 'http://localhost:4001/graphql' 264 | }, { 265 | name: 'post', 266 | url: 'http://localhost:4002/graphql' 267 | }, { 268 | name: 'comment', 269 | url: 'http://localhost:4003/graphql', 270 | wsUrl: 'ws://localhost:4003/graphql', 271 | wsConnectionParams: { 272 | // OPTIONAL: uncomment this line if you are using the `subscriptions-transport-ws` library 273 | // protocols: ['graphql-ws'] 274 | }, 275 | keepAlive: 3000 276 | }] 277 | } 278 | }) 279 | 280 | await gateway.listen({ port: 4000 }) 281 | } 282 | 283 | start() 284 | -------------------------------------------------------------------------------- /examples/gateway.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const Fastify = require('fastify') 3 | const mercuriusWithFederation = require('@mercuriusjs/federation') 4 | const mercuriusWithGateway = require('@mercuriusjs/gateway') 5 | const mercurius = require('..') 6 | const { ErrorWithProps } = mercurius 7 | 8 | async function createService (port, schema, resolvers = {}) { 9 | const service = Fastify() 10 | 11 | service.register(mercuriusWithFederation, { 12 | schema, 13 | resolvers, 14 | graphiql: true, 15 | jit: 1 16 | }) 17 | await service.listen(port) 18 | } 19 | 20 | const users = { 21 | u1: { 22 | id: 'u1', 23 | name: 'John' 24 | }, 25 | u2: { 26 | id: 'u2', 27 | name: 'Jane' 28 | }, 29 | u3: { 30 | id: 'u3', 31 | name: 'Jack' 32 | } 33 | } 34 | 35 | const posts = { 36 | p1: { 37 | pid: 'p1', 38 | title: 'Post 1', 39 | content: 'Content 1', 40 | authorId: 'u1' 41 | }, 42 | p2: { 43 | pid: 'p2', 44 | title: 'Post 2', 45 | content: 'Content 2', 46 | authorId: 'u2' 47 | }, 48 | p3: { 49 | pid: 'p3', 50 | title: 'Post 3', 51 | content: 'Content 3', 52 | authorId: 'u1' 53 | }, 54 | p4: { 55 | pid: 'p4', 56 | title: 'Post 4', 57 | content: 'Content 4', 58 | authorId: 'u2' 59 | } 60 | } 61 | 62 | async function start () { 63 | await createService(4001, ` 64 | extend type Query { 65 | me: User 66 | you: User 67 | hello: String 68 | } 69 | 70 | type User @key(fields: "id") { 71 | id: ID! 72 | name: String! 73 | fullName: String 74 | avatar(size: AvatarSize): String 75 | friends: [User] 76 | } 77 | 78 | enum AvatarSize { 79 | small 80 | medium 81 | large 82 | } 83 | `, { 84 | Query: { 85 | me: (root, args, context, info) => { 86 | return users.u1 87 | }, 88 | you: (root, args, context, info) => { 89 | throw new ErrorWithProps('Can\'t fetch other users data', { code: 'NOT_ALLOWED' }) 90 | }, 91 | hello: () => 'world' 92 | }, 93 | User: { 94 | __resolveReference: (user, args, context, info) => { 95 | return users[user.id] 96 | }, 97 | avatar: (user, { size }) => `avatar-${size}.jpg`, 98 | friends: (user) => Object.values(users).filter(u => u.id !== user.id), 99 | fullName: (user) => user.name + ' Doe' 100 | } 101 | }) 102 | 103 | await createService(4002, ` 104 | type Post @key(fields: "pid") { 105 | pid: ID! 106 | title: String 107 | content: String 108 | author: User @requires(fields: "pid title") 109 | } 110 | 111 | type Query @extends { 112 | topPosts(count: Int): [Post] 113 | } 114 | 115 | type User @key(fields: "id") @extends { 116 | id: ID! @external 117 | name: String @external 118 | posts: [Post] 119 | numberOfPosts: Int @requires(fields: "id name") 120 | } 121 | 122 | extend type Mutation { 123 | createPost(post: PostInput!): Post 124 | updateHello: String 125 | } 126 | 127 | input PostInput { 128 | title: String! 129 | content: String! 130 | authorId: String! 131 | } 132 | `, { 133 | Post: { 134 | __resolveReference: (post, args, context, info) => { 135 | return posts[post.pid] 136 | }, 137 | author: (post, args, context, info) => { 138 | return { 139 | __typename: 'User', 140 | id: post.authorId 141 | } 142 | } 143 | }, 144 | User: { 145 | posts: (user, args, context, info) => { 146 | return Object.values(posts).filter(p => p.authorId === user.id) 147 | }, 148 | numberOfPosts: (user) => { 149 | return Object.values(posts).filter(p => p.authorId === user.id).length 150 | } 151 | }, 152 | Query: { 153 | topPosts: (root, { count = 2 }) => Object.values(posts).slice(0, count) 154 | }, 155 | Mutation: { 156 | createPost: (root, { post }) => { 157 | const pid = `p${Object.values(posts).length + 1}` 158 | 159 | const result = { 160 | pid, 161 | ...post 162 | } 163 | posts[pid] = result 164 | 165 | return result 166 | }, 167 | updateHello: () => 'World' 168 | } 169 | }) 170 | 171 | const gateway = Fastify() 172 | gateway.register(mercuriusWithGateway, { 173 | graphiql: true, 174 | jit: 1, 175 | gateway: { 176 | services: [{ 177 | name: 'user', 178 | url: 'http://localhost:4001/graphql', 179 | setResponseHeaders: (reply) => { 180 | reply.header('abc', 'abc') 181 | } 182 | }, { 183 | name: 'post', 184 | url: 'http://localhost:4002/graphql' 185 | }] 186 | } 187 | }) 188 | 189 | await gateway.listen({ port: 4000 }) 190 | } 191 | 192 | start() 193 | -------------------------------------------------------------------------------- /examples/graphiql-plugin/README.md: -------------------------------------------------------------------------------- 1 | # GraphiQL custom plugin 2 | 3 | ## Quick start 4 | 5 | Execute the local `index.js` app to try the GraphiQL plugin integration. 6 | 7 | ```javascript 8 | // examples/graphiql-plugin 9 | node ./index.js 10 | ``` 11 | 12 | ## Create the plugin 13 | 14 | You can easily create a GraphiQL plugin and integrate it in Mercurius GraphiQL instance. 15 | 16 | * [GraphiQL.](https://github.com/graphql/graphiql) 17 | * [GraphiQL Explorer Plugin example.](https://github.com/graphql/graphiql/tree/main/packages/graphiql-plugin-explorer) 18 | 19 | ### Plugin component 20 | 21 | A GraphiQL plugin is an object that contains these properties: 22 | 23 | * `title`: string. The title of the plugin 24 | * `icon`: React component. The icon shown in the toolbar 25 | * `content`: React component with the plugin implementation 26 | 27 | It can be created using the sample: 28 | 29 | ```javascript 30 | import React from 'react' 31 | 32 | function Content() { 33 | return ( 34 |
35 |
This is a sample plugin
36 |
37 | ) 38 | } 39 | 40 | function Icon() { 41 | return

P

42 | } 43 | 44 | /* 45 | * Enrich, extract or modify the data returned by the GraphiQL fetcher. 46 | * 47 | * Q: Why do I need to intercept the data? 48 | * A: GraphiQL do not provide a direct access to the fetched data. 49 | * The data are fetched and injected directly in the viewer in a stringified format. 50 | * 51 | * To provide a way to access the fetched data a similar function can implemented and passed 52 | * to the plugin in the attribute `fetcherWrapper` of the configuration. 53 | * 54 | * { 55 | * name: '...', 56 | * props: ..., 57 | * umdUrl: '...', 58 | * fetcherWrapper: 'parseFetchResponse' 59 | * } 60 | */ 61 | export function parseFetchResponse(data) { 62 | if (data) { 63 | // Eg. storeDataSomewhere(data) 64 | // Eg. addInfoToData(data) 65 | // Eg. removeAttributeFromData(data) 66 | } 67 | return data 68 | } 69 | 70 | export function graphiqlSamplePlugin(props) { 71 | return { 72 | title: props.title || 'GraphiQL Sample', 73 | icon: () => , 74 | content: () => { 75 | return 76 | } 77 | } 78 | } 79 | 80 | // This is required for the Mercurius integration 81 | export function umdPlugin(props) { 82 | return graphiqlSamplePlugin(props) 83 | } 84 | ``` 85 | 86 | ### Export as Umd 87 | 88 | ```javascript 89 | import resolve from '@rollup/plugin-node-resolve' 90 | import commonjs from '@rollup/plugin-commonjs' 91 | import jsx from 'rollup-plugin-jsx' 92 | 93 | const packageJson = require('./package.json') 94 | 95 | const rollup = [ 96 | { 97 | input: 'src/export.js', // path to the plugin entry point 98 | output: [ 99 | { 100 | file: packageJson.main, 101 | format: 'cjs', 102 | sourcemap: true 103 | }, 104 | { 105 | file: packageJson.module, 106 | format: 'esm', 107 | sourcemap: true 108 | }, 109 | { 110 | file: packageJson.umd, 111 | format: 'umd', 112 | sourcemap: true, 113 | name: 'mercuriusPluginSample' 114 | } 115 | ], 116 | external: ['react', '@graphiql/toolkit'], 117 | plugins: [ 118 | resolve({ 119 | extensions: ['.js', '.jsx'] 120 | }), 121 | svgr(), 122 | commonjs(), 123 | jsx({ factory: 'React.createElement' }) 124 | ] 125 | } 126 | ] 127 | 128 | export default rollup 129 | ``` 130 | 131 | Check the [plugin-sources](./plugin-sources) folder for a complete example. 132 | 133 | ### Serve the plugin 134 | 135 | To work with `Mercurius` the plugin should be available using a GET request. 136 | 137 | The preferred approach is to deploy the package on a public CDN like [unpkg.com](https://unpkg.com/). 138 | 139 | Alternatively it can be served by the local fastify. 140 | 141 | ```javascript 142 | app.get('/graphiql/samplePlugin.js', (req, reply) => { 143 | reply.sendFile('samplePlugin.js') 144 | }) 145 | ``` 146 | 147 | ## Add the plugin to Mercurius 148 | 149 | In the configuration file add the plugin in the `graphiql` parameter 150 | 151 | ```javascript 152 | app.register(mercurius, { 153 | schema, 154 | resolvers, 155 | graphiql: { 156 | enabled: true, 157 | plugins: [ 158 | { 159 | name: 'mercuriusPluginSample', 160 | props: {title: 'Sample plugin'}, 161 | umdUrl: 'http://localhost:3000/graphiql/samplePlugin.js', 162 | fetcherWrapper: 'parseFetchResponse' 163 | } 164 | ] 165 | } 166 | }) 167 | ``` 168 | 169 | * `name`, string. The same used in the `rollup` export `output(format === umd).name` 170 | * `props`, object. The props to be passed to the plugin 171 | * `umdUrl`, string. The url of the static `umd` file 172 | * `fetcherWrapper`, function. The name of an exported function that intercept the result from the fetch. 173 | -------------------------------------------------------------------------------- /examples/graphiql-plugin/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | const mercurius = require('../..') 5 | const Static = require('@fastify/static') 6 | const { join } = require('path') 7 | 8 | const app = Fastify() 9 | 10 | const schema = ` 11 | type Query { 12 | add(x: Int, y: Int): Int 13 | } 14 | ` 15 | 16 | const resolvers = { 17 | Query: { 18 | add: async (_, obj) => { 19 | const { x, y } = obj 20 | return x + y 21 | } 22 | } 23 | } 24 | 25 | app.register(mercurius, { 26 | schema, 27 | resolvers, 28 | graphiql: { 29 | plugins: [ 30 | { 31 | name: 'samplePlugin', 32 | props: {}, 33 | umdUrl: 'http://localhost:3000/graphiql/samplePlugin.js', 34 | fetcherWrapper: 'parseFetchResponse' 35 | } 36 | ] 37 | } 38 | }) 39 | 40 | app.register(Static, { 41 | root: join(__dirname, './plugin'), 42 | wildcard: false, 43 | serve: false 44 | }) 45 | 46 | app.get('/', async function (req, reply) { 47 | const query = '{ add(x: 2, y: 2) }' 48 | return reply.graphql(query) 49 | }) 50 | 51 | app.get('/graphiql/samplePlugin.js', (req, reply) => { 52 | reply.sendFile('samplePlugin.js') 53 | }) 54 | 55 | app.listen({ port: 3000 }) 56 | -------------------------------------------------------------------------------- /examples/graphiql-plugin/plugin-sources/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /dist 6 | -------------------------------------------------------------------------------- /examples/graphiql-plugin/plugin-sources/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphiql-sample-plugin", 3 | "version": "1.0.0", 4 | "description": "This is a sample plugin for graphiql.", 5 | "dependencies": {}, 6 | "scripts": { 7 | "build": "rollup -c" 8 | }, 9 | "devDependencies": { 10 | "@rollup/plugin-commonjs": "^23.0.0", 11 | "@rollup/plugin-node-resolve": "^15.0.0", 12 | "rollup": "^2.79.1", 13 | "rollup-plugin-jsx": "^1.0.3" 14 | }, 15 | "main": "dist/cjs/index.js", 16 | "module": "dist/esm/index.js", 17 | "umd": "dist/umd/index.js", 18 | "files": [ 19 | "dist" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /examples/graphiql-plugin/plugin-sources/rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve' 2 | import commonjs from '@rollup/plugin-commonjs' 3 | import jsx from 'rollup-plugin-jsx' 4 | 5 | const packageJson = require('./package.json') 6 | 7 | const rollup = [ 8 | { 9 | input: 'src/index.js', 10 | output: [ 11 | { 12 | file: packageJson.main, 13 | format: 'cjs', 14 | sourcemap: true 15 | }, 16 | { 17 | file: packageJson.module, 18 | format: 'esm', 19 | sourcemap: true 20 | }, 21 | { 22 | file: packageJson.umd, 23 | format: 'umd', 24 | sourcemap: true, 25 | name: 'samplePlugin' 26 | } 27 | ], 28 | external: ['react', '@graphiql/toolkit'], 29 | plugins: [ 30 | resolve({ 31 | extensions: ['.js', '.jsx'] 32 | }), 33 | commonjs(), 34 | jsx({ factory: 'React.createElement' }) 35 | ] 36 | } 37 | // This is for typescript in the future 38 | // { 39 | // input: 'dist/esm/types/index.d.ts', 40 | // output: [{ file: 'dist/index.d.ts', format: 'esm' }], 41 | // plugins: [dts()] 42 | // } 43 | ] 44 | 45 | export default rollup 46 | -------------------------------------------------------------------------------- /examples/graphiql-plugin/plugin-sources/src/index.js: -------------------------------------------------------------------------------- 1 | export { graphiqlSamplePlugin, umdPlugin } from './plugin' 2 | export { parseFetchResponse } from './utils' 3 | -------------------------------------------------------------------------------- /examples/graphiql-plugin/plugin-sources/src/plugin.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import useSampleData from './useSampleData' 3 | 4 | export function Content () { 5 | const { sampleData } = useSampleData() 6 | 7 | return ( 8 |
9 |
This is a sample plugin
10 | {sampleData &&
{JSON.stringify(sampleData, null, 2)}
} 11 |
12 | ) 13 | } 14 | 15 | export function Icon () { 16 | return

GE

17 | } 18 | 19 | export function graphiqlSamplePlugin (props) { 20 | return { 21 | title: props.title || 'GraphiQL Sample', 22 | icon: () => , 23 | content: () => { 24 | return 25 | } 26 | } 27 | } 28 | 29 | export function umdPlugin (props) { 30 | return graphiqlSamplePlugin(props) 31 | } 32 | -------------------------------------------------------------------------------- /examples/graphiql-plugin/plugin-sources/src/sampleDataManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A data manager to collect the data. 3 | */ 4 | class SampleDataManager extends EventTarget { 5 | constructor () { 6 | super() 7 | this.sampleData = [] 8 | } 9 | 10 | getSampleData () { 11 | return this.sampleData 12 | } 13 | 14 | setSampleData (sampleData) { 15 | this.sampleData = sampleData || [] 16 | this.dispatchEvent(new Event('updateSampleData')) 17 | } 18 | } 19 | 20 | const sampleDataManager = new SampleDataManager() 21 | 22 | export default sampleDataManager 23 | -------------------------------------------------------------------------------- /examples/graphiql-plugin/plugin-sources/src/useSampleData.js: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from 'react' 2 | import sampleDataManager from './sampleDataManager' 3 | 4 | const useSampleData = () => { 5 | const [sampleData, setSampleData] = useState( 6 | sampleDataManager.getSampleData() 7 | ) 8 | 9 | useEffect(() => { 10 | const eventListener = sampleDataManager.addEventListener( 11 | 'updateSampleData', 12 | (e, value) => { 13 | setSampleData(_ => e.target.sampleData || []) 14 | } 15 | ) 16 | return () => { 17 | sampleDataManager.removeEventListener('updateSampleData', eventListener) 18 | } 19 | }, []) 20 | 21 | return { 22 | sampleData 23 | } 24 | } 25 | 26 | export default useSampleData 27 | -------------------------------------------------------------------------------- /examples/graphiql-plugin/plugin-sources/src/utils.js: -------------------------------------------------------------------------------- 1 | import sampleDataManager from './sampleDataManager' 2 | 3 | /** 4 | * Intercept and store the data fetched by GQL in the DataManager. 5 | */ 6 | export function parseFetchResponse (data) { 7 | if (data.data) { 8 | sampleDataManager.setSampleData(data.data) 9 | } 10 | return data 11 | } 12 | -------------------------------------------------------------------------------- /examples/graphiql-plugin/plugin/samplePlugin.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | (function (global, factory) { 3 | typeof exports === 'object' && typeof module !== 'undefined' 4 | ? factory(exports, require('react')) 5 | : typeof define === 'function' && define.amd 6 | ? define(['exports', 'react'], factory) 7 | : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.samplePlugin = {}, global.React)) 8 | })(this, function (exports, React) { 9 | 'use strict' 10 | 11 | function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e } } 12 | 13 | const React__default = /* #__PURE__ */_interopDefaultLegacy(React) 14 | 15 | /** 16 | * A data manager to collect the data. 17 | */ 18 | class SampleDataManager extends EventTarget { 19 | constructor () { 20 | super() 21 | this.sampleData = [] 22 | } 23 | 24 | getSampleData () { 25 | return this.sampleData 26 | } 27 | 28 | setSampleData (sampleData) { 29 | this.sampleData = sampleData || [] 30 | this.dispatchEvent(new Event('updateSampleData')) 31 | } 32 | } 33 | 34 | const sampleDataManager = new SampleDataManager() 35 | 36 | const useSampleData = () => { 37 | const [sampleData, setSampleData] = React.useState( 38 | sampleDataManager.getSampleData() 39 | ) 40 | 41 | React.useEffect(() => { 42 | const eventListener = sampleDataManager.addEventListener( 43 | 'updateSampleData', 44 | (e, value) => { 45 | setSampleData(_ => e.target.sampleData || []) 46 | } 47 | ) 48 | return () => { 49 | sampleDataManager.removeEventListener('updateSampleData', eventListener) 50 | } 51 | }, []) 52 | 53 | return { 54 | sampleData 55 | } 56 | } 57 | 58 | function Content () { 59 | const { sampleData } = useSampleData() 60 | 61 | return ( 62 | React__default.default.createElement('div', { style: { maxWidth: '300px' } }, [ 63 | React__default.default.createElement('div', { style: { height: '100%' } }, ['This is a sample plugin']), 64 | sampleData && React__default.default.createElement('pre', null, [JSON.stringify(sampleData, null, 2)]) 65 | ]) 66 | ) 67 | } 68 | 69 | function Icon () { 70 | return React__default.default.createElement('p', null, ['GE']) 71 | } 72 | 73 | function graphiqlSamplePlugin (props) { 74 | return { 75 | title: props.title || 'GraphiQL Sample', 76 | icon: () => Icon(), 77 | content: () => { 78 | return Content() 79 | } 80 | } 81 | } 82 | 83 | function umdPlugin (props) { 84 | return graphiqlSamplePlugin(props) 85 | } 86 | 87 | /** 88 | * Intercept and store the data fetched by GQL in the DataManager. 89 | */ 90 | function parseFetchResponse (data) { 91 | if (data.data) { 92 | sampleDataManager.setSampleData(data.data) 93 | } 94 | return data 95 | } 96 | 97 | exports.graphiqlSamplePlugin = graphiqlSamplePlugin 98 | exports.parseFetchResponse = parseFetchResponse 99 | exports.umdPlugin = umdPlugin 100 | 101 | Object.defineProperty(exports, '__esModule', { value: true }) 102 | }) 103 | -------------------------------------------------------------------------------- /examples/hooks-gateway.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const Fastify = require('fastify') 3 | const mercuriusWithFederation = require('@mercuriusjs/federation') 4 | const mercuriusWithGateway = require('@mercuriusjs/gateway') 5 | const mercurius = require('..') 6 | const { ErrorWithProps } = mercurius 7 | 8 | async function createService (port, schema, resolvers = {}) { 9 | const service = Fastify() 10 | 11 | service.register(mercuriusWithFederation, { 12 | schema, 13 | resolvers, 14 | graphiql: true, 15 | jit: 1 16 | }) 17 | await service.listen(port) 18 | } 19 | 20 | const users = { 21 | u1: { 22 | id: 'u1', 23 | name: 'John' 24 | }, 25 | u2: { 26 | id: 'u2', 27 | name: 'Jane' 28 | }, 29 | u3: { 30 | id: 'u3', 31 | name: 'Jack' 32 | } 33 | } 34 | 35 | const posts = { 36 | p1: { 37 | pid: 'p1', 38 | title: 'Post 1', 39 | content: 'Content 1', 40 | authorId: 'u1' 41 | }, 42 | p2: { 43 | pid: 'p2', 44 | title: 'Post 2', 45 | content: 'Content 2', 46 | authorId: 'u2' 47 | }, 48 | p3: { 49 | pid: 'p3', 50 | title: 'Post 3', 51 | content: 'Content 3', 52 | authorId: 'u1' 53 | }, 54 | p4: { 55 | pid: 'p4', 56 | title: 'Post 4', 57 | content: 'Content 4', 58 | authorId: 'u2' 59 | } 60 | } 61 | 62 | async function start () { 63 | await createService(4001, ` 64 | extend type Query { 65 | me: User 66 | you: User 67 | hello: String 68 | } 69 | 70 | type User @key(fields: "id") { 71 | id: ID! 72 | name: String! 73 | fullName: String 74 | avatar(size: AvatarSize): String 75 | friends: [User] 76 | } 77 | 78 | enum AvatarSize { 79 | small 80 | medium 81 | large 82 | } 83 | `, { 84 | Query: { 85 | me: (root, args, context, info) => { 86 | return users.u1 87 | }, 88 | you: (root, args, context, info) => { 89 | throw new ErrorWithProps('Can\'t fetch other users data', { code: 'NOT_ALLOWED' }) 90 | }, 91 | hello: () => 'world' 92 | }, 93 | User: { 94 | __resolveReference: (user, args, context, info) => { 95 | return users[user.id] 96 | }, 97 | avatar: (user, { size }) => `avatar-${size}.jpg`, 98 | friends: (user) => Object.values(users).filter(u => u.id !== user.id), 99 | fullName: (user) => user.name + ' Doe' 100 | } 101 | }) 102 | 103 | await createService(4002, ` 104 | type Post @key(fields: "pid") { 105 | pid: ID! 106 | title: String 107 | content: String 108 | author: User @requires(fields: "pid title") 109 | } 110 | 111 | type Query @extends { 112 | topPosts(count: Int): [Post] 113 | } 114 | 115 | type User @key(fields: "id") @extends { 116 | id: ID! @external 117 | name: String @external 118 | posts: [Post] 119 | numberOfPosts: Int @requires(fields: "id name") 120 | } 121 | 122 | extend type Mutation { 123 | createPost(post: PostInput!): Post 124 | updateHello: String 125 | } 126 | 127 | input PostInput { 128 | title: String! 129 | content: String! 130 | authorId: String! 131 | } 132 | `, { 133 | Post: { 134 | __resolveReference: (post, args, context, info) => { 135 | return posts[post.pid] 136 | }, 137 | author: (post, args, context, info) => { 138 | return { 139 | __typename: 'User', 140 | id: post.authorId 141 | } 142 | } 143 | }, 144 | User: { 145 | posts: (user, args, context, info) => { 146 | return Object.values(posts).filter(p => p.authorId === user.id) 147 | }, 148 | numberOfPosts: (user) => { 149 | return Object.values(posts).filter(p => p.authorId === user.id).length 150 | } 151 | }, 152 | Query: { 153 | topPosts: (root, { count = 2 }) => Object.values(posts).slice(0, count) 154 | }, 155 | Mutation: { 156 | createPost: (root, { post }) => { 157 | const pid = `p${Object.values(posts).length + 1}` 158 | 159 | const result = { 160 | pid, 161 | ...post 162 | } 163 | posts[pid] = result 164 | 165 | return result 166 | }, 167 | updateHello: () => 'World' 168 | } 169 | }) 170 | 171 | const gateway = Fastify() 172 | gateway.register(mercuriusWithGateway, { 173 | graphiql: true, 174 | jit: 1, 175 | gateway: { 176 | services: [{ 177 | name: 'user', 178 | url: 'http://localhost:4001/graphql' 179 | }, { 180 | name: 'post', 181 | url: 'http://localhost:4002/graphql' 182 | }] 183 | } 184 | }) 185 | 186 | await gateway.ready() 187 | 188 | gateway.graphql.addHook('preParsing', async function (schema, source, context) { 189 | console.log('preParsing called') 190 | }) 191 | 192 | gateway.graphql.addHook('preValidation', async function (schema, document, context) { 193 | console.log('preValidation called') 194 | }) 195 | 196 | gateway.graphql.addHook('preExecution', async function (schema, document, context) { 197 | console.log('preExecution called') 198 | return { 199 | document, 200 | errors: [ 201 | new Error('foo') 202 | ] 203 | } 204 | }) 205 | 206 | gateway.graphql.addHook('preGatewayExecution', async function (schema, document, context, service) { 207 | console.log('preGatewayExecution called', service.name) 208 | return { 209 | document, 210 | errors: [ 211 | new Error('foo') 212 | ] 213 | } 214 | }) 215 | 216 | gateway.graphql.addHook('onResolution', async function (execution, context) { 217 | console.log('onResolution called') 218 | }) 219 | 220 | gateway.graphql.addHook('onGatewayReplaceSchema', async (instance, schema) => { 221 | console.log('onGatewayReplaceSchema called') 222 | }) 223 | 224 | await gateway.listen({ port: 4000 }) 225 | } 226 | 227 | start() 228 | -------------------------------------------------------------------------------- /examples/hooks-subscription.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | const mq = require('mqemitter') 5 | const mercurius = require('..') 6 | 7 | async function start () { 8 | const app = Fastify() 9 | 10 | const schema = ` 11 | type Notification { 12 | id: ID! 13 | message: String 14 | } 15 | 16 | type Mutation { 17 | addNotification(message: String): Notification 18 | } 19 | 20 | type Subscription { 21 | notificationAdded: Notification 22 | } 23 | ` 24 | 25 | const emitter = mq() 26 | 27 | const resolvers = { 28 | Mutation: { 29 | addNotification: async (_, { message }) => { 30 | const notification = { 31 | id: 1, 32 | message 33 | } 34 | await emitter.emit({ 35 | topic: 'NOTIFICATION_ADDED', 36 | payload: { 37 | notificationAdded: notification 38 | } 39 | }) 40 | 41 | return notification 42 | } 43 | }, 44 | Subscription: { 45 | notificationAdded: { 46 | subscribe: (root, args, { pubsub }) => pubsub.subscribe('NOTIFICATION_ADDED') 47 | } 48 | } 49 | } 50 | 51 | app.register(mercurius, { 52 | schema, 53 | resolvers, 54 | subscription: { 55 | emitter 56 | } 57 | }) 58 | 59 | await app.ready() 60 | 61 | app.graphql.addHook('preSubscriptionParsing', async function (schema, source, context) { 62 | console.log('preSubscriptionParsing called') 63 | }) 64 | 65 | app.graphql.addHook('preSubscriptionExecution', async function (schema, document, context) { 66 | console.log('preSubscriptionExecution called') 67 | }) 68 | 69 | app.graphql.addHook('onSubscriptionResolution', async function (execution, context) { 70 | console.log('onSubscriptionResolution called') 71 | }) 72 | 73 | app.graphql.addHook('onSubscriptionEnd', async function (context, id) { 74 | console.log('onSubscriptionEnd called') 75 | }) 76 | 77 | await app.listen({ port: 3000 }) 78 | } 79 | 80 | start() 81 | -------------------------------------------------------------------------------- /examples/hooks.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | const mercurius = require('..') 5 | 6 | async function start () { 7 | const app = Fastify() 8 | 9 | const schema = ` 10 | type Query { 11 | add(x: Int, y: Int): Int 12 | } 13 | ` 14 | 15 | const resolvers = { 16 | Query: { 17 | add: async (_, obj) => { 18 | const { x, y } = obj 19 | return x + y 20 | } 21 | } 22 | } 23 | 24 | app.register(mercurius, { 25 | schema, 26 | resolvers, 27 | graphiql: true 28 | }) 29 | 30 | await app.ready() 31 | 32 | app.graphql.addHook('preParsing', async function (schema, source, context) { 33 | console.log('preParsing called') 34 | }) 35 | 36 | app.graphql.addHook('preValidation', async function (schema, document, context) { 37 | console.log('preValidation called') 38 | }) 39 | 40 | app.graphql.addHook('preExecution', async function (schema, document, context) { 41 | console.log('preExecution called') 42 | return { 43 | document, 44 | errors: [ 45 | new Error('foo') 46 | ] 47 | } 48 | }) 49 | 50 | app.graphql.addHook('onResolution', async function (execution, context) { 51 | console.log('onResolution called') 52 | }) 53 | 54 | app.get('/', async function (req, reply) { 55 | const query = '{ add(x: 2, y: 2) }' 56 | 57 | return reply.graphql(query) 58 | }) 59 | 60 | app.listen({ port: 3000 }) 61 | } 62 | 63 | start() 64 | -------------------------------------------------------------------------------- /examples/loaders.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | const mercurius = require('..') 5 | 6 | const app = Fastify() 7 | 8 | const dogs = [{ 9 | name: 'Max' 10 | }, { 11 | name: 'Charlie' 12 | }, { 13 | name: 'Buddy' 14 | }, { 15 | name: 'Max' 16 | }] 17 | 18 | const owners = { 19 | Max: { 20 | name: 'Jennifer' 21 | }, 22 | Charlie: { 23 | name: 'Sarah' 24 | }, 25 | Buddy: { 26 | name: 'Tracy' 27 | } 28 | } 29 | 30 | const schema = ` 31 | type Human { 32 | name: String! 33 | } 34 | 35 | type Dog { 36 | name: String! 37 | owner: Human 38 | } 39 | 40 | type Query { 41 | dogs: [Dog] 42 | } 43 | ` 44 | 45 | const resolvers = { 46 | Query: { 47 | dogs (_, params, { reply }) { 48 | return dogs 49 | } 50 | } 51 | } 52 | 53 | const loaders = { 54 | Dog: { 55 | async owner (queries, { reply }) { 56 | return queries.map(({ obj }) => owners[obj.name]) 57 | } 58 | } 59 | } 60 | 61 | app.register(mercurius, { 62 | schema, 63 | resolvers, 64 | loaders, 65 | graphiql: true 66 | }) 67 | 68 | app.listen({ port: 3000 }) 69 | -------------------------------------------------------------------------------- /examples/persisted-queries/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | const mercurius = require('../..') 5 | const persistedQueries = require('./queries.json') 6 | 7 | const app = Fastify() 8 | 9 | const schema = ` 10 | type Query { 11 | add(x: Int, y: Int): Int 12 | } 13 | ` 14 | 15 | const resolvers = { 16 | Query: { 17 | add: async (_, obj) => { 18 | const { x, y } = obj 19 | return x + y 20 | } 21 | } 22 | } 23 | 24 | app.register(mercurius, { 25 | schema, 26 | resolvers, 27 | persistedQueries, 28 | onlyPersisted: true, // will nullify the effect of the option below (graphiql) 29 | graphiql: true 30 | }) 31 | 32 | app.listen({ port: 3000 }) 33 | -------------------------------------------------------------------------------- /examples/persisted-queries/queries.json: -------------------------------------------------------------------------------- 1 | { 2 | "248eb276edb4f22aced0a2848c539810b55f79d89abc531b91145e76838f5602": "{ add(x: 1, y: 1) }", 3 | "495ccd73abc8436544cfeedd65f24beee660d2c7be2c32536e3fbf911f935ddf": "query Add($x: Int!, $y: Int!) { add(x: $x, y: $y) }", 4 | "03ec1635d1a0ea530672bf33f28f3533239a5a7021567840c541c31d5e28c65e": "{ add(x: 3, y: 3) }" 5 | } 6 | -------------------------------------------------------------------------------- /examples/playground.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | const mercurius = require('..') 5 | 6 | const app = Fastify() 7 | 8 | const schema = ` 9 | type Query { 10 | add(x: Int, y: Int): Int 11 | } 12 | ` 13 | 14 | const resolvers = { 15 | Query: { 16 | add: async (_, obj) => { 17 | const { x, y } = obj 18 | return x + y 19 | } 20 | } 21 | } 22 | 23 | app.register(mercurius, { 24 | schema, 25 | resolvers, 26 | graphiql: true 27 | }) 28 | 29 | app.get('/', async function (req, reply) { 30 | const query = '{ add(x: 2, y: 2) }' 31 | return reply.graphql(query) 32 | }) 33 | 34 | app.listen({ port: 3000 }) 35 | -------------------------------------------------------------------------------- /examples/schema-by-http-header.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Fastify = require('fastify') 4 | const mercurius = require('..') 5 | 6 | // Define the constraint custom strategy 7 | const schemaStrategy = { 8 | name: 'schema', 9 | storage: function () { 10 | const handlers = {} 11 | return { 12 | get: (type) => { return handlers[type] || null }, 13 | set: (type, store) => { handlers[type] = store } 14 | } 15 | }, 16 | deriveConstraint: (req, ctx) => { 17 | return req.headers.schema 18 | }, 19 | validate: () => true, 20 | mustMatchWhenDerived: true 21 | } 22 | 23 | // Initialize fastify 24 | const app = Fastify({ constraints: { schema: schemaStrategy } }) 25 | 26 | // Schema 1 definition 27 | const schema = ` 28 | type Query { 29 | add(x: Int, y: Int): Int 30 | } 31 | ` 32 | 33 | const resolvers = { 34 | Query: { 35 | add: async (_, obj) => { 36 | const { x, y } = obj 37 | return x + y 38 | } 39 | } 40 | } 41 | 42 | // Schema A registration with A constraint 43 | app.register(async childServer => { 44 | childServer.register(mercurius, { 45 | schema, 46 | resolvers, 47 | graphiql: false, 48 | routes: false 49 | }) 50 | 51 | childServer.route({ 52 | path: '/', 53 | method: 'POST', 54 | constraints: { schema: 'A' }, 55 | handler: (req, reply) => { 56 | const query = req.body 57 | return reply.graphql(query) 58 | } 59 | }) 60 | }) 61 | 62 | const schema2 = ` 63 | type Query { 64 | subtract(x: Int, y: Int): Int 65 | } 66 | ` 67 | 68 | const resolvers2 = { 69 | Query: { 70 | subtract: async (_, obj) => { 71 | const { x, y } = obj 72 | return x - y 73 | } 74 | } 75 | } 76 | 77 | app.register(async childServer => { 78 | childServer.register(mercurius, { 79 | schema: schema2, 80 | resolvers: resolvers2, 81 | graphiql: false, 82 | routes: false 83 | }) 84 | 85 | childServer.route({ 86 | path: '/', 87 | method: 'POST', 88 | constraints: { schema: 'B' }, 89 | handler: (req, reply) => { 90 | const query = req.body 91 | return reply.graphql(query) 92 | } 93 | }) 94 | }) 95 | 96 | app.listen({ port: 3000 }) 97 | -------------------------------------------------------------------------------- /examples/subscription/memory.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const mercurius = require('../..') 4 | const Fastify = require('fastify') 5 | 6 | const app = Fastify({ logger: { level: 'debug' } }) 7 | 8 | // list of products 9 | const products = [] 10 | 11 | // graphql schema 12 | const schema = ` 13 | type Product { 14 | name: String! 15 | state: String! 16 | } 17 | 18 | type Query { 19 | products: [Product] 20 | } 21 | 22 | type Mutation { 23 | addProduct(name: String!, state: String!): Product 24 | } 25 | 26 | type Subscription { 27 | productAdded: Product 28 | } 29 | ` 30 | 31 | // graphql resolvers 32 | const resolvers = { 33 | Query: { 34 | products: () => products 35 | }, 36 | Mutation: { 37 | addProduct: async (_, { name, state }, { pubsub }) => { 38 | const product = { name, state } 39 | 40 | products.push(product) 41 | 42 | pubsub.publish({ 43 | topic: 'new_product_updates', 44 | payload: { 45 | productAdded: product 46 | } 47 | }) 48 | 49 | return product 50 | } 51 | }, 52 | Subscription: { 53 | productAdded: { 54 | subscribe: async (_, __, { pubsub }) => { 55 | return await pubsub.subscribe('new_product_updates') 56 | } 57 | } 58 | } 59 | } 60 | 61 | // server start 62 | const start = async () => { 63 | try { 64 | // register GraphQl 65 | app.register(mercurius, { 66 | schema, 67 | resolvers, 68 | graphiql: true, 69 | subscription: { 70 | async onConnect ({ payload }) { 71 | app.log.info({ payload }, 'connection_init data') 72 | return true 73 | } 74 | } 75 | }) 76 | 77 | // start server 78 | await app.listen({ port: 3000 }) 79 | } catch (error) { 80 | app.log.error(error) 81 | } 82 | } 83 | 84 | start() 85 | -------------------------------------------------------------------------------- /examples/subscription/mqemitter-mongodb-subscription.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const mercurius = require('mercurius') 4 | const Fastify = require('fastify') 5 | const mongodbMQEmitter = require('mqemitter-mongodb') 6 | 7 | const app = Fastify({ logger: true }) 8 | 9 | // mq 10 | let emitter 11 | 12 | // list of products 13 | const products = [] 14 | 15 | // graphql schema 16 | const schema = ` 17 | type Product { 18 | name: String! 19 | state: String! 20 | } 21 | 22 | type Query { 23 | products: [Product] 24 | } 25 | 26 | type Mutation { 27 | addProduct(name: String!, state: String!): Product 28 | } 29 | 30 | type Subscription { 31 | productAdded: Product 32 | } 33 | ` 34 | 35 | // graphql resolvers 36 | const resolvers = { 37 | Query: { 38 | products: () => products 39 | }, 40 | Mutation: { 41 | addProduct: async (_, { name, state }, { pubsub }) => { 42 | const product = { name, state } 43 | 44 | products.push(product) 45 | 46 | pubsub.publish({ 47 | topic: 'new_product_updates', 48 | payload: { 49 | productAdded: product 50 | } 51 | }) 52 | 53 | return product 54 | } 55 | }, 56 | Subscription: { 57 | productAdded: { 58 | subscribe: async (_, __, { pubsub }) => { 59 | return await pubsub.subscribe('new_product_updates') 60 | } 61 | } 62 | } 63 | } 64 | 65 | const handle = (conn) => conn.pipe(conn) 66 | 67 | // server start 68 | const start = async () => { 69 | try { 70 | // initialize emitter 71 | emitter = mongodbMQEmitter({ url: 'mongodb://localhost/test' }) 72 | 73 | // register GraphQl 74 | app.register(mercurius, { 75 | schema, 76 | resolvers, 77 | subscription: { 78 | emitter, 79 | handle 80 | } 81 | }) 82 | 83 | // start server 84 | await app.listen({ port: 3000 }) 85 | } catch (error) { 86 | app.log.error(error) 87 | } 88 | } 89 | 90 | start() 91 | -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { GraphQLError } = require('graphql') 4 | const createError = require('@fastify/error') 5 | 6 | class ErrorWithProps extends Error { 7 | constructor (message, extensions, statusCode) { 8 | super(message) 9 | this.extensions = extensions 10 | this.statusCode = statusCode || 200 11 | } 12 | } 13 | 14 | // converts an error to a `GraphQLError` compatible 15 | // allows to copy the `path` & `locations` properties 16 | // from the already serialized error 17 | function toGraphQLError (err) { 18 | if (err instanceof GraphQLError) { 19 | return err 20 | } 21 | 22 | const gqlError = new GraphQLError( 23 | err.message, 24 | err.nodes, 25 | err.source, 26 | err.positions, 27 | err.path, 28 | err, 29 | err.extensions 30 | ) 31 | 32 | gqlError.locations = err.locations 33 | 34 | return gqlError 35 | } 36 | 37 | function defaultErrorFormatter (execution, ctx) { 38 | // There is always app if there is a context 39 | const log = ctx.reply ? ctx.reply.log : ctx.app.log 40 | 41 | let statusCode = execution.data ? 200 : (execution.statusCode || 200) 42 | 43 | const errors = execution.errors.map((error) => { 44 | log.info({ err: error }, error.message) 45 | 46 | // it handles fastify errors MER_ERR_GQL_VALIDATION 47 | if (error.originalError?.errors && Array.isArray(error.originalError.errors)) { 48 | // not all errors are `GraphQLError` type, we need to convert them 49 | return error.originalError.errors.map(toGraphQLError) 50 | } 51 | 52 | return error 53 | // as the result of the outer map could potentially contain arrays with errors 54 | // the result needs to be flattened 55 | // and convert error into serializable format 56 | }).reduce((acc, val) => acc.concat(val), []).map((error) => error.toJSON()) 57 | 58 | // Override status code when there is no data or statusCode present 59 | if (!execution.data && typeof execution.statusCode === 'undefined' && execution.errors.length > 0) { 60 | if (errors.length === 1) { 61 | // If single error defined, use status code if present 62 | if (typeof execution.errors[0].originalError !== 'undefined' && typeof execution.errors[0].originalError.statusCode === 'number') { 63 | statusCode = execution.errors[0].originalError.statusCode 64 | // Otherwise, use 200 as per graphql-over-http spec 65 | } else { 66 | statusCode = 200 67 | } 68 | } 69 | } 70 | 71 | return { 72 | statusCode, 73 | response: { 74 | data: execution.data || null, 75 | errors 76 | } 77 | } 78 | } 79 | 80 | function addErrorsToExecutionResult (execution, errors) { 81 | if (errors) { 82 | let newErrors 83 | if (execution.errors) { 84 | newErrors = execution.errors.concat(errors) 85 | } else { 86 | newErrors = errors 87 | } 88 | execution.errors = newErrors.map((error) => toGraphQLError(error)) 89 | } 90 | return execution 91 | } 92 | 93 | function addErrorsToContext (context, errors) { 94 | let newErrors 95 | if (context.errors !== null) { 96 | newErrors = context.errors.concat(errors) 97 | } else { 98 | newErrors = errors 99 | } 100 | 101 | context.errors = newErrors 102 | } 103 | 104 | const errors = { 105 | /** 106 | * General errors 107 | */ 108 | MER_ERR_INVALID_OPTS: createError( 109 | 'MER_ERR_INVALID_OPTS', 110 | 'Invalid options: %s' 111 | ), 112 | MER_ERR_INVALID_METHOD: createError( 113 | 'MER_ERR_INVALID_METHOD', 114 | 'Invalid method: %s' 115 | ), 116 | MER_ERR_METHOD_NOT_ALLOWED: createError( 117 | 'MER_ERR_METHOD_NOT_ALLOWED', 118 | 'Method not allowed', 119 | 405 120 | ), 121 | /** 122 | * General graphql errors 123 | */ 124 | MER_ERR_GQL_INVALID_SCHEMA: createError( 125 | 'MER_ERR_GQL_INVALID_SCHEMA', 126 | 'Invalid schema: check out the .errors property on the Error' 127 | ), 128 | MER_ERR_GQL_VALIDATION: createError( 129 | 'MER_ERR_GQL_VALIDATION', 130 | 'Graphql validation error', 131 | 400 132 | ), 133 | MER_ERR_GQL_QUERY_DEPTH: createError( 134 | 'MER_ERR_GQL_QUERY_DEPTH', 135 | '`%s query depth (%s) exceeds the query depth limit of %s`' 136 | ), 137 | /** 138 | * Persisted query errors 139 | */ 140 | MER_ERR_GQL_PERSISTED_QUERY_NOT_FOUND: createError( 141 | 'MER_ERR_GQL_PERSISTED_QUERY_NOT_FOUND', 142 | '%s', 143 | 400 144 | ), 145 | MER_ERR_GQL_PERSISTED_QUERY_NOT_SUPPORTED: createError( 146 | 'MER_ERR_GQL_PERSISTED_QUERY_NOT_SUPPORTED', 147 | '%s', 148 | 400 149 | ), 150 | MER_ERR_GQL_PERSISTED_QUERY_MISMATCH: createError( 151 | 'MER_ERR_GQL_PERSISTED_QUERY_MISMATCH', 152 | '%s', 153 | 400 154 | ), 155 | /** 156 | * Subscription errors 157 | */ 158 | MER_ERR_GQL_SUBSCRIPTION_CONNECTION_NOT_READY: createError( 159 | 'MER_ERR_GQL_SUBSCRIPTION_CONNECTION_NOT_READY', 160 | 'Connection is not ready' 161 | ), 162 | MER_ERR_GQL_SUBSCRIPTION_FORBIDDEN: createError( 163 | 'MER_ERR_GQL_SUBSCRIPTION_FORBIDDEN', 164 | 'Forbidden' 165 | ), 166 | MER_ERR_GQL_SUBSCRIPTION_UNKNOWN_EXTENSION: createError( 167 | 'MER_ERR_GQL_SUBSCRIPTION_UNKNOWN_EXTENSION', 168 | 'Unknown extension %s' 169 | ), 170 | MER_ERR_GQL_SUBSCRIPTION_MESSAGE_INVALID: createError( 171 | 'MER_ERR_GQL_SUBSCRIPTION_MESSAGE_INVALID', 172 | 'Invalid message received: %s' 173 | ), 174 | MER_ERR_GQL_SUBSCRIPTION_INVALID_OPERATION: createError( 175 | 'MER_ERR_GQL_SUBSCRIPTION_INVALID_OPERATION', 176 | 'Invalid operation: %s' 177 | ), 178 | /** 179 | * Hooks errors 180 | */ 181 | MER_ERR_HOOK_INVALID_TYPE: createError( 182 | 'MER_ERR_HOOK_INVALID_TYPE', 183 | 'The hook name must be a string', 184 | 500, 185 | TypeError 186 | ), 187 | MER_ERR_HOOK_INVALID_HANDLER: createError( 188 | 'MER_ERR_HOOK_INVALID_HANDLER', 189 | 'The hook callback must be a function', 190 | 500, 191 | TypeError 192 | ), 193 | MER_ERR_HOOK_UNSUPPORTED_HOOK: createError( 194 | 'MER_ERR_HOOK_UNSUPPORTED_HOOK', 195 | '%s hook not supported!', 196 | 500 197 | ), 198 | MER_ERR_SERVICE_RETRY_FAILED: createError( 199 | 'MER_ERR_SERVICE_RETRY_FAILED', 200 | 'Mandatory services retry failed - [%s]', 201 | 500 202 | ) 203 | } 204 | 205 | module.exports = errors 206 | module.exports.ErrorWithProps = ErrorWithProps 207 | module.exports.defaultErrorFormatter = defaultErrorFormatter 208 | module.exports.addErrorsToExecutionResult = addErrorsToExecutionResult 209 | module.exports.addErrorsToContext = addErrorsToContext 210 | module.exports.toGraphQLError = toGraphQLError 211 | -------------------------------------------------------------------------------- /lib/handlers.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { hooksRunner, preExecutionHooksRunner, hookRunner, preParsingHookRunner, onResolutionHookRunner, onEndHookRunner } = require('./hooks') 4 | const { addErrorsToContext } = require('./errors') 5 | 6 | async function preParsingHandler (request) { 7 | await hooksRunner( 8 | request.context.preParsing, 9 | preParsingHookRunner, 10 | request 11 | ) 12 | } 13 | 14 | async function preValidationHandler (request) { 15 | await hooksRunner( 16 | request.context.preValidation, 17 | hookRunner, 18 | request 19 | ) 20 | } 21 | 22 | async function preExecutionHandler (request) { 23 | const { 24 | errors, 25 | modifiedDocument, 26 | modifiedSchema, 27 | modifiedVariables 28 | } = await preExecutionHooksRunner( 29 | request.context.preExecution, 30 | request 31 | ) 32 | 33 | if (errors.length > 0) { 34 | addErrorsToContext(request.context, errors) 35 | } 36 | if ( 37 | typeof modifiedDocument !== 'undefined' || 38 | typeof modifiedSchema !== 'undefined' || 39 | typeof modifiedVariables !== 'undefined' 40 | ) { 41 | return Object.create(null, { 42 | modifiedDocument: { value: modifiedDocument }, 43 | modifiedSchema: { value: modifiedSchema }, 44 | modifiedVariables: { value: modifiedVariables } 45 | }) 46 | } 47 | 48 | return {} 49 | } 50 | 51 | async function onResolutionHandler (request) { 52 | await hooksRunner( 53 | request.context.onResolution, 54 | onResolutionHookRunner, 55 | request 56 | ) 57 | } 58 | 59 | async function preSubscriptionParsingHandler (request) { 60 | await hooksRunner( 61 | request.context.preSubscriptionParsing, 62 | preParsingHookRunner, 63 | request 64 | ) 65 | } 66 | 67 | async function preSubscriptionExecutionHandler (request) { 68 | await hooksRunner( 69 | request.context.preSubscriptionExecution, 70 | hookRunner, 71 | request 72 | ) 73 | } 74 | 75 | async function onSubscriptionResolutionHandler (request) { 76 | await hooksRunner( 77 | request.context.onSubscriptionResolution, 78 | onResolutionHookRunner, 79 | request 80 | ) 81 | } 82 | 83 | async function onSubscriptionEndHandler (request) { 84 | await hooksRunner( 85 | request.context.onSubscriptionEnd, 86 | onEndHookRunner, 87 | request 88 | ) 89 | } 90 | 91 | async function onExtendSchemaHandler (request) { 92 | await hooksRunner( 93 | request.context.onExtendSchema, 94 | hookRunner, 95 | request 96 | ) 97 | } 98 | 99 | module.exports = { 100 | preParsingHandler, 101 | preValidationHandler, 102 | preExecutionHandler, 103 | onResolutionHandler, 104 | preSubscriptionParsingHandler, 105 | preSubscriptionExecutionHandler, 106 | onSubscriptionResolutionHandler, 107 | onSubscriptionEndHandler, 108 | onExtendSchemaHandler 109 | } 110 | -------------------------------------------------------------------------------- /lib/hooks.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const applicationHooks = [ 4 | 'onExtendSchema' 5 | ] 6 | 7 | const lifecycleHooks = [ 8 | 'preParsing', 9 | 'preValidation', 10 | 'preExecution', 11 | 'onResolution', 12 | 'preSubscriptionParsing', 13 | 'preSubscriptionExecution', 14 | 'onSubscriptionResolution', 15 | 'onSubscriptionEnd' 16 | ] 17 | const supportedHooks = lifecycleHooks.concat(applicationHooks) 18 | const { MER_ERR_HOOK_INVALID_TYPE, MER_ERR_HOOK_INVALID_HANDLER, MER_ERR_HOOK_UNSUPPORTED_HOOK } = require('./errors') 19 | 20 | function Hooks () { 21 | this.preParsing = [] 22 | this.preValidation = [] 23 | this.preExecution = [] 24 | this.onResolution = [] 25 | this.preSubscriptionParsing = [] 26 | this.preSubscriptionExecution = [] 27 | this.onSubscriptionResolution = [] 28 | this.onSubscriptionEnd = [] 29 | this.onExtendSchema = [] 30 | } 31 | 32 | Hooks.prototype.validate = function (hook, fn) { 33 | if (typeof hook !== 'string') throw new MER_ERR_HOOK_INVALID_TYPE() 34 | if (typeof fn !== 'function') throw new MER_ERR_HOOK_INVALID_HANDLER() 35 | if (supportedHooks.indexOf(hook) === -1) { 36 | throw new MER_ERR_HOOK_UNSUPPORTED_HOOK(hook) 37 | } 38 | } 39 | 40 | Hooks.prototype.add = function (hook, fn) { 41 | this.validate(hook, fn) 42 | this[hook].push(fn) 43 | } 44 | 45 | function assignLifeCycleHooksToContext (context, hooks) { 46 | const contextHooks = { 47 | preParsing: null, 48 | preValidation: null, 49 | preExecution: null, 50 | onResolution: null, 51 | preSubscriptionParsing: null, 52 | preSubscriptionExecution: null, 53 | onSubscriptionResolution: null, 54 | onSubscriptionEnd: null 55 | } 56 | if (hooks.preParsing.length > 0) contextHooks.preParsing = hooks.preParsing.slice() 57 | if (hooks.preValidation.length > 0) contextHooks.preValidation = hooks.preValidation.slice() 58 | if (hooks.preExecution.length > 0) contextHooks.preExecution = hooks.preExecution.slice() 59 | if (hooks.onResolution.length > 0) contextHooks.onResolution = hooks.onResolution.slice() 60 | if (hooks.preSubscriptionParsing.length > 0) contextHooks.preSubscriptionParsing = hooks.preSubscriptionParsing.slice() 61 | if (hooks.preSubscriptionExecution.length > 0) contextHooks.preSubscriptionExecution = hooks.preSubscriptionExecution.slice() 62 | if (hooks.onSubscriptionResolution.length > 0) contextHooks.onSubscriptionResolution = hooks.onSubscriptionResolution.slice() 63 | if (hooks.onSubscriptionEnd.length > 0) contextHooks.onSubscriptionEnd = hooks.onSubscriptionEnd.slice() 64 | return Object.assign(context, contextHooks) 65 | } 66 | 67 | function assignApplicationHooksToContext (context, hooks) { 68 | const contextHooks = { 69 | onExtendSchema: null 70 | } 71 | if (hooks.onExtendSchema.length > 0) contextHooks.onExtendSchema = hooks.onExtendSchema.slice() 72 | return Object.assign(context, contextHooks) 73 | } 74 | 75 | async function hooksRunner (functions, runner, request) { 76 | for (const fn of functions) { 77 | await runner(fn, request) 78 | } 79 | } 80 | 81 | async function preExecutionHooksRunner (functions, request) { 82 | let errors = [] 83 | let modifiedSchema 84 | let modifiedDocument 85 | let modifiedVariables 86 | 87 | for (const fn of functions) { 88 | const result = await fn( 89 | modifiedSchema || request.schema, 90 | modifiedDocument || request.document, 91 | request.context, 92 | modifiedVariables || request.variables 93 | ) 94 | 95 | if (result) { 96 | if (typeof result.schema !== 'undefined') { 97 | modifiedSchema = result.schema 98 | } 99 | if (typeof result.document !== 'undefined') { 100 | modifiedDocument = result.document 101 | } 102 | if (typeof result.variables !== 'undefined') { 103 | modifiedVariables = result.variables 104 | } 105 | if (typeof result.errors !== 'undefined') { 106 | errors = errors.concat(result.errors) 107 | } 108 | } 109 | } 110 | 111 | return { errors, modifiedDocument, modifiedSchema, modifiedVariables } 112 | } 113 | 114 | function hookRunner (fn, request) { 115 | return fn(request.schema, request.document, request.context) 116 | } 117 | 118 | function preParsingHookRunner (fn, request) { 119 | return fn(request.schema, request.source, request.context) 120 | } 121 | 122 | function onResolutionHookRunner (fn, request) { 123 | return fn(request.execution, request.context) 124 | } 125 | 126 | function onEndHookRunner (fn, request) { 127 | return fn(request.context, request.id) 128 | } 129 | 130 | module.exports = { 131 | Hooks, 132 | assignLifeCycleHooksToContext, 133 | assignApplicationHooksToContext, 134 | hooksRunner, 135 | preExecutionHooksRunner, 136 | hookRunner, 137 | preParsingHookRunner, 138 | onResolutionHookRunner, 139 | onEndHookRunner 140 | } 141 | -------------------------------------------------------------------------------- /lib/persistedQueryDefaults.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const crypto = require('crypto') 4 | const LRU = require('tiny-lru').lru 5 | 6 | const persistedQueryDefaults = { 7 | prepared: (persistedQueries) => ({ 8 | isPersistedQuery: (request) => request.persisted, 9 | getHash: (request) => request.query, 10 | getQueryFromHash: async (hash) => persistedQueries[hash] 11 | }), 12 | preparedOnly: (persistedQueries) => ({ 13 | isPersistedQuery: (request) => true, 14 | getHash: (request) => request.persisted ? request.query : false, // Only support persisted queries 15 | getQueryFromHash: async (hash) => persistedQueries[hash] 16 | }), 17 | automatic: (maxSize) => { 18 | // Initialize a LRU cache in the local scope. 19 | // LRU is used to prevent DoS attacks. 20 | const cache = LRU(maxSize || 1024) 21 | return ({ 22 | isPersistedQuery: (request) => !request.query && (request.extensions || {}).persistedQuery, 23 | isPersistedQueryRetry: (request) => request.query && (request.extensions || {}).persistedQuery, 24 | getHash: (request) => { 25 | const { version, sha256Hash } = request.extensions.persistedQuery 26 | return version === 1 ? sha256Hash : false 27 | }, 28 | getQueryFromHash: async (hash) => cache.get(hash), 29 | getHashForQuery: (query) => crypto.createHash('sha256').update(query, 'utf8').digest('hex'), 30 | saveQuery: async (hash, query) => cache.set(hash, query), 31 | notFoundError: 'PersistedQueryNotFound', 32 | notSupportedError: 'PersistedQueryNotSupported', 33 | mismatchError: 'provided sha does not match query' 34 | }) 35 | } 36 | } 37 | 38 | module.exports = persistedQueryDefaults 39 | -------------------------------------------------------------------------------- /lib/queryDepth.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Kind } = require('graphql') 4 | const { MER_ERR_GQL_QUERY_DEPTH } = require('./errors') 5 | 6 | /** 7 | * Returns the depth of nodes in a graphql query 8 | * Based on the GraphQL Depth Limit package from Stem (https://stem.is) 9 | * Project: graphql-depth-limit https://github.com/stems/graphql-depth-limit 10 | * Copyright (c) 2017 Stem 11 | * License (MIT License) https://github.com/stems/graphql-depth-limit/blob/master/LICENSE 12 | * @param {Array} [definition] the definitions from a graphQL document 13 | * @returns {Array} {Errors} An array of errors 14 | */ 15 | function queryDepth (definitions, queryDepthLimit) { 16 | const queries = getQueriesAndMutations(definitions) 17 | const queryDepth = {} 18 | 19 | for (const name in queries) { 20 | queryDepth[name] = determineDepth(queries[name]) 21 | } 22 | 23 | const errors = [] 24 | if (typeof queryDepthLimit === 'number') { 25 | for (const query of Object.keys(queryDepth)) { 26 | const totalDepth = queryDepth[query] 27 | if (totalDepth > queryDepthLimit) { 28 | const queryDepthError = new MER_ERR_GQL_QUERY_DEPTH(query, totalDepth, queryDepthLimit) 29 | errors.push(queryDepthError) 30 | } 31 | } 32 | } 33 | 34 | return errors 35 | } 36 | function determineDepth (node, current = 0) { 37 | if (node.selectionSet) { 38 | return Math.max(...node.selectionSet.selections.map((selection) => determineDepth(selection, current + 1))) 39 | } 40 | return current 41 | } 42 | function getQueriesAndMutations (definitions) { 43 | return definitions.reduce((map, definition) => { 44 | if (definition.kind === Kind.OPERATION_DEFINITION) { 45 | map[definition.name ? definition.name.value : 'unnamedQuery'] = definition 46 | } 47 | return map 48 | }, {}) 49 | } 50 | 51 | module.exports = queryDepth 52 | -------------------------------------------------------------------------------- /lib/subscriber.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { Readable } = require('readable-stream') 4 | 5 | class PubSub { 6 | constructor (emitter) { 7 | this.emitter = emitter 8 | } 9 | 10 | subscribe (topic, queue) { 11 | return new Promise((resolve, reject) => { 12 | function listener (value, cb) { 13 | queue.push(value.payload) 14 | cb() 15 | } 16 | 17 | const close = () => { 18 | this.emitter.removeListener(topic, listener) 19 | } 20 | 21 | this.emitter.on(topic, listener, (err) => { 22 | if (err) { 23 | return reject(err) 24 | } 25 | 26 | resolve() 27 | }) 28 | if (!queue.close) queue.close = [] 29 | queue.close.push(close) 30 | }) 31 | } 32 | 33 | publish (event, callback) { 34 | this.emitter.emit(event, callback) 35 | } 36 | } 37 | 38 | // One context - and queue for each subscription 39 | class SubscriptionContext { 40 | constructor ({ pubsub, fastify }) { 41 | this.fastify = fastify 42 | this.pubsub = pubsub 43 | this.queue = new Readable({ 44 | objectMode: true, 45 | read: () => {} 46 | }) 47 | } 48 | 49 | // `topics` param can be: 50 | // - string: subscribe to a single topic 51 | // - array: subscribe to multiple topics 52 | subscribe (topics, ...customArgs) { 53 | if (typeof topics === 'string') { 54 | return this.pubsub.subscribe(topics, this.queue, ...customArgs).then(() => this.queue) 55 | } 56 | return Promise.all(topics.map((topic) => this.pubsub.subscribe(topic, this.queue, ...customArgs))).then(() => this.queue) 57 | } 58 | 59 | publish (event) { 60 | return new Promise((resolve, reject) => { 61 | this.pubsub.publish(event, (err) => { 62 | if (err) { 63 | return reject(err) 64 | } 65 | resolve() 66 | }) 67 | }).catch(err => { 68 | this.fastify.log.error(err) 69 | }) 70 | } 71 | 72 | close () { 73 | // In rare cases when `subscribe()` not called (e.g. some network error) 74 | // `close` will be `undefined`. 75 | if (Array.isArray(this.queue.close)) { 76 | this.queue.close.map((close) => close()) 77 | } 78 | this.queue.push(null) 79 | } 80 | } 81 | 82 | function withFilter (subscribeFn, filterFn) { 83 | return async function * (root, args, context, info) { 84 | const subscription = await subscribeFn(root, args, context, info) 85 | for await (const payload of subscription) { 86 | try { 87 | if (await filterFn(payload, args, context, info)) { 88 | yield payload 89 | } 90 | } catch (err) { 91 | context.app.log.error(err) 92 | continue 93 | } 94 | } 95 | } 96 | } 97 | 98 | module.exports = { 99 | PubSub, 100 | SubscriptionContext, 101 | withFilter 102 | } 103 | -------------------------------------------------------------------------------- /lib/subscription-protocol.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const GRAPHQL_WS = 'graphql-ws' 4 | const GRAPHQL_TRANSPORT_WS = 'graphql-transport-ws' 5 | 6 | module.exports.GRAPHQL_WS = GRAPHQL_WS 7 | module.exports.GRAPHQL_TRANSPORT_WS = GRAPHQL_TRANSPORT_WS 8 | 9 | module.exports.getProtocolByName = function (name) { 10 | switch (true) { 11 | case (name.indexOf(GRAPHQL_TRANSPORT_WS) !== -1): 12 | return { 13 | GQL_CONNECTION_INIT: 'connection_init', // Client -> Server 14 | GQL_CONNECTION_ACK: 'connection_ack', // Server -> Client 15 | GQL_CONNECTION_ERROR: 'connection_error', // Server -> Client 16 | GQL_CONNECTION_KEEP_ALIVE: 'ping', // Bidirectional 17 | GQL_CONNECTION_KEEP_ALIVE_ACK: 'pong', // Bidirectional 18 | GQL_CONNECTION_TERMINATE: 'connection_terminate', // Client -> Server 19 | GQL_START: 'subscribe', // Client -> Server 20 | GQL_DATA: 'next', // Server -> Client 21 | GQL_ERROR: 'error', // Server -> Client 22 | GQL_COMPLETE: 'complete', // Server -> Client 23 | GQL_STOP: 'complete' // Client -> Server 24 | } 25 | case (name.indexOf(GRAPHQL_WS) !== -1): 26 | return { 27 | GQL_CONNECTION_INIT: 'connection_init', // Client -> Server 28 | GQL_CONNECTION_ACK: 'connection_ack', // Server -> Client 29 | GQL_CONNECTION_ERROR: 'connection_error', // Server -> Client 30 | GQL_CONNECTION_KEEP_ALIVE: 'ka', // Server -> Client 31 | GQL_CONNECTION_TERMINATE: 'connection_terminate', // Client -> Server 32 | GQL_START: 'start', // Client -> Server 33 | GQL_DATA: 'data', // Server -> Client 34 | GQL_ERROR: 'error', // Server -> Client 35 | GQL_COMPLETE: 'complete', // Server -> Client 36 | GQL_STOP: 'stop' // Client -> Server 37 | } 38 | default: 39 | return null 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /lib/subscription.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const fastifyWebsocket = require('@fastify/websocket') 4 | const { assignLifeCycleHooksToContext, Hooks } = require('./hooks') 5 | const { kHooks } = require('./symbols') 6 | const SubscriptionConnection = require('./subscription-connection') 7 | const { getProtocolByName } = require('./subscription-protocol') 8 | 9 | function createConnectionHandler ({ subscriber, fastify, onConnect, onDisconnect, entityResolversFactory, subscriptionContextFn, keepAlive, fullWsTransport, errorFormatter }) { 10 | return async (socket, request) => { 11 | if (socket.protocol === undefined || getProtocolByName(socket.protocol) === null) { 12 | request.log.warn('wrong websocket protocol') 13 | // Close the connection with an error code, ws v2 ensures that the 14 | // connection is cleaned up even when the closing handshake fails. 15 | // 1002: protocol error 16 | socket.close(1002) 17 | 18 | return 19 | } 20 | 21 | let context = { 22 | app: fastify, 23 | pubsub: subscriber, 24 | request 25 | } 26 | 27 | if (context.app.graphql && context.app.graphql[kHooks]) { 28 | context = assignLifeCycleHooksToContext(context, context.app.graphql[kHooks]) 29 | } else { 30 | context = assignLifeCycleHooksToContext(context, new Hooks()) 31 | } 32 | 33 | let resolveContext 34 | 35 | if (subscriptionContextFn) { 36 | resolveContext = () => subscriptionContextFn(socket, request) 37 | } 38 | 39 | const subscriptionConnection = new SubscriptionConnection(socket, { 40 | subscriber, 41 | fastify, 42 | onConnect, 43 | onDisconnect, 44 | entityResolversFactory, 45 | context, 46 | resolveContext, 47 | keepAlive, 48 | fullWsTransport, 49 | errorFormatter 50 | }) 51 | 52 | /* istanbul ignore next */ 53 | socket.on('error', () => { 54 | subscriptionConnection.close() 55 | }) 56 | socket.on('close', () => { 57 | subscriptionConnection.close() 58 | }) 59 | } 60 | } 61 | 62 | module.exports = async function (fastify, opts) { 63 | const { getOptions, subscriber, verifyClient, onConnect, onDisconnect, entityResolversFactory, subscriptionContextFn, keepAlive, fullWsTransport, errorFormatter } = opts 64 | 65 | // If `fastify.websocketServer` exists, it means `@fastify/websocket` already registered. 66 | // Without this check, @fastify/websocket will be registered multiple times and raises FST_ERR_DEC_ALREADY_PRESENT. 67 | if (fastify.websocketServer === undefined) { 68 | await fastify.register(fastifyWebsocket, { 69 | options: { 70 | maxPayload: 1048576, 71 | verifyClient 72 | } 73 | }) 74 | } 75 | 76 | fastify.route({ 77 | ...getOptions, 78 | wsHandler: createConnectionHandler({ 79 | subscriber, 80 | fastify, 81 | onConnect, 82 | onDisconnect, 83 | entityResolversFactory, 84 | subscriptionContextFn, 85 | keepAlive, 86 | fullWsTransport, 87 | errorFormatter 88 | }) 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /lib/symbols.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const keys = { 4 | kLoaders: Symbol('mercurius.loaders'), 5 | kFactory: Symbol('mercurius.loadersFactory'), 6 | kSubscriptionFactory: Symbol('mercurius.subscriptionLoadersFactory'), 7 | kHooks: Symbol('mercurius.hooks'), 8 | kRequestContext: Symbol('mercurius.requestContext') 9 | } 10 | 11 | module.exports = keys 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mercurius", 3 | "version": "16.1.0", 4 | "description": "Fastify GraphQL adapter with subscription support", 5 | "main": "index.js", 6 | "types": "index.d.ts", 7 | "scripts": { 8 | "docs": "docsify serve", 9 | "unit": "tap test/*.js test/internals/*.js", 10 | "cov": "tap --coverage-report=html -J test/*.js test/internals/*.js", 11 | "lint": "eslint", 12 | "lint:fix": "eslint --fix", 13 | "typescript": "tsd", 14 | "test": "npm run lint && npm run test:unit && npm run typescript", 15 | "test:unit": "npm run unit" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/mercurius-js/mercurius.git" 20 | }, 21 | "author": "Matteo Collina ", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/mercurius-js/mercurius/issues" 25 | }, 26 | "homepage": "https://mercurius.dev", 27 | "peerDependencies": { 28 | "graphql": "^16.0.0" 29 | }, 30 | "devDependencies": { 31 | "@graphql-tools/merge": "^9.0.0", 32 | "@graphql-tools/schema": "^10.0.0", 33 | "@graphql-tools/utils": "^10.0.0", 34 | "@sinonjs/fake-timers": "^14.0.0", 35 | "@types/isomorphic-form-data": "^2.0.0", 36 | "@types/node": "^22.0.0", 37 | "@types/ws": "^8.2.0", 38 | "autocannon": "^8.0.0", 39 | "concurrently": "^9.0.0", 40 | "docsify-cli": "^4.4.3", 41 | "eslint": "^9.9.1", 42 | "fastify": "^5.0.0", 43 | "graphql": "^16.0.0", 44 | "graphql-tag": "^2.12.6", 45 | "graphql-ws": "^6.0.1", 46 | "neostandard": "^0.12.0", 47 | "pre-commit": "^1.2.2", 48 | "proxyquire": "^2.1.3", 49 | "semver": "^7.5.0", 50 | "sinon": "^20.0.0", 51 | "split2": "^4.0.0", 52 | "tap": "^21.0.0", 53 | "tsd": "^0.32.0", 54 | "typescript": "~5.8.3", 55 | "undici": "^7.0.0", 56 | "wait-on": "^8.0.0" 57 | }, 58 | "dependencies": { 59 | "@fastify/error": "^4.0.0", 60 | "@fastify/static": "^8.0.0", 61 | "@fastify/websocket": "^11.0.0", 62 | "fastify-plugin": "^5.0.0", 63 | "graphql-jit": "0.8.7", 64 | "mqemitter": "^7.0.0", 65 | "p-map": "^4.0.0", 66 | "quick-lru": "^7.0.0", 67 | "readable-stream": "^4.0.0", 68 | "safe-stable-stringify": "^2.3.0", 69 | "secure-json-parse": "^3.0.0", 70 | "single-user-cache": "^2.0.0", 71 | "tiny-lru": "^11.0.0", 72 | "ws": "^8.2.2" 73 | }, 74 | "tsd": { 75 | "directory": "test/types" 76 | }, 77 | "engines": { 78 | "node": "^20.9.0 || >=22.0.0" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /static/graphiql.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | GraphiQL 6 | 7 | 8 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mercurius-js/mercurius/c68bdffe215de21889e15e957c7152bcc77b236d/static/img/favicon.ico -------------------------------------------------------------------------------- /static/main.js: -------------------------------------------------------------------------------- 1 | /* global React:false ReactDOM:false GraphiQL:false */ 2 | 3 | const importer = { 4 | url: (url) => { 5 | return new Promise((resolve, reject) => { 6 | const script = document.createElement('script') 7 | script.type = 'text/javascript' 8 | script.src = url 9 | script.crossOrigin = 'anonymous' 10 | script.addEventListener('load', () => resolve(script), false) 11 | script.addEventListener('error', (err) => reject(err), false) 12 | document.body.appendChild(script) 13 | }) 14 | }, 15 | urls: (urls) => { 16 | return Promise.all(urls.map(importer.url)) 17 | } 18 | } 19 | 20 | // The functions above are required to wrap the fetcher and access/enrich the data returned by the GQL query 21 | // Except `fetcherWrapper`, they are copy/pasted directly from the `graphiql` codebase. 22 | 23 | function observableToPromise (observable) { 24 | return new Promise((resolve, reject) => { 25 | const subscription = observable.subscribe({ 26 | next: v => { 27 | resolve(v) 28 | subscription.unsubscribe() 29 | }, 30 | error: reject, 31 | complete: () => { 32 | reject(new Error('no value resolved')) 33 | } 34 | }) 35 | }) 36 | } 37 | 38 | function isObservable (value) { 39 | return ( 40 | typeof value === 'object' && 41 | value !== null && 42 | 'subscribe' in value && 43 | typeof value.subscribe === 'function' 44 | ) 45 | } 46 | 47 | function isAsyncIterable (input) { 48 | return ( 49 | typeof input === 'object' && 50 | input !== null && 51 | ((input)[Symbol.toStringTag] === 'AsyncGenerator' || 52 | Symbol.asyncIterator in input) 53 | ) 54 | } 55 | 56 | function asyncIterableToPromise ( 57 | input 58 | ) { 59 | return new Promise((resolve, reject) => { 60 | const iteratorReturn = ( 61 | 'return' in input ? input : input[Symbol.asyncIterator]() 62 | ).return?.bind(input) 63 | const iteratorNext = ( 64 | 'next' in input ? input : input[Symbol.asyncIterator]() 65 | ).next.bind(input) 66 | 67 | iteratorNext() 68 | .then(result => { 69 | resolve(result.value) 70 | // ensure cleanup 71 | iteratorReturn?.() 72 | }) 73 | .catch(err => { 74 | reject(err) 75 | }) 76 | }) 77 | } 78 | 79 | function fetcherReturnToPromise (fetcherResult) { 80 | return Promise.resolve(fetcherResult).then(result => { 81 | if (isAsyncIterable(result)) { 82 | return asyncIterableToPromise(result) 83 | } else if (isObservable(result)) { 84 | return observableToPromise(result) 85 | } 86 | return result 87 | }) 88 | } 89 | 90 | function fetcherWrapper (fetcher, cbs = []) { 91 | return async (gqlp, fetchOpt) => { 92 | const fetchResponse = await fetcher(gqlp, fetchOpt) 93 | const result = await fetcherReturnToPromise(fetchResponse) 94 | return cbs.reduce((acc, cb) => cb(acc), result) 95 | } 96 | } 97 | 98 | /** 99 | * Verify if the baseUrl is already present in the first part of GRAPHQL_ENDPOINT url 100 | * to avoid unexpected duplication of paths 101 | * @param {string} baseUrl [comes from {@link render} function] 102 | * @returns boolean 103 | */ 104 | function isDuplicatedUrlArg (baseUrl) { 105 | const checker = window.GRAPHQL_ENDPOINT.split('/') 106 | return (checker[1] === baseUrl) 107 | } 108 | 109 | function render () { 110 | const host = window.location.host 111 | const websocketProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:' 112 | let url = '' 113 | let subscriptionUrl = '' 114 | let pathName = window.location.pathname 115 | if (pathName.startsWith('/')) { 116 | pathName = pathName.substring(1) 117 | } 118 | pathName = pathName.split('/') 119 | const baseUrl = pathName[0] 120 | if (baseUrl !== 'graphiql') { 121 | url = `${window.location.protocol}//${host}/${baseUrl}${window.GRAPHQL_ENDPOINT}` 122 | subscriptionUrl = `${websocketProtocol}//${host}/${baseUrl}${window.GRAPHQL_ENDPOINT}` 123 | if (isDuplicatedUrlArg(baseUrl)) { 124 | url = `${window.location.protocol}//${host}${window.GRAPHQL_ENDPOINT}` 125 | subscriptionUrl = `${websocketProtocol}//${host}${window.GRAPHQL_ENDPOINT}` 126 | } 127 | } else { 128 | url = `${window.location.protocol}//${host}${window.GRAPHQL_ENDPOINT}` 129 | subscriptionUrl = `${websocketProtocol}//${host}${window.GRAPHQL_ENDPOINT}` 130 | } 131 | 132 | const availablePlugins = window.GRAPHIQL_PLUGIN_LIST 133 | .map(plugin => window[`GRAPIHQL_PLUGIN_${plugin.toUpperCase()}`]) 134 | .filter(pluginData => pluginData && pluginData.umdUrl) 135 | 136 | const fetcherWrapperPlugins = availablePlugins 137 | .filter(plugin => plugin.fetcherWrapper) 138 | .map(pluginData => window[pluginData.name][window[`GRAPIHQL_PLUGIN_${pluginData.name.toUpperCase()}`].fetcherWrapper]) 139 | 140 | const fetcher = fetcherWrapper(GraphiQL.createFetcher({ 141 | url, 142 | subscriptionUrl 143 | }), fetcherWrapperPlugins) 144 | 145 | const plugins = availablePlugins.map(pluginData => window[pluginData.name].umdPlugin(window[`GRAPIHQL_PLUGIN_${pluginData.name.toUpperCase()}`].props)) 146 | 147 | ReactDOM.render( 148 | React.createElement(GraphiQL, { 149 | fetcher, 150 | headerEditorEnabled: true, 151 | shouldPersistHeaders: true, 152 | plugins 153 | }), 154 | document.getElementById('main') 155 | ) 156 | } 157 | 158 | function importDependencies () { 159 | const link = document.createElement('link') 160 | link.href = 'https://unpkg.com/graphiql@3.8.3/graphiql.min.css' 161 | link.type = 'text/css' 162 | link.rel = 'stylesheet' 163 | link.media = 'screen,print' 164 | link.crossOrigin = 'anonymous' 165 | document.getElementsByTagName('head')[0].appendChild(link) 166 | 167 | return importer.urls([ 168 | 'https://unpkg.com/react@18.3.1/umd/react.production.min.js', 169 | 'https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js', 170 | 'https://unpkg.com/graphiql@3.8.3/graphiql.min.js' 171 | ]).then(function () { 172 | const pluginUrls = window.GRAPHIQL_PLUGIN_LIST 173 | .map(plugin => window[`GRAPIHQL_PLUGIN_${plugin.toUpperCase()}`].umdUrl) 174 | .filter(url => !!url) 175 | 176 | if (pluginUrls.length) { 177 | return importer.urls(pluginUrls) 178 | } 179 | }) 180 | } 181 | 182 | if ('serviceWorker' in navigator) { 183 | navigator 184 | .serviceWorker 185 | .register('./graphiql/sw.js') 186 | .then(importDependencies).then(render) 187 | } else { 188 | importDependencies() 189 | .then(render) 190 | } 191 | -------------------------------------------------------------------------------- /static/sw.js: -------------------------------------------------------------------------------- 1 | /* global fetch:false caches:false self:false */ 2 | 3 | self.addEventListener('install', function (e) { 4 | e.waitUntil( 5 | caches.open('graphiql-v3.8.3').then(function (cache) { 6 | return cache.addAll([ 7 | './main.js', 8 | 'https://unpkg.com/graphiql@3.8.3/graphiql.css', 9 | 'https://unpkg.com/react@18.3.1/umd/react.production.min.js', 10 | 'https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js', 11 | 'https://unpkg.com/graphiql@3.8.3/graphiql.min.js' 12 | ]) 13 | }) 14 | ) 15 | }) 16 | 17 | self.addEventListener('fetch', function (event) { 18 | console.log('loading', event.request.url) 19 | 20 | event.respondWith( 21 | caches.match(event.request).then(function (response) { 22 | return response || fetch(event.request) 23 | }, console.log) 24 | ) 25 | }) 26 | -------------------------------------------------------------------------------- /tap-snapshots/test/errors.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports['test/errors.js > TAP > app.graphql which throws, with JIT enabled, twice > must match snapshot 1'] = ` 9 | { 10 | "errors": [ 11 | { 12 | "message": "Bad Resolver", 13 | "locations": [ 14 | { 15 | "line": 3, 16 | "column": 9 17 | } 18 | ], 19 | "path": [ 20 | "bad" 21 | ] 22 | } 23 | ], 24 | "data": { 25 | "bad": null 26 | } 27 | } 28 | ` 29 | 30 | exports['test/errors.js > TAP > app.graphql which throws, with JIT enabled, twice > must match snapshot 2'] = ` 31 | { 32 | "data": { 33 | "bad": null 34 | }, 35 | "errors": [ 36 | { 37 | "message": "Int cannot represent non-integer value: [function bad]", 38 | "locations": [ 39 | { 40 | "line": 3, 41 | "column": 9 42 | } 43 | ], 44 | "path": [ 45 | "bad" 46 | ] 47 | } 48 | ] 49 | } 50 | ` 51 | 52 | exports['test/errors.js > TAP > POST query which throws, with JIT enabled, twice > must match snapshot 1'] = ` 53 | { 54 | "data": { 55 | "bad": null 56 | }, 57 | "errors": [ 58 | { 59 | "message": "Bad Resolver", 60 | "locations": [ 61 | { 62 | "line": 3, 63 | "column": 15 64 | } 65 | ], 66 | "path": [ 67 | "bad" 68 | ] 69 | } 70 | ] 71 | } 72 | ` 73 | 74 | exports['test/errors.js > TAP > POST query which throws, with JIT enabled, twice > must match snapshot 2'] = ` 75 | { 76 | "data": { 77 | "bad": null 78 | }, 79 | "errors": [ 80 | { 81 | "message": "Int cannot represent non-integer value: [function bad]", 82 | "locations": [ 83 | { 84 | "line": 3, 85 | "column": 15 86 | } 87 | ], 88 | "path": [ 89 | "bad" 90 | ] 91 | } 92 | ] 93 | } 94 | ` 95 | -------------------------------------------------------------------------------- /tap-snapshots/test/routes.js.test.cjs: -------------------------------------------------------------------------------- 1 | /* IMPORTANT 2 | * This snapshot file is auto-generated, but designed for humans. 3 | * It should be checked into source control and tracked carefully. 4 | * Re-generate by setting TAP_SNAPSHOT=1 and running tests. 5 | * Make sure to inspect the output below. Do not ignore changes! 6 | */ 7 | 'use strict' 8 | exports['test/routes.js > TAP > GET return 200 on resolver error > must match snapshot 1'] = ` 9 | { 10 | "data": { 11 | "add": null 12 | }, 13 | "errors": [ 14 | { 15 | "message": "this is a dummy error", 16 | "locations": [ 17 | { 18 | "line": 1, 19 | "column": 2 20 | } 21 | ], 22 | "path": [ 23 | "add" 24 | ] 25 | } 26 | ] 27 | } 28 | ` 29 | 30 | exports['test/routes.js > TAP > HTTP mutation with GET errors > must match snapshot 1'] = ` 31 | { 32 | "data": null, 33 | "errors": [ 34 | { 35 | "message": "Operation cannot be performed via a GET request" 36 | } 37 | ] 38 | } 39 | ` 40 | 41 | exports['test/routes.js > TAP > if ide is graphiql, serve config.js with the correct endpoint > must match snapshot 1'] = ` 42 | window.GRAPHQL_ENDPOINT = '/app/graphql'; 43 | window.GRAPHIQL_PLUGIN_LIST = [] 44 | ` 45 | 46 | exports['test/routes.js > TAP > POST return 200 on resolver error > must match snapshot 1'] = ` 47 | { 48 | "data": { 49 | "add": null 50 | }, 51 | "errors": [ 52 | { 53 | "message": "this is a dummy error", 54 | "locations": [ 55 | { 56 | "line": 1, 57 | "column": 2 58 | } 59 | ], 60 | "path": [ 61 | "add" 62 | ] 63 | } 64 | ] 65 | } 66 | ` 67 | 68 | exports['test/routes.js > TAP > POST return 400 on error > must match snapshot 1'] = ` 69 | { 70 | "data": null, 71 | "errors": [ 72 | { 73 | "message": "Syntax Error: Expected Name, found .", 74 | "locations": [ 75 | { 76 | "line": 1, 77 | "column": 18 78 | } 79 | ] 80 | } 81 | ] 82 | } 83 | ` 84 | 85 | exports['test/routes.js > TAP > server should return 200 on graphql errors (if field can be null) > must match snapshot 1'] = ` 86 | { 87 | "data": { 88 | "hello": null 89 | }, 90 | "errors": [ 91 | { 92 | "message": "Simple error", 93 | "locations": [ 94 | { 95 | "line": 3, 96 | "column": 7 97 | } 98 | ], 99 | "path": [ 100 | "hello" 101 | ] 102 | } 103 | ] 104 | } 105 | ` 106 | 107 | exports['test/routes.js > TAP > server should return 200 on graphql errors (if field can not be null) > must match snapshot 1'] = ` 108 | { 109 | "data": null, 110 | "errors": [ 111 | { 112 | "message": "Simple error", 113 | "locations": [ 114 | { 115 | "line": 3, 116 | "column": 7 117 | } 118 | ], 119 | "path": [ 120 | "hello" 121 | ] 122 | } 123 | ] 124 | } 125 | ` 126 | -------------------------------------------------------------------------------- /test/aliases.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const Fastify = require('fastify') 5 | const GQL = require('..') 6 | 7 | const schema = ` 8 | type Query { 9 | me: User 10 | } 11 | 12 | type Metadata { 13 | info: String! 14 | } 15 | 16 | type User { 17 | id: ID! 18 | name: String! 19 | quote(input: String!): String! 20 | metadata(input: String!): Metadata! 21 | }` 22 | 23 | const users = { 24 | u1: { 25 | id: 'u1', 26 | name: 'John' 27 | }, 28 | u2: { 29 | id: 'u2', 30 | name: 'Jane' 31 | } 32 | } 33 | 34 | const resolvers = { 35 | Query: { 36 | me: (root, args, context, info) => { 37 | return users.u1 38 | } 39 | }, 40 | User: { 41 | quote: (user, args, context, info) => { 42 | return args.input 43 | }, 44 | metadata: (user, args, context, info) => { 45 | return { 46 | info: args.input 47 | } 48 | } 49 | } 50 | } 51 | 52 | function createTestServer (t, customResolvers = resolvers) { 53 | const app = Fastify() 54 | t.teardown(app.close.bind(app)) 55 | app.register(GQL, { schema, resolvers: customResolvers }) 56 | return app 57 | } 58 | 59 | test('should support aliases', async t => { 60 | t.plan(1) 61 | const app = await createTestServer(t) 62 | 63 | const query = ` 64 | query { 65 | user: me { 66 | id 67 | name 68 | newName: name 69 | otherName: name 70 | quote(input: "quote") 71 | firstQuote: quote(input: "foo") 72 | secondQuote: quote(input: "bar") 73 | metadata(input: "info") { 74 | info 75 | } 76 | originalMetadata: metadata(input: "hello") { 77 | hi: info 78 | ho: info 79 | } 80 | moreMetadata: metadata(input: "hi") { 81 | info 82 | } 83 | } 84 | }` 85 | 86 | const res = await app.inject({ 87 | method: 'POST', 88 | headers: { 'content-type': 'application/json' }, 89 | url: '/graphql', 90 | body: JSON.stringify({ query }) 91 | }) 92 | 93 | t.same(JSON.parse(res.body), { 94 | data: { 95 | user: { 96 | id: 'u1', 97 | name: 'John', 98 | newName: 'John', 99 | otherName: 'John', 100 | quote: 'quote', 101 | firstQuote: 'foo', 102 | secondQuote: 'bar', 103 | metadata: { 104 | info: 'info' 105 | }, 106 | originalMetadata: { 107 | hi: 'hello', 108 | ho: 'hello' 109 | }, 110 | moreMetadata: { 111 | info: 'hi' 112 | } 113 | } 114 | } 115 | }) 116 | }) 117 | -------------------------------------------------------------------------------- /test/cache.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const Fastify = require('fastify') 5 | const proxyquire = require('proxyquire') 6 | const GQL = require('..') 7 | const { GraphQLError } = require('graphql-jit/dist/error') 8 | 9 | const schema = ` 10 | type User { 11 | name: String! 12 | password: String! 13 | } 14 | 15 | type Query { 16 | read: [User] 17 | } 18 | ` 19 | 20 | const resolvers = { 21 | Query: { 22 | read: async (_, obj) => { 23 | return [ 24 | { 25 | name: 'foo', 26 | password: 'bar' 27 | } 28 | ] 29 | } 30 | } 31 | } 32 | 33 | test('cache skipped when the GQL Schema has been changed', async t => { 34 | t.plan(4) 35 | 36 | const app = Fastify() 37 | t.teardown(() => app.close()) 38 | 39 | await app.register(GQL, { 40 | schema, 41 | resolvers, 42 | jit: 1 43 | }) 44 | 45 | app.graphql.addHook('preExecution', async (schema, document, context) => { 46 | if (context.reply.request.headers.super === 'true') { 47 | return 48 | } 49 | 50 | const documentClone = JSON.parse(JSON.stringify(document)) 51 | documentClone.definitions[0].selectionSet.selections[0].selectionSet.selections = 52 | document.definitions[0].selectionSet.selections[0].selectionSet.selections.filter(sel => sel.name.value !== 'password') 53 | 54 | return { 55 | document: documentClone 56 | } 57 | }) 58 | 59 | const query = `{ 60 | read { 61 | name 62 | password 63 | } 64 | }` 65 | 66 | await superUserCall('this call warm up the jit counter') 67 | await superUserCall('this call triggers the jit cache') 68 | 69 | { 70 | const res = await app.inject({ 71 | method: 'POST', 72 | headers: { 'content-type': 'application/json', super: 'false' }, 73 | url: '/graphql', 74 | body: JSON.stringify({ query }) 75 | }) 76 | t.same(res.json(), { 77 | data: { 78 | read: [ 79 | { 80 | name: 'foo' 81 | } 82 | ] 83 | } 84 | }, 'this query should not use the cached query') 85 | } 86 | 87 | await superUserCall('this call must use the cache') 88 | 89 | async function superUserCall (msg) { 90 | const res = await app.inject({ 91 | method: 'POST', 92 | headers: { 'content-type': 'application/json', super: 'true' }, 93 | url: '/graphql', 94 | body: JSON.stringify({ query }) 95 | }) 96 | t.same(res.json(), { 97 | data: { 98 | read: [ 99 | { 100 | name: 'foo', 101 | password: 'bar' 102 | } 103 | ] 104 | } 105 | }, msg) 106 | } 107 | }) 108 | 109 | const GQLMock = proxyquire('../index', { 110 | 'graphql-jit': { 111 | compileQuery: () => new GraphQLError('compileQuery stub') 112 | } 113 | }) 114 | 115 | test('cache skipped when no jit response', async t => { 116 | t.plan(1) 117 | 118 | const app = Fastify() 119 | t.teardown(() => app.close()) 120 | 121 | await app.register(GQLMock, { 122 | schema, 123 | resolvers, 124 | jit: 1 125 | }) 126 | 127 | const query = `{ 128 | read { 129 | name 130 | password 131 | } 132 | }` 133 | 134 | { 135 | const res = await app.inject({ 136 | method: 'POST', 137 | headers: { 'content-type': 'application/json', super: 'false' }, 138 | url: '/graphql', 139 | body: JSON.stringify({ query }) 140 | }) 141 | t.same(res.json(), { 142 | data: { 143 | read: [ 144 | { 145 | name: 'foo', 146 | password: 'bar' 147 | } 148 | ] 149 | } 150 | }, 'this query should not use the cached query') 151 | } 152 | }) 153 | -------------------------------------------------------------------------------- /test/custom-root-types.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const Fastify = require('fastify') 5 | const WebSocket = require('ws') 6 | const mq = require('mqemitter') 7 | const GQL = require('..') 8 | 9 | test('redefine query', async (t) => { 10 | const schema = ` 11 | schema { 12 | query: BetterQuery 13 | } 14 | 15 | type BetterQuery { 16 | q: Query 17 | } 18 | 19 | type Query { 20 | id: ID! 21 | } 22 | ` 23 | 24 | const resolvers = { 25 | BetterQuery: { 26 | q: async () => ({ id: '1' }) 27 | }, 28 | 29 | Query: { 30 | id: async () => '1' 31 | } 32 | } 33 | 34 | const app = Fastify() 35 | app.register(GQL, { 36 | schema, 37 | resolvers 38 | }) 39 | 40 | // needed so that graphql is defined 41 | await app.ready() 42 | 43 | const query = '{ q { id } }' 44 | const result = await app.graphql(query) 45 | t.same(result, { 46 | data: { 47 | q: { 48 | id: '1' 49 | } 50 | } 51 | }) 52 | }) 53 | 54 | test('redefined mutation type', async (t) => { 55 | const schema = ` 56 | schema { 57 | query: Query 58 | mutation: BetterMutation 59 | } 60 | 61 | type BetterMutation { 62 | m: Mutation 63 | } 64 | 65 | type Mutation { 66 | name: String! 67 | } 68 | 69 | type Query { 70 | mut: Mutation! 71 | } 72 | ` 73 | 74 | const resolvers = { 75 | BetterMutation: { 76 | m: async () => ({ name: 'Bobby' }) 77 | }, 78 | 79 | Mutation: { 80 | name: async () => 'Bobby' 81 | }, 82 | 83 | Query: { 84 | mut: async () => ({ name: 'Bobby' }) 85 | } 86 | } 87 | 88 | const app = Fastify() 89 | app.register(GQL, { 90 | schema, 91 | resolvers 92 | }) 93 | 94 | // needed so that graphql is defined 95 | await app.ready() 96 | 97 | const mutation = 'mutation { m { name } }' 98 | const res = await app.graphql(mutation) 99 | t.same(res, { 100 | data: { 101 | m: { 102 | name: 'Bobby' 103 | } 104 | } 105 | }) 106 | }) 107 | 108 | test('redefined subscription type', t => { 109 | const app = Fastify() 110 | t.teardown(() => app.close()) 111 | 112 | const sendTestQuery = () => { 113 | app.inject({ 114 | method: 'POST', 115 | url: '/graphql', 116 | body: { 117 | query: ` 118 | query { 119 | notifications { 120 | id 121 | message 122 | } 123 | } 124 | ` 125 | } 126 | }, () => { 127 | sendTestMutation() 128 | }) 129 | } 130 | 131 | const sendTestMutation = () => { 132 | app.inject({ 133 | method: 'POST', 134 | url: '/graphql', 135 | body: { 136 | query: ` 137 | mutation { 138 | addNotification(message: "Hello World") { 139 | id 140 | } 141 | } 142 | ` 143 | } 144 | }, () => {}) 145 | } 146 | 147 | const emitter = mq() 148 | const schema = ` 149 | schema { 150 | query: Query, 151 | mutation: Mutation, 152 | subscription: BetterSubscription 153 | } 154 | 155 | type Notification { 156 | id: ID! 157 | message: String 158 | } 159 | 160 | type Query { 161 | notifications: [Notification] 162 | } 163 | 164 | type Mutation { 165 | addNotification(message: String): Notification 166 | } 167 | 168 | type BetterSubscription { 169 | notificationAdded: Notification 170 | } 171 | ` 172 | 173 | let idCount = 1 174 | const notifications = [{ 175 | id: idCount, 176 | message: 'Notification message' 177 | }] 178 | 179 | const resolvers = { 180 | Query: { 181 | notifications: () => notifications 182 | }, 183 | Mutation: { 184 | addNotification: async (_, { message }) => { 185 | const id = idCount++ 186 | const notification = { 187 | id, 188 | message 189 | } 190 | notifications.push(notification) 191 | await emitter.emit({ 192 | topic: 'NOTIFICATION_ADDED', 193 | payload: { 194 | notificationAdded: notification 195 | } 196 | }) 197 | 198 | return notification 199 | } 200 | }, 201 | BetterSubscription: { 202 | notificationAdded: { 203 | subscribe: (root, args, { pubsub }) => pubsub.subscribe('NOTIFICATION_ADDED') 204 | } 205 | } 206 | } 207 | 208 | app.register(GQL, { 209 | schema, 210 | resolvers, 211 | subscription: { 212 | emitter 213 | } 214 | }) 215 | 216 | app.listen({ port: 0 }, err => { 217 | t.error(err) 218 | 219 | const ws = new WebSocket('ws://localhost:' + (app.server.address()).port + '/graphql', 'graphql-ws') 220 | const client = WebSocket.createWebSocketStream(ws, { encoding: 'utf8', objectMode: true }) 221 | t.teardown(client.destroy.bind(client)) 222 | client.setEncoding('utf8') 223 | 224 | client.write(JSON.stringify({ 225 | type: 'connection_init' 226 | })) 227 | 228 | client.write(JSON.stringify({ 229 | id: 1, 230 | type: 'start', 231 | payload: { 232 | query: ` 233 | subscription { 234 | notificationAdded { 235 | id 236 | message 237 | } 238 | } 239 | ` 240 | } 241 | })) 242 | 243 | client.write(JSON.stringify({ 244 | id: 2, 245 | type: 'start', 246 | payload: { 247 | query: ` 248 | subscription { 249 | notificationAdded { 250 | id 251 | message 252 | } 253 | } 254 | ` 255 | } 256 | })) 257 | 258 | client.write(JSON.stringify({ 259 | id: 2, 260 | type: 'stop' 261 | })) 262 | 263 | client.on('data', chunk => { 264 | const data = JSON.parse(chunk) 265 | 266 | if (data.id === 1 && data.type === 'data') { 267 | t.equal(chunk, JSON.stringify({ 268 | type: 'data', 269 | id: 1, 270 | payload: { 271 | data: { 272 | notificationAdded: { 273 | id: '1', 274 | message: 'Hello World' 275 | } 276 | } 277 | } 278 | })) 279 | 280 | client.end() 281 | t.end() 282 | } else if (data.id === 2 && data.type === 'complete') { 283 | sendTestQuery() 284 | } 285 | }) 286 | }) 287 | }) 288 | -------------------------------------------------------------------------------- /test/disable-instrospection.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const Fastify = require('fastify') 5 | const mercurius = require('..') 6 | const graphql = require('graphql') 7 | 8 | const schema = ` 9 | type Query { 10 | add(x: Int, y: Int): Int 11 | } 12 | ` 13 | 14 | const resolvers = { 15 | Query: { 16 | add: async (_, obj) => { 17 | const { x, y } = obj 18 | return x + y 19 | } 20 | } 21 | } 22 | 23 | test('should disallow instrospection with "__schema" when NoSchemaIntrospectionCustomRule are applied to validationRules', async (t) => { 24 | const app = Fastify() 25 | 26 | const query = '{ __schema { queryType { name } } }' 27 | 28 | app.register(mercurius, { 29 | schema, 30 | resolvers, 31 | graphiql: true, 32 | validationRules: [graphql.NoSchemaIntrospectionCustomRule] 33 | }) 34 | 35 | // needed so that graphql is defined 36 | await app.ready() 37 | await t.rejects(app.graphql(query), { errors: [{ message: 'GraphQL introspection has been disabled, but the requested query contained the field "__schema".' }] }) 38 | }) 39 | 40 | test('should disallow instrospection with "__type" when NoSchemaIntrospectionCustomRule are applied to validationRules', async (t) => { 41 | const app = Fastify() 42 | 43 | const query = '{ __type(name: "Query"){ name } }' 44 | 45 | app.register(mercurius, { 46 | schema, 47 | resolvers, 48 | graphiql: true, 49 | validationRules: [graphql.NoSchemaIntrospectionCustomRule] 50 | }) 51 | 52 | // needed so that graphql is defined 53 | await app.ready() 54 | await t.rejects(app.graphql(query), { errors: [{ message: 'GraphQL introspection has been disabled, but the requested query contained the field "__type".' }] }) 55 | }) 56 | -------------------------------------------------------------------------------- /test/fix-790.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const Fastify = require('fastify') 5 | const GQL = require('..') 6 | 7 | test('Subscription type is not treated as a subscription when subscriptions disabled', async (t) => { 8 | const schema = ` 9 | type Query { 10 | subscription: Subscription! 11 | } 12 | 13 | type Subscription { 14 | id: ID! 15 | } 16 | ` 17 | 18 | const resolvers = { 19 | Query: { 20 | subscription: () => ({ id: '1' }) 21 | }, 22 | 23 | Subscription: { 24 | id: () => '1' 25 | } 26 | } 27 | 28 | const app = Fastify() 29 | app.register(GQL, { 30 | schema, 31 | resolvers, 32 | subscription: false 33 | }) 34 | 35 | // needed so that graphql is defined 36 | await app.ready() 37 | 38 | const query = '{ subscription { id } }' 39 | const result = await app.graphql(query) 40 | t.same(result, { 41 | data: { 42 | subscription: { 43 | id: '1' 44 | } 45 | } 46 | }) 47 | }) 48 | 49 | test('Subscription type is not treated as a subscription by default', async (t) => { 50 | const schema = ` 51 | type Query { 52 | subscription: Subscription! 53 | } 54 | 55 | type Subscription { 56 | id: ID! 57 | } 58 | ` 59 | 60 | const resolvers = { 61 | Query: { 62 | subscription: () => ({ id: '1' }) 63 | }, 64 | 65 | Subscription: { 66 | id: () => '1' 67 | } 68 | } 69 | 70 | const app = Fastify() 71 | app.register(GQL, { 72 | schema, 73 | resolvers 74 | }) 75 | 76 | // needed so that graphql is defined 77 | await app.ready() 78 | 79 | const query = '{ subscription { id } }' 80 | const result = await app.graphql(query) 81 | t.same(result, { 82 | data: { 83 | subscription: { 84 | id: '1' 85 | } 86 | } 87 | }) 88 | }) 89 | -------------------------------------------------------------------------------- /test/graphql-option-override.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const Fastify = require('fastify') 5 | const mercurius = require('..') 6 | 7 | const schema = ` 8 | type User { 9 | name: String! 10 | password: String! 11 | } 12 | 13 | type Query { 14 | read: [User] 15 | } 16 | ` 17 | 18 | const resolvers = { 19 | Query: { 20 | read: async (_, obj) => { 21 | return [ 22 | { 23 | name: 'foo', 24 | password: 'bar' 25 | } 26 | ] 27 | } 28 | } 29 | } 30 | 31 | const query = `{ 32 | read { 33 | name 34 | password 35 | } 36 | }` 37 | 38 | const query2 = `{ 39 | read { 40 | intentionallyUnknownField1 41 | intentionallyUnknownField2 42 | intentionallyUnknownField3 43 | } 44 | }` 45 | 46 | test('do not override graphql function options', async t => { 47 | const app = Fastify() 48 | t.teardown(() => app.close()) 49 | 50 | await app.register(mercurius, { 51 | schema, 52 | resolvers 53 | }) 54 | 55 | await app.ready() 56 | 57 | const res = await app.graphql(query) 58 | 59 | const expectedResult = { 60 | data: { 61 | read: [{ 62 | name: 'foo', 63 | password: 'bar' 64 | }] 65 | } 66 | } 67 | 68 | t.same(res, expectedResult) 69 | }) 70 | 71 | test('override graphql.parse options', async t => { 72 | const app = Fastify() 73 | t.teardown(() => app.close()) 74 | 75 | await app.register(mercurius, { 76 | schema, 77 | resolvers, 78 | graphql: { 79 | parseOptions: { 80 | maxTokens: 1 81 | } 82 | } 83 | }) 84 | 85 | await app.ready() 86 | 87 | const expectedErr = { 88 | errors: [{ 89 | message: 'Syntax Error: Document contains more that 1 tokens. Parsing aborted.' 90 | }] 91 | } 92 | 93 | await t.rejects(app.graphql(query), expectedErr) 94 | }) 95 | 96 | test('do not override graphql.validate options', async t => { 97 | const app = Fastify() 98 | t.teardown(() => app.close()) 99 | 100 | await app.register(mercurius, { 101 | schema, 102 | resolvers 103 | }) 104 | 105 | await app.ready() 106 | 107 | const expectedErr = { 108 | errors: [ 109 | { message: 'Cannot query field "intentionallyUnknownField1" on type "User".' }, 110 | { message: 'Cannot query field "intentionallyUnknownField2" on type "User".' }, 111 | { message: 'Cannot query field "intentionallyUnknownField3" on type "User".' } 112 | ] 113 | } 114 | 115 | await t.rejects(app.graphql(query2), expectedErr) 116 | }) 117 | 118 | test('override graphql.validate options', async t => { 119 | const app = Fastify() 120 | t.teardown(() => app.close()) 121 | 122 | await app.register(mercurius, { 123 | schema, 124 | resolvers, 125 | graphql: { 126 | validateOptions: { 127 | maxErrors: 1 128 | } 129 | } 130 | }) 131 | 132 | await app.ready() 133 | 134 | const expectedErr = { 135 | errors: [ 136 | { message: 'Cannot query field "intentionallyUnknownField1" on type "User".' }, 137 | { message: 'Too many validation errors, error limit reached. Validation aborted.' } 138 | ] 139 | } 140 | 141 | await t.rejects(app.graphql(query2), expectedErr) 142 | }) 143 | -------------------------------------------------------------------------------- /test/hooks-with-batching.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const Fastify = require('fastify') 5 | const sinon = require('sinon') 6 | const GQL = require('..') 7 | 8 | test('batched query has an individual context for each operation through all the lifecycle hooks', async (t) => { 9 | const app = Fastify() 10 | 11 | const preParsingSpy = sinon.spy() 12 | const preValidationSpy = sinon.spy() 13 | const preExecutionSpy = sinon.spy() 14 | const onResolutionSpy = sinon.spy() 15 | 16 | const schema = ` 17 | type Query { 18 | test: String 19 | } 20 | ` 21 | 22 | const resolvers = { 23 | test: () => 'test' 24 | } 25 | 26 | await app.register(GQL, { 27 | schema, 28 | resolvers, 29 | allowBatchedQueries: true 30 | }) 31 | 32 | app.graphql.addHook('preParsing', (_, __, ctx) => { 33 | preParsingSpy(ctx.operationId, ctx.operationsCount, ctx.__currentQuery) 34 | }) 35 | 36 | app.graphql.addHook('preValidation', (_, __, ctx) => { 37 | preValidationSpy(ctx.operationId, ctx.operationsCount, ctx.__currentQuery) 38 | }) 39 | 40 | app.graphql.addHook('preExecution', (_, __, ctx) => { 41 | preExecutionSpy(ctx.operationId, ctx.operationsCount, ctx.__currentQuery) 42 | }) 43 | 44 | app.graphql.addHook('onResolution', (_, ctx) => { 45 | onResolutionSpy(ctx.operationId, ctx.operationsCount, ctx.__currentQuery) 46 | }) 47 | 48 | await app.inject({ 49 | method: 'POST', 50 | url: '/graphql', 51 | body: [ 52 | { 53 | operationName: 'TestQuery', 54 | query: 'query TestQuery { test }' 55 | }, 56 | { 57 | operationName: 'DoubleQuery', 58 | query: 'query DoubleQuery { test }' 59 | } 60 | ] 61 | }) 62 | 63 | sinon.assert.calledTwice(preParsingSpy) 64 | sinon.assert.calledWith(preParsingSpy, 0, 2, sinon.match(/TestQuery/)) 65 | sinon.assert.calledWith(preParsingSpy, 1, 2, sinon.match(/DoubleQuery/)) 66 | 67 | sinon.assert.calledTwice(preValidationSpy) 68 | sinon.assert.calledWith(preValidationSpy, 0, 2, sinon.match(/TestQuery/)) 69 | sinon.assert.calledWith(preValidationSpy, 1, 2, sinon.match(/DoubleQuery/)) 70 | 71 | sinon.assert.calledTwice(preExecutionSpy) 72 | sinon.assert.calledWith(preExecutionSpy, 0, 2, sinon.match(/TestQuery/)) 73 | sinon.assert.calledWith(preExecutionSpy, 1, 2, sinon.match(/DoubleQuery/)) 74 | 75 | sinon.assert.calledTwice(onResolutionSpy) 76 | sinon.assert.calledWith(onResolutionSpy, 0, 2, sinon.match(/TestQuery/)) 77 | sinon.assert.calledWith(onResolutionSpy, 1, 2, sinon.match(/DoubleQuery/)) 78 | }) 79 | -------------------------------------------------------------------------------- /test/options.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const proxyquire = require('proxyquire') 4 | const sinon = require('sinon') 5 | const { test } = require('tap') 6 | const Fastify = require('fastify') 7 | 8 | const schema = ` 9 | type User { 10 | name: String! 11 | password: String! 12 | } 13 | 14 | type Query { 15 | read: [User] 16 | } 17 | ` 18 | 19 | const resolvers = { 20 | Query: { 21 | read: async (_, obj) => { 22 | return [ 23 | { 24 | name: 'foo', 25 | password: 'bar' 26 | } 27 | ] 28 | } 29 | } 30 | } 31 | 32 | test('call compileQuery with correct options if compilerOptions specified', async t => { 33 | const app = Fastify() 34 | t.teardown(() => app.close()) 35 | 36 | const compileQueryStub = sinon.stub() 37 | 38 | const GQL = proxyquire('../index', { 39 | 'graphql-jit': { 40 | compileQuery: compileQueryStub 41 | } 42 | }) 43 | 44 | await app.register(GQL, { 45 | schema, 46 | resolvers, 47 | jit: 1, 48 | compilerOptions: { 49 | customJSONSerializer: true 50 | } 51 | }) 52 | 53 | const queryStub = sinon.stub() 54 | 55 | compileQueryStub.returns({ 56 | query: queryStub 57 | }) 58 | 59 | queryStub.resolves({ errors: [] }) 60 | 61 | const query = `{ 62 | read { 63 | name 64 | password 65 | } 66 | }` 67 | 68 | // warm up the jit counter 69 | await app.inject({ 70 | method: 'POST', 71 | headers: { 'content-type': 'application/json', super: 'false' }, 72 | url: '/graphql', 73 | body: JSON.stringify({ query }) 74 | }) 75 | 76 | await app.inject({ 77 | method: 'POST', 78 | headers: { 'content-type': 'application/json', super: 'false' }, 79 | url: '/graphql', 80 | body: JSON.stringify({ query }) 81 | }) 82 | 83 | sinon.assert.calledOnceWithExactly(compileQueryStub, sinon.match.any, sinon.match.any, sinon.match.any, { customJSONSerializer: true }) 84 | }) 85 | -------------------------------------------------------------------------------- /test/plugin-definition.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const fp = require('fastify-plugin') 5 | const Fastify = require('fastify') 6 | const GQL = require('..') 7 | 8 | test('plugin name definition', async (t) => { 9 | const app = Fastify() 10 | const schema = ` 11 | type Query { 12 | add(x: Int, y: Int): Int 13 | } 14 | ` 15 | app.register(GQL, { schema }) 16 | app.register(fp(async (app, opts) => {}, { 17 | dependencies: ['mercurius'] 18 | })) 19 | 20 | t.resolves(app.ready()) 21 | }) 22 | -------------------------------------------------------------------------------- /test/reply-decorator.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const Fastify = require('fastify') 5 | const GQL = require('..') 6 | 7 | test('reply decorator', async (t) => { 8 | const app = Fastify() 9 | const schema = ` 10 | type Query { 11 | add(x: Int, y: Int): Int 12 | } 13 | ` 14 | t.teardown(app.close.bind(app)) 15 | 16 | const resolvers = { 17 | add: async ({ x, y }) => x + y 18 | } 19 | 20 | app.register(GQL, { 21 | schema, 22 | resolvers 23 | }) 24 | 25 | app.get('/', async function (req, reply) { 26 | const query = '{ add(x: 2, y: 2) }' 27 | return reply.graphql(query) 28 | }) 29 | 30 | const res = await app.inject({ 31 | method: 'GET', 32 | url: '/' 33 | }) 34 | 35 | t.same(JSON.parse(res.body), { 36 | data: { 37 | add: 4 38 | } 39 | }) 40 | }) 41 | 42 | test('reply decorator operationName', async (t) => { 43 | const app = Fastify() 44 | t.teardown(app.close.bind(app)) 45 | const schema = ` 46 | type Query { 47 | add(x: Int, y: Int): Int 48 | } 49 | ` 50 | 51 | const resolvers = { 52 | add: async ({ x, y }) => x + y 53 | } 54 | 55 | app.register(GQL, { 56 | schema, 57 | resolvers 58 | }) 59 | 60 | app.get('/', async function (req, reply) { 61 | const query = ` 62 | query MyQuery ($x: Int!, $y: Int!) { 63 | add(x: $x, y: $y) 64 | } 65 | 66 | query Double ($x: Int!) { 67 | add(x: $x, y: $x) 68 | } 69 | ` 70 | return reply.graphql(query, null, { 71 | x: 2, 72 | y: 1 // useless but we need it verify we call Double 73 | }, 'Double') 74 | }) 75 | 76 | const res = await app.inject({ 77 | method: 'GET', 78 | url: '/' 79 | }) 80 | 81 | t.same(JSON.parse(res.body), { 82 | data: { 83 | add: 4 84 | } 85 | }) 86 | }) 87 | 88 | test('reply decorator set status code to 400 with bad query', async (t) => { 89 | t.plan(3) 90 | 91 | const app = Fastify() 92 | t.teardown(app.close.bind(app)) 93 | const schema = ` 94 | type Query { 95 | add(x: Int, y: Int): Int 96 | } 97 | ` 98 | 99 | const resolvers = { 100 | add: async ({ x, y }) => x + y 101 | } 102 | 103 | app.register(GQL, { 104 | schema, 105 | resolvers 106 | }) 107 | 108 | app.setErrorHandler(async function (err, request, reply) { 109 | reply.code(err.statusCode) 110 | t.equal(err.statusCode, 400) 111 | return { errors: err.errors } 112 | }) 113 | 114 | app.get('/', function (req, reply) { 115 | const query = '{ add(x: 2, y: 2)' 116 | return reply.graphql(query) 117 | }) 118 | 119 | const res = await app.inject({ 120 | method: 'GET', 121 | url: '/' 122 | }) 123 | 124 | t.equal(res.statusCode, 400) 125 | t.same(res.json(), { 126 | errors: [ 127 | { 128 | message: 'Syntax Error: Expected Name, found .', 129 | locations: [ 130 | { 131 | line: 1, 132 | column: 18 133 | } 134 | ] 135 | } 136 | ] 137 | 138 | }) 139 | }) 140 | 141 | test('reply decorator supports encapsulation when loaders are defined in parent object', async (t) => { 142 | const app = Fastify() 143 | t.teardown(app.close.bind(app)) 144 | const schema = ` 145 | type Query { 146 | add(x: Int, y: Int): Int 147 | } 148 | ` 149 | const resolvers = { 150 | add: async ({ x, y }) => x + y 151 | } 152 | 153 | app.register(GQL, { 154 | schema, 155 | resolvers, 156 | loaders: {} 157 | }) 158 | 159 | app.register(async (app) => { 160 | const schema = ` 161 | type Query { 162 | multiply(x: Int, y: Int): Int 163 | } 164 | ` 165 | const resolvers = { 166 | multiply: async ({ x, y }) => x * y 167 | } 168 | 169 | app.register(GQL, { 170 | schema, 171 | resolvers, 172 | prefix: '/prefix' 173 | }) 174 | }) 175 | 176 | const res = await app.inject({ 177 | method: 'POST', 178 | url: '/prefix/graphql', 179 | payload: { 180 | query: '{ multiply(x: 5, y: 5) }' 181 | } 182 | }) 183 | 184 | t.equal(res.statusCode, 200) 185 | t.same(JSON.parse(res.body), { 186 | data: { 187 | multiply: 25 188 | } 189 | }) 190 | 191 | t.same(res.json(), { 192 | data: { 193 | multiply: 25 194 | } 195 | }) 196 | }) 197 | -------------------------------------------------------------------------------- /test/subscriber.js: -------------------------------------------------------------------------------- 1 | const { test } = require('tap') 2 | const mq = require('mqemitter') 3 | const { PubSub, SubscriptionContext } = require('../lib/subscriber') 4 | 5 | test('subscriber published an event', async (t) => { 6 | class MyQueue { 7 | push (value) { 8 | t.equal(value, 1) 9 | } 10 | } 11 | 12 | const s = new PubSub(mq()) 13 | s.subscribe('TOPIC', new MyQueue()) 14 | s.publish({ 15 | topic: 'TOPIC', 16 | payload: 1 17 | }, () => { 18 | t.pass() 19 | }) 20 | }) 21 | 22 | test('subscription context not throw error on close', t => { 23 | t.plan(1) 24 | const pubsub = new PubSub(mq()) 25 | 26 | const sc = new SubscriptionContext({ pubsub }) 27 | 28 | sc.close() 29 | t.pass() 30 | }) 31 | 32 | test('subscription context publish event returns a promise', t => { 33 | t.plan(1) 34 | const pubsub = new PubSub(mq()) 35 | 36 | const sc = new SubscriptionContext({ pubsub }) 37 | 38 | sc.subscribe('TOPIC') 39 | sc.publish({ 40 | topic: 'TOPIC', 41 | payload: 1 42 | }).then(() => { 43 | t.pass() 44 | }) 45 | }) 46 | 47 | test('subscription context publish event errs, error is catched', t => { 48 | t.plan(1) 49 | const emitter = mq() 50 | const pubsub = new PubSub(emitter) 51 | 52 | const fastifyMock = { 53 | log: { 54 | error () { 55 | t.pass() 56 | } 57 | } 58 | } 59 | const sc = new SubscriptionContext({ pubsub, fastify: fastifyMock }) 60 | 61 | sc.subscribe('TOPIC') 62 | emitter.close(() => {}) 63 | sc.publish({ 64 | topic: 'TOPIC', 65 | payload: 1 66 | }) 67 | }) 68 | 69 | test('subscription context publish event returns a promise reject on error', async t => { 70 | const emitter = mq() 71 | const error = new Error('Dummy error') 72 | emitter.on = (topic, listener, done) => done(error) 73 | 74 | const pubsub = new PubSub(emitter) 75 | const sc = new SubscriptionContext({ pubsub }) 76 | 77 | await t.rejects(sc.subscribe('TOPIC'), error) 78 | }) 79 | 80 | test('subscription context can handle multiple topics', t => { 81 | t.plan(4) 82 | 83 | const q = mq() 84 | const pubsub = new PubSub(q) 85 | const sc = new SubscriptionContext({ pubsub }) 86 | 87 | sc.subscribe(['TOPIC1', 'TOPIC2']) 88 | sc.publish({ 89 | topic: 'TOPIC1', 90 | payload: 1 91 | }).then(() => { 92 | t.pass() 93 | }) 94 | sc.publish({ 95 | topic: 'TOPIC2', 96 | payload: 2 97 | }).then(() => { 98 | t.pass() 99 | }) 100 | 101 | t.equal(q._matcher._trie.size, 2, 'Two listeners not found') 102 | sc.close() 103 | setImmediate(() => { t.equal(q._matcher._trie.size, 0, 'All listeners not removed') }) 104 | }) 105 | -------------------------------------------------------------------------------- /test/subscription-protocol.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { test } = require('tap') 3 | const { getProtocolByName } = require('../lib/subscription-protocol') 4 | 5 | test('getProtocolByName returns correct protocol message types', t => { 6 | t.plan(3) 7 | t.same(getProtocolByName('graphql-ws'), { 8 | GQL_CONNECTION_INIT: 'connection_init', 9 | GQL_CONNECTION_ACK: 'connection_ack', 10 | GQL_CONNECTION_ERROR: 'connection_error', 11 | GQL_CONNECTION_KEEP_ALIVE: 'ka', 12 | GQL_CONNECTION_TERMINATE: 'connection_terminate', 13 | GQL_START: 'start', 14 | GQL_DATA: 'data', 15 | GQL_ERROR: 'error', 16 | GQL_COMPLETE: 'complete', 17 | GQL_STOP: 'stop' 18 | }) 19 | t.same(getProtocolByName('graphql-transport-ws'), { 20 | GQL_CONNECTION_INIT: 'connection_init', 21 | GQL_CONNECTION_ACK: 'connection_ack', 22 | GQL_CONNECTION_ERROR: 'connection_error', 23 | GQL_CONNECTION_KEEP_ALIVE: 'ping', 24 | GQL_CONNECTION_KEEP_ALIVE_ACK: 'pong', 25 | GQL_CONNECTION_TERMINATE: 'connection_terminate', 26 | GQL_START: 'subscribe', 27 | GQL_DATA: 'next', 28 | GQL_ERROR: 'error', 29 | GQL_COMPLETE: 'complete', 30 | GQL_STOP: 'complete' 31 | }) 32 | t.equal(getProtocolByName('unsupported-protocol'), null) 33 | }) 34 | -------------------------------------------------------------------------------- /test/validation-rules.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { test } = require('tap') 4 | const Fastify = require('fastify') 5 | const { GraphQLError } = require('graphql') 6 | const GQL = require('..') 7 | 8 | const schema = ` 9 | type Query { 10 | add(x: Int, y: Int): Int 11 | } 12 | ` 13 | 14 | const resolvers = { 15 | Query: { 16 | add: async (_, obj) => { 17 | const { x, y } = obj 18 | return x + y 19 | } 20 | } 21 | } 22 | 23 | const query = '{ add(x: 2, y: 2) }' 24 | 25 | test('validationRules array - reports an error', async (t) => { 26 | const app = Fastify() 27 | 28 | app.register(GQL, { 29 | schema, 30 | resolvers, 31 | validationRules: [ 32 | // validation rule that reports an error 33 | function (context) { 34 | return { 35 | Document () { 36 | context.reportError(new GraphQLError('Validation rule error')) 37 | } 38 | } 39 | } 40 | ] 41 | }) 42 | 43 | // needed so that graphql is defined 44 | await app.ready() 45 | await t.rejects(app.graphql(query), { errors: [{ message: 'Validation rule error' }] }) 46 | }) 47 | 48 | test('validationRules array - passes when no errors', async (t) => { 49 | t.plan(1) 50 | const app = Fastify() 51 | 52 | app.register(GQL, { 53 | schema, 54 | resolvers, 55 | validationRules: [ 56 | // validation rule that reports no errors 57 | function (_context) { 58 | return { 59 | Document () { 60 | return false 61 | } 62 | } 63 | } 64 | ] 65 | }) 66 | 67 | // needed so that graphql is defined 68 | await app.ready() 69 | 70 | const res = await app.graphql(query) 71 | t.same(res, { data: { add: 4 } }) 72 | }) 73 | 74 | test('validationRules array - works with empty validationRules', async (t) => { 75 | t.plan(1) 76 | const app = Fastify() 77 | 78 | app.register(GQL, { 79 | schema, 80 | resolvers, 81 | validationRules: [] 82 | }) 83 | 84 | // needed so that graphql is defined 85 | await app.ready() 86 | 87 | const res = await app.graphql(query) 88 | t.same(res, { data: { add: 4 } }) 89 | }) 90 | 91 | test('validationRules - reports an error', async (t) => { 92 | const app = Fastify() 93 | 94 | app.register(GQL, { 95 | schema, 96 | resolvers, 97 | cache: false, 98 | validationRules: () => [ 99 | // validation rule that reports an error 100 | function (context) { 101 | return { 102 | Document () { 103 | context.reportError(new GraphQLError('Validation rule error')) 104 | } 105 | } 106 | } 107 | ] 108 | }) 109 | 110 | // needed so that graphql is defined 111 | await app.ready() 112 | await t.rejects(app.graphql(query), { errors: [{ message: 'Validation rule error' }] }) 113 | }) 114 | 115 | test('validationRules - passes when no errors', async (t) => { 116 | t.plan(1) 117 | const app = Fastify() 118 | 119 | app.register(GQL, { 120 | schema, 121 | resolvers, 122 | cache: false, 123 | validationRules: () => [ 124 | // validation rule that reports no errors 125 | function (_context) { 126 | return { 127 | Document () { 128 | return false 129 | } 130 | } 131 | } 132 | ] 133 | }) 134 | 135 | // needed so that graphql is defined 136 | await app.ready() 137 | 138 | const res = await app.graphql(query) 139 | t.same(res, { data: { add: 4 } }) 140 | }) 141 | 142 | test('validationRules - works with empty validationRules', async (t) => { 143 | t.plan(1) 144 | const app = Fastify() 145 | 146 | app.register(GQL, { 147 | schema, 148 | resolvers, 149 | cache: false, 150 | validationRules: () => [] 151 | }) 152 | 153 | // needed so that graphql is defined 154 | await app.ready() 155 | 156 | const res = await app.graphql(query) 157 | t.same(res, { data: { add: 4 } }) 158 | }) 159 | 160 | test('validationRules - works with missing validationRules', async (t) => { 161 | t.plan(1) 162 | const app = Fastify() 163 | 164 | app.register(GQL, { 165 | schema, 166 | resolvers, 167 | validationRules: undefined 168 | }) 169 | 170 | // needed so that graphql is defined 171 | await app.ready() 172 | 173 | const res = await app.graphql(query) 174 | t.same(res, { data: { add: 4 } }) 175 | }) 176 | 177 | test('validationRules - includes graphql request metadata', async (t) => { 178 | t.plan(4) 179 | const app = Fastify() 180 | 181 | const query = ` 182 | query Add ($x: Int!, $y: Int!) { 183 | add(x: $x, y: $y) 184 | } 185 | ` 186 | 187 | app.register(GQL, { 188 | schema, 189 | resolvers, 190 | cache: false, 191 | validationRules: function ({ source, variables, operationName }) { 192 | t.equal(source, query) 193 | t.same(variables, { x: 2, y: 2 }) 194 | t.same(operationName, 'Add') 195 | return [ 196 | // validation rule that reports no errors 197 | function (_context) { 198 | return { 199 | Document () { 200 | return false 201 | } 202 | } 203 | } 204 | ] 205 | } 206 | }) 207 | 208 | // needed so that graphql is defined 209 | await app.ready() 210 | 211 | const res = await app.graphql(query, null, { x: 2, y: 2 }, 'Add') 212 | t.same(res, { data: { add: 4 } }) 213 | }) 214 | 215 | test('validationRules - errors if cache is used with the function', async (t) => { 216 | const app = Fastify() 217 | 218 | app.register(GQL, { 219 | schema, 220 | resolvers, 221 | cache: true, 222 | validationRules: () => [] 223 | }) 224 | 225 | // needed so that graphql is defined 226 | await t.rejects(app.ready(), { message: 'Invalid options: Using a function for the validationRules is incompatible with query caching' }) 227 | }) 228 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "noEmit": true, 6 | "strict": true, 7 | "esModuleInterop": true 8 | }, 9 | "exclude": ["node_modules"], 10 | "files": [ 11 | "./test/types/index.ts" 12 | ] 13 | } 14 | --------------------------------------------------------------------------------