├── .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 | 
2 |
3 | # mercurius
4 |
5 | [](https://github.com/mercurius-js/mercurius/actions/workflows/ci.yml)
6 | [](https://www.npmjs.com/package/mercurius)
7 | [](https://www.npmjs.com/package/mercurius)
8 | [](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 |
17 |
{{ error }}
18 |
19 |
20 |
21 |
22 |
27 |
28 |
31 |
32 |
33 | {{ label }}
34 |
35 |
36 |
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 |
--------------------------------------------------------------------------------