├── .commitlintrc.json
├── .eslintignore
├── .eslintrc.js
├── .github
├── dependabot.yml
└── workflows
│ ├── ci.yml
│ └── codeql-analysis.yml
├── .gitignore
├── .husky
├── .gitignore
├── commit-msg
└── pre-commit
├── .npmignore
├── .prettierignore
├── .prettierrc
├── .release-it.json
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE
├── README.md
├── index.d.ts
├── index.js
├── index.ts
├── lib
├── base-mercurius.module.ts
├── constants.ts
├── decorators
│ ├── hook.decorator.ts
│ ├── index.ts
│ ├── resolve-loader.decorator.ts
│ └── resolve-reference-loader.decorator.ts
├── factories
│ ├── graphql.factory.ts
│ └── params.factory.ts
├── index.ts
├── interfaces
│ ├── base-mercurius-module-options.interface.ts
│ ├── index.ts
│ ├── loader.interface.ts
│ ├── mercurius-gateway-module-options.interface.ts
│ ├── mercurius-module-options.interface.ts
│ └── reference.interface.ts
├── mercurius-core.module.ts
├── mercurius-gateway.module.ts
├── mercurius.module.ts
├── services
│ ├── gql-arguments-host.ts
│ ├── gql-execution-context.ts
│ ├── hook-explorer.service.ts
│ ├── index.ts
│ ├── loaders-explorer.service.ts
│ └── pub-sub-host.ts
└── utils
│ ├── decorate-loader-resolver.util.ts
│ ├── extract-hook-metadata.util.ts
│ ├── extract-loader-metadata.util.ts
│ ├── faderation-factory.util.ts
│ ├── is-loader-context.ts
│ ├── merge-defaults.ts
│ └── to-async-iterator.ts
├── package.json
├── tests
├── code-first
│ ├── animal.interface.ts
│ ├── app.module.ts
│ ├── directives
│ │ └── upper-case.directive.ts
│ ├── inputs
│ │ ├── create-cat.input.ts
│ │ └── create-dog.input.ts
│ ├── main.ts
│ ├── resolvers
│ │ ├── animal.resolver.ts
│ │ └── cat.resolver.ts
│ ├── services
│ │ ├── cat.service.ts
│ │ └── dog.service.ts
│ ├── species.enum.ts
│ └── types
│ │ ├── cat.ts
│ │ └── dog.ts
├── e2e
│ ├── code-first-federation.spec.ts
│ ├── code-first.spec.ts
│ ├── gateway.spec.ts
│ ├── graphql-async-class.spec.ts
│ ├── graphql-async.spec.ts
│ ├── graphql-request-scoped.spec.ts
│ ├── graphql.spec.ts
│ └── hook.spec.ts
├── example
│ ├── app.controller.ts
│ ├── app.module.ts
│ ├── decorators
│ │ └── header.decorator.ts
│ ├── directives
│ │ └── upper-case.directive.ts
│ ├── filters
│ │ └── forbidden-exception.filter.ts
│ ├── guards
│ │ └── auth.guard.ts
│ ├── interceptors
│ │ └── log.interceptor.ts
│ ├── main.ts
│ ├── modules
│ │ └── user
│ │ │ ├── inputs
│ │ │ ├── create-user.input.ts
│ │ │ └── full-name.args.ts
│ │ │ ├── resolvers
│ │ │ ├── customer.resolver.ts
│ │ │ ├── person.resolver.ts
│ │ │ ├── search.resolver.ts
│ │ │ └── user.resolver.ts
│ │ │ ├── services
│ │ │ ├── customer.service.ts
│ │ │ ├── post.service.ts
│ │ │ └── user.service.ts
│ │ │ └── user.module.ts
│ ├── resolvers
│ │ └── image.resolver.ts
│ ├── scalars
│ │ └── hash.scalar.ts
│ └── types
│ │ ├── customer.type.ts
│ │ ├── person.interface.ts
│ │ ├── post.type.ts
│ │ └── user.type.ts
├── federation
│ ├── gateway.mjs
│ ├── gateway
│ │ ├── app.module.ts
│ │ └── main.ts
│ ├── postService
│ │ ├── app.module.ts
│ │ ├── main.ts
│ │ ├── post.resolver.ts
│ │ ├── post.ts
│ │ ├── user.resolver.ts
│ │ └── user.ts
│ └── userService
│ │ ├── app.module.ts
│ │ ├── main.ts
│ │ ├── user.resolver.ts
│ │ └── user.ts
├── graphql
│ ├── app.module.ts
│ ├── async-options-class.module.ts
│ ├── async-options.module.ts
│ ├── cats
│ │ ├── cats-request-scoped.service.ts
│ │ ├── cats.guard.ts
│ │ ├── cats.module.ts
│ │ ├── cats.resolvers.ts
│ │ ├── cats.service.ts
│ │ ├── cats.types.graphql
│ │ └── interfaces
│ │ │ └── cat.interface.ts
│ ├── common
│ │ └── scalars
│ │ │ └── date.scalar.ts
│ ├── config.module.ts
│ ├── config.service.ts
│ ├── hello
│ │ ├── cats.types.graphql
│ │ ├── dto
│ │ │ └── test.dto.ts
│ │ ├── guards
│ │ │ └── request-scoped.guard.ts
│ │ ├── hello.module.ts
│ │ ├── hello.resolver.ts
│ │ ├── hello.service.ts
│ │ ├── interceptors
│ │ │ └── logging.interceptor.ts
│ │ └── users
│ │ │ ├── user-by-id.pipe.ts
│ │ │ └── users.service.ts
│ └── main.ts
├── nest-cli.json
├── schema.graphql
├── tsconfig.build.json
├── tsconfig.json
└── utils
│ └── create-test-client.ts
├── tsconfig.json
└── yarn.lock
/.commitlintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@commitlint/config-angular"],
3 | "rules": {
4 | "subject-case": [
5 | 2,
6 | "always",
7 | ["sentence-case", "start-case", "pascal-case", "upper-case", "lower-case"]
8 | ],
9 | "type-enum": [
10 | 2,
11 | "always",
12 | [
13 | "build",
14 | "chore",
15 | "ci",
16 | "docs",
17 | "feat",
18 | "fix",
19 | "perf",
20 | "refactor",
21 | "revert",
22 | "style",
23 | "test",
24 | "sample"
25 | ]
26 | ]
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | tests/**
2 | example/**
3 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: 'tsconfig.json',
5 | sourceType: 'module',
6 | },
7 | plugins: ['@typescript-eslint/eslint-plugin'],
8 | extends: [
9 | 'plugin:@typescript-eslint/eslint-recommended',
10 | 'plugin:@typescript-eslint/recommended',
11 | 'prettier',
12 | ],
13 | root: true,
14 | env: {
15 | node: true,
16 | jest: true,
17 | },
18 | rules: {
19 | '@typescript-eslint/interface-name-prefix': 'off',
20 | '@typescript-eslint/explicit-function-return-type': 'off',
21 | '@typescript-eslint/no-explicit-any': 'off',
22 | '@typescript-eslint/no-use-before-define': 'off',
23 | '@typescript-eslint/no-unused-vars': 'off',
24 | '@typescript-eslint/explicit-module-boundary-types': 'off',
25 | '@typescript-eslint/ban-types': 'off',
26 | },
27 | };
28 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 | - package-ecosystem: "npm" # See documentation for possible values
9 | directory: "/" # Location of package manifests
10 | open-pull-requests-limit: 10
11 | schedule:
12 | interval: "daily"
13 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 |
2 | name: CI workflow
3 | on: [push, pull_request]
4 | jobs:
5 | test:
6 | runs-on: ubuntu-latest
7 | strategy:
8 | matrix:
9 | node-version: [12, 14, 16]
10 | steps:
11 | - uses: actions/checkout@v2
12 | - name: Use Node.js ${{ matrix.node-version }}
13 | uses: actions/setup-node@v2
14 | with:
15 | node-version: ${{ matrix.node-version }}
16 | - name: Cache node_modules
17 | uses: actions/cache@v2
18 | with:
19 | path: '**/node_modules'
20 | key: ${{ runner.os }}-${{ matrix.node-version }}-modules-${{ hashFiles('**/yarn.lock') }}
21 | - name: Install Dependencies
22 | run: yarn install --frozen-lockfile
23 | - name: Lint
24 | run: yarn lint
25 | - name: Build
26 | run: yarn build
27 | - name: Test
28 | run: yarn test
29 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ master ]
17 | pull_request:
18 | # The branches below must be a subset of the branches above
19 | branches: [ master ]
20 | schedule:
21 | - cron: '25 7 * * 1'
22 |
23 | jobs:
24 | analyze:
25 | name: Analyze
26 | runs-on: ubuntu-latest
27 |
28 | strategy:
29 | fail-fast: false
30 | matrix:
31 | language: [ 'javascript' ]
32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
33 | # Learn more:
34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
35 |
36 | steps:
37 | - name: Checkout repository
38 | uses: actions/checkout@v2
39 |
40 | # Initializes the CodeQL tools for scanning.
41 | - name: Initialize CodeQL
42 | uses: github/codeql-action/init@v1
43 | with:
44 | languages: ${{ matrix.language }}
45 | # If you wish to specify custom queries, you can do so here or in a config file.
46 | # By default, queries listed here will override any specified in a config file.
47 | # Prefix the list here with "+" to use these queries and those in the config file.
48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main
49 |
50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
51 | # If this step fails, then you should remove it and run the build manually (see below)
52 | - name: Autobuild
53 | uses: github/codeql-action/autobuild@v1
54 |
55 | # ℹ️ Command-line programs to run using the OS shell.
56 | # 📚 https://git.io/JvXDl
57 |
58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
59 | # and modify them (or add more) to build your code if your project
60 | # uses a compiled language
61 |
62 | #- run: |
63 | # make bootstrap
64 | # make release
65 |
66 | - name: Perform CodeQL Analysis
67 | uses: github/codeql-action/analyze@v1
68 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # dependencies
2 | /node_modules
3 |
4 | # IDE
5 | /.idea
6 | /.awcache
7 | /.vscode
8 |
9 | # misc
10 | npm-debug.log
11 | .DS_Store
12 |
13 | # tests
14 | /test
15 | /coverage
16 | /.nyc_output
17 |
18 | # dist
19 | dist
20 |
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | commitlint -c .commitlintrc.json --edit $1
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | ./node_modules/.bin/lint-staged
5 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # source
2 | .idea
3 | lib
4 | index.ts
5 | package-lock.json
6 | yarn.lock
7 | .eslintrc.js
8 | .eslintignore
9 | tsconfig.json
10 | .prettierrc
11 | .commitlintrc.json
12 | .release-it.json
13 | tests
14 | .husky
15 | node_modules
16 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | tests/generated-definitions/*.ts
2 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "all",
3 | "singleQuote": true
4 | }
5 |
--------------------------------------------------------------------------------
/.release-it.json:
--------------------------------------------------------------------------------
1 | {
2 | "git": {
3 | "commitMessage": "chore(): release v${version}"
4 | },
5 | "github": {
6 | "release": true
7 | },
8 | "plugins": {
9 | "@release-it/conventional-changelog": {
10 | "preset": "angular",
11 | "infile": "CHANGELOG.md"
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # [0.21.0](https://github.com/Davide-Gheri/nestjs-mercurius/compare/0.20.1...0.21.0) (2021-11-05)
2 |
3 | * Nestjs 9 support
4 | * Updated Mercurius to v8.8
5 | * Replaced @apollo/federation with @apollo/subgraph
6 |
7 | ## [0.20.1](https://github.com/Davide-Gheri/nestjs-mercurius/compare/0.20.0...0.20.1) (2021-07-19)
8 |
9 |
10 | ### Bug Fixes
11 |
12 | * npm dependency fixes ([656a784](https://github.com/Davide-Gheri/nestjs-mercurius/commit/656a784108a7bf93a6d73a7e355729182e2508f3))
13 |
14 | # [0.20.0](https://github.com/Davide-Gheri/nestjs-mercurius/compare/0.10.1...0.20.0) (2021-07-13)
15 |
16 | * Nestjs 8 support
17 | * Mercurius 8 support
18 |
19 | ## [0.10.1](https://github.com/Davide-Gheri/nestjs-mercurius/compare/0.10.0...0.10.1) (2021-06-21)
20 |
21 | # [0.10.0](https://github.com/Davide-Gheri/nestjs-mercurius/compare/0.9.2...0.10.0) (2021-06-16)
22 |
23 | ## [0.9.2](https://github.com/Davide-Gheri/nestjs-mercurius/compare/0.9.1...0.9.2) (2021-05-25)
24 |
25 |
26 | ### Bug Fixes
27 |
28 | * **federation-factory:** implement workaround for federated schema ([ed5ed68](https://github.com/Davide-Gheri/nestjs-mercurius/commit/ed5ed687225e86314679bae8b890268e93768cf7))
29 |
30 | ## [0.9.1](https://github.com/Davide-Gheri/nestjs-mercurius/compare/0.9.0...0.9.1) (2021-05-25)
31 |
32 |
33 | ### Bug Fixes
34 |
35 | * **factory:** return undefined if no validation rules are provided ([326965c](https://github.com/Davide-Gheri/nestjs-mercurius/commit/326965c93d4d7226648bc14f92689acd7c4bf2dd))
36 |
37 | # [0.9.0](https://github.com/Davide-Gheri/nestjs-mercurius/compare/0.8.1...0.9.0) (2021-05-11)
38 |
39 |
40 | ### Bug Fixes
41 |
42 | * **gateway:** wrong function call on gateway options factory ([675d4e1](https://github.com/Davide-Gheri/nestjs-mercurius/commit/675d4e1673fccaf1cfd8c7770a5035fc02e2f766))
43 | * Fixed import ([9f4b972](https://github.com/Davide-Gheri/nestjs-mercurius/commit/9f4b97203b703096dd788fc3c383007b830aa5dc))
44 |
45 | ## [0.8.1](https://github.com/Davide-Gheri/nestjs-mercurius/compare/0.8.0...0.8.1) (2021-04-13)
46 |
47 |
48 | ### Bug Fixes
49 |
50 | * **gateway:** hookservice dependencies ([85f1cf8](https://github.com/Davide-Gheri/nestjs-mercurius/commit/85f1cf81495a419a44cfb295cb724f6b63061518))
51 |
52 | # [0.8.0](https://github.com/Davide-Gheri/nestjs-mercurius/compare/0.7.0...0.8.0) (2021-03-23)
53 |
54 |
55 | ### Bug Fixes
56 |
57 | * **loader:** correct interface loader inherit ([6e6357f](https://github.com/Davide-Gheri/nestjs-mercurius/commit/6e6357fb0a635a988d6bd6dfeae6eb396acd475a))
58 |
59 | # [0.7.0](https://github.com/Davide-Gheri/nestjs-mercurius/compare/0.6.0...0.7.0) (2021-03-17)
60 |
61 |
62 | ### Bug Fixes
63 |
64 | * **loader:** remove interface loaders as it breaks resolve type fn ([c3de299](https://github.com/Davide-Gheri/nestjs-mercurius/commit/c3de299ddf8249792f79db9fd4723581c8c1cc61))
65 |
66 | # [0.6.0](https://github.com/Davide-Gheri/nestjs-mercurius/compare/0.5.1...0.6.0) (2021-02-23)
67 |
68 |
69 | ### Features
70 |
71 | * **hooks:** add mercurius hooks support ([23b4f3a](https://github.com/Davide-Gheri/nestjs-mercurius/commit/23b4f3a670786ab89310653ea9d6549eaaa4bfa9))
72 |
73 | ## [0.5.1](https://github.com/Davide-Gheri/nestjs-mercurius/compare/0.5.0...0.5.1) (2021-02-22)
74 |
75 |
76 | ### Bug Fixes
77 |
78 | * removed peer dep typings ([d448836](https://github.com/Davide-Gheri/nestjs-mercurius/commit/d448836a0d42293be102c01358cdcc74e8cb5684))
79 |
80 | # [0.5.0](https://github.com/Davide-Gheri/nestjs-mercurius/compare/0.5.0-beta.1...0.5.0) (2021-02-22)
81 |
82 | * Merge pull request #18 from Davide-Gheri/feature/interface-loaders (8d92191)
83 | * Merge branch 'master' into feature/interface-loaders (ea2f035)
84 | * Merge pull request #19 from Davide-Gheri/dependabot/npm_and_yarn/eslint-config-prettier-8.0.0 (79529a4)
85 | * Merge pull request #22 from Davide-Gheri/dependabot/npm_and_yarn/release-it-14.4.1 (7e92d1d)
86 | * Merge pull request #21 from Davide-Gheri/dependabot/npm_and_yarn/mercurius-7.1.0 (f14cb3f)
87 | * Merge pull request #20 from Davide-Gheri/dependabot/npm_and_yarn/husky-5.1.0 (5f3cbd7)
88 | * fix: prettier (0c0891b)
89 | * chore(deps-dev): bump release-it from 14.4.0 to 14.4.1 (1a52dd1)
90 | * chore(deps-dev): bump mercurius from 7.0.0 to 7.1.0 (c517821)
91 | * chore(deps-dev): bump husky from 5.0.9 to 5.1.0 (dd36ef3)
92 | * chore(deps-dev): bump eslint-config-prettier from 7.2.0 to 8.0.0 (804078d)
93 | * test(loaders): interface defined loader (1f3b6c4)
94 | * feat(loaders): types inherits interface defined loaders (d47e232)
95 | * chore: removed unused mercurius param type enum (53aeddf)
96 | * Update README.md (7b7083b)
97 | * Merge branch 'master' of github.com:Davide-Gheri/nestjs-mercurius (7e31570)
98 | * chore: moved optional deps to peer (7fe4bb7)
99 | * Merge pull request #17 from Davide-Gheri/add-license-1 (6d62a0c)
100 | * Create CODE_OF_CONDUCT.md (7503211)
101 | * Create LICENSE (52e583c)
102 |
103 | # [0.5.0-beta.0](https://github.com/Davide-Gheri/nestjs-mercurius/compare/0.4.0...0.5.0-beta.0) (2021-02-19)
104 |
105 |
106 | ### Bug Fixes
107 |
108 | * merge error ([23d0ced](https://github.com/Davide-Gheri/nestjs-mercurius/commit/23d0cedc728ee9218cdf8463617ea8445c4493eb))
109 |
110 |
111 | ### Features
112 |
113 | * **federation:** define federation service ([688cea2](https://github.com/Davide-Gheri/nestjs-mercurius/commit/688cea22edb6bdc1e4e0de049005f1737ea83563))
114 | * **federation:** gateway module ([5c835f0](https://github.com/Davide-Gheri/nestjs-mercurius/commit/5c835f067cd996c9a6faab5e026441ccde38cb1e))
115 | * **federation:** need a custom graphql factory ([e2b1bf8](https://github.com/Davide-Gheri/nestjs-mercurius/commit/e2b1bf874408270ef8cd8a06a711a866ed9e7bab))
116 | * **federation:** need a custom graphql factory ([989aace](https://github.com/Davide-Gheri/nestjs-mercurius/commit/989aace729987e437793f699eda81f41912c9ea4))
117 |
118 | # [0.4.0](https://github.com/Davide-Gheri/nestjs-mercurius/compare/0.3.0...0.4.0) (2021-02-19)
119 |
120 |
121 | ### Features
122 |
123 | * **pubsub:** global pubsub host' ([064640b](https://github.com/Davide-Gheri/nestjs-mercurius/commit/064640b5c64a68c21456810864bfcf59d7c2e76c))
124 |
125 | # [0.3.0](https://github.com/Davide-Gheri/nestjs-mercurius/compare/0.2.0...0.3.0) (2021-02-18)
126 |
127 |
128 | ### Features
129 |
130 | * add opts object to loaders ([2a94abf](https://github.com/Davide-Gheri/nestjs-mercurius/commit/2a94abfd6bca0f0f6b16a96da0d19e3b09836c4f))
131 | * ResolveLoader accepts optional opts object ([5ad6dfe](https://github.com/Davide-Gheri/nestjs-mercurius/commit/5ad6dfeb45b9213c1fbc041fe0be236d679ba32f))
132 |
133 | # [0.2.0](https://github.com/Davide-Gheri/nestjs-mercurius/compare/0.1.0...0.2.0) (2021-02-18)
134 |
135 |
136 | ### Features
137 |
138 | * loader middleware, testing unions/enums/interfaces ([35e25e3](https://github.com/Davide-Gheri/nestjs-mercurius/commit/35e25e384a8951a80816ada6f72ad0c3d323a56b))
139 |
140 | # [0.1.0](https://github.com/Davide-Gheri/nestjs-mercurius/compare/0.1.0-beta.1...0.1.0) (2021-02-17)
141 |
142 | * test: resolvers merging with third party resolvers (58184dc)
143 | * Merge pull request #4 from Davide-Gheri/feature/query-complexity (881952f)
144 | * ci: run only on 1 os (5b740c4)
145 | * ci: github workflow on push and pr (244d6a2)
146 | * Merge pull request #3 from Davide-Gheri/feature/enhancers (7c2a57b)
147 | * test: testing scalars (3c05f39)
148 | * feat: validation rules (4b50148)
149 | * Merge branch 'feature/enhancers' of github.com:Davide-Gheri/nestjs-mercurius into feature/enhancers (3c586b2)
150 | * feat: nestjs enhanchers (guards, filters, interceptors) (888439b)
151 | * feat: nestjs enhanchers (guards, filters, interceptors) (71bb07b)
152 | * refactor(loader): simplified loader decorators (ace3606)
153 |
154 | # 0.1.0-beta.0 (2021-02-14)
155 |
156 |
157 | ### Features
158 |
159 | * support Subscriptions ([235e5ed](https://github.com/Davide-Gheri/nestjs-mercurius/commit/235e5ed6bd0ea78082a742bcc5bda07a83c126ed))
160 | * support Subscriptions ([aaeb124](https://github.com/Davide-Gheri/nestjs-mercurius/commit/aaeb12494e1012ed2b143e90b0c6a0fc7922f3d6))
161 | * upload ([450d997](https://github.com/Davide-Gheri/nestjs-mercurius/commit/450d99798a3f663dbd7a9a1d4651e7652595722c))
162 |
163 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at davide@davidegheri.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Davide Gheri
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 | If you want to use Mercurius with @nestjs/graphql > v10, please use the @nestjs/mercurius package
3 |
4 |
5 |
6 | # Nestjs Mercurius
7 |
8 | Use [Mercurius GraphQL](https://github.com/mercurius-js/mercurius) with Nestjs framework
9 |
10 | Visit the [Wiki](https://github.com/Davide-Gheri/nestjs-mercurius/wiki)
11 |
12 | ## Install
13 |
14 | ```bash
15 | npm i @nestjs/platform-fastify fastify mercurius nestjs-mercurius
16 | ```
17 |
18 | ## Use
19 |
20 | ### Register the module
21 | ```typescript
22 | import { Module } from '@nestjs/common';
23 | import { MercuriusModule } from 'nestjs-mercurius';
24 |
25 | @Module({
26 | imports: [
27 | // Work also with async configuration (MercuriusModule.forRootAsync)
28 | MercuriusModule.forRoot({
29 | autoSchemaFile: true,
30 | context: (request, reply) => ({
31 | user: request.user,
32 | }),
33 | subscription: {
34 | context: (connection, request) => ({
35 | user: request.user,
36 | }),
37 | },
38 | }),
39 | ],
40 | providers: [
41 | CatResolver,
42 | ],
43 | })
44 | export class AppModule {}
45 | ```
46 |
47 | ### The Object type
48 |
49 | ```typescript
50 | import { Field, ID, ObjectType } from '@nestjs/graphql';
51 |
52 | @ObjectType()
53 | export class Cat {
54 | @Field(() => ID)
55 | id: number;
56 |
57 | @Field()
58 | name: string;
59 |
60 | @Field(() => Int)
61 | ownerId: number;
62 | }
63 | ```
64 |
65 | ### The Resolver
66 |
67 | ```typescript
68 | import { Resolver, Query, ResolveField, Parent, Mutation, Subscription, Context, Args } from '@nestjs/graphql';
69 | import { ParseIntPipe } from '@nestjs/common';
70 | import { ResolveLoader, toAsyncIterator, LoaderQuery } from 'nestjs-mercurius';
71 | import { PubSub } from 'mercurius';
72 | import { Cat } from './cat';
73 |
74 | @Resolver(() => Cat)
75 | export class CatResolver {
76 | constructor(
77 | private readonly catService: CatService,
78 | private readonly userService: UserService,
79 | ) {}
80 |
81 | @Query(() => [Cat])
82 | cats(@Args({name: 'filter', type: () => String, nullable: true}) filter?: string) {
83 | return this.catService.find(filter);
84 | }
85 |
86 | @Query(() => Cat, { nullable: true })
87 | cat(@Args('id', ParseIntPipe) id: number) {
88 | return this.catService.findOne(id);
89 | }
90 |
91 | @Mutation(() => Cat)
92 | createCat(
93 | @Args('name') name: string,
94 | @Context('pubsub') pubSub: PubSub,
95 | @Context('user') user: User,
96 | ) {
97 | const cat = new Cat();
98 | cat.name = name;
99 | cat.ownerId = user.id;
100 | //...
101 | pubSub.publish({
102 | topic: 'CatCreated',
103 | payload: { cat },
104 | });
105 | return cat;
106 | }
107 |
108 | @Subscription(() => Cat, {
109 | resolve: (payload) => payload.cat,
110 | filter: (payload, vars, context) =>
111 | payload.cat.ownerId !== context.user.id,
112 | })
113 | onCatCreated(
114 | @Context('pubsub') pubSub: PubSub,
115 | ) {
116 | return toAsyncIterator(pubSub.subscribe('CatCreated'));
117 | }
118 |
119 | @ResolveField(() => Int)
120 | age(@Parent() cat: Cat) {
121 | return 5;
122 | }
123 |
124 | @ResolveLoader(() => User, { opts: { cache: false } })
125 | owner(
126 | @Parent() queries: LoaderQuery[],
127 | ) {
128 | return this.userService.findById(
129 | // queries is an array of objects defined as { obj, params } where obj is the current object and params are the GraphQL params
130 | queries.map(({ obj }) => obj.ownerId)
131 | );
132 | }
133 | }
134 | ```
135 |
136 | ## Federation
137 |
138 | Install necessary dependencies
139 | ```typescript
140 | npm i @apollo/federation
141 | ```
142 |
143 | ### The Gateway
144 |
145 | ```typescript
146 | import { Module } from '@nestjs/common';
147 | import { MercuriusGatewayModule } from 'nestjs-mercurius';
148 |
149 | @Module({
150 | imports: [
151 | MercuriusGatewayModule.forRoot({
152 | graphiql: 'playground',
153 | subscription: true,
154 | gateway: {
155 | pollingInterval: 10000,
156 | services: [
157 | {
158 | name: 'users',
159 | url: 'https://....',
160 | wsUrl: 'wss://...',
161 | },
162 | {
163 | name: 'pets',
164 | url: 'https://...',
165 | rewriteHeaders: headers => headers,
166 | },
167 | ],
168 | },
169 | }),
170 | ],
171 | })
172 | export class GatewayModule {}
173 | ```
174 |
175 | ### The Service
176 |
177 | ```typescript
178 | import { Module } from '@nestjs/common';
179 | import { MercuriusModule } from './mercurius.module';
180 | import { User } from './user';
181 | import { PetResolver, UserResolver } from './resolvers';
182 |
183 | @Module({
184 | imports: [
185 | MercuriusModule.forRoot({
186 | autoSchemaFile: true,
187 | federationMetadata: true,
188 | buildSchemaOptions: {
189 | orphanedTypes: [User],
190 | },
191 | //...
192 | }),
193 | ],
194 | providers: [
195 | PetResolver,
196 | UserResolver,
197 | ],
198 | })
199 | export class PetModule {}
200 | ```
201 |
202 | ### The Resolver
203 |
204 | ```typescript
205 | import { Resolver, ResolveReference } from '@nestjs/graphql';
206 | import { Pet } from './pet';
207 | import { Reference } from './reference.interface';
208 |
209 | @Resolver(() => Pet)
210 | export class PetResolver {
211 | constructor(
212 | private readonly petService: PetService,
213 | ) {}
214 |
215 | @ResolveReference()
216 | resolveReference(ref: Reference<'Pet', 'id'>) {
217 | return this.petService.findOne(ref.id);
218 | }
219 | }
220 | ```
221 |
222 | Resolve reference could also be defined as Loader, potentially improving performance:
223 |
224 | ```typescript
225 | import { ResolveReferenceLoader } from './resolve-reference-loader.decorator';
226 | import { LoaderQuery } from './loader.interface';
227 |
228 | @Resolver(() => Pet)
229 | export class PetResolver {
230 | constructor(
231 | private readonly petService: PetService,
232 | ) {}
233 |
234 | @ResolveReferenceLoader()
235 | resolveReference(refs: LoaderQuery>) {
236 | return this.petService.findById(
237 | refs.map(({ obj }) => obj.id)
238 | );
239 | }
240 | }
241 | ```
242 |
243 | ## Hooks
244 |
245 | Register mercurius hooks as service methods, using the `@GraphQLHook()` decorator
246 |
247 | ```typescript
248 | import { GraphQLHook } from 'nestjs-mercurius';
249 |
250 | @Injectable()
251 | export class HookService {
252 | @GraphQLHook('preValidation')
253 | async onPreValidation(schema: GraphQLSchema, source: DocumentNode, context: any) {
254 | //...
255 | }
256 | }
257 | ```
258 |
259 |
260 |
--------------------------------------------------------------------------------
/index.d.ts:
--------------------------------------------------------------------------------
1 | export * from './dist';
2 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 | function __export(m) {
3 | for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p];
4 | }
5 | exports.__esModule = true;
6 | __export(require("./dist"));
7 |
--------------------------------------------------------------------------------
/index.ts:
--------------------------------------------------------------------------------
1 | export * from './dist';
2 |
--------------------------------------------------------------------------------
/lib/base-mercurius.module.ts:
--------------------------------------------------------------------------------
1 | import { loadPackage } from '@nestjs/common/utils/load-package.util';
2 | import { FastifyInstance } from 'fastify';
3 | import { normalizeRoutePath } from '@nestjs/graphql/dist/utils';
4 | import { ApplicationConfig, HttpAdapterHost } from '@nestjs/core';
5 | import { BaseMercuriusModuleOptions } from './interfaces/base-mercurius-module-options.interface';
6 | import { HookExplorerService } from './services';
7 |
8 | export abstract class BaseMercuriusModule<
9 | Opts extends BaseMercuriusModuleOptions,
10 | > {
11 | constructor(
12 | protected readonly httpAdapterHost: HttpAdapterHost,
13 | protected readonly applicationConfig: ApplicationConfig,
14 | protected readonly options: Opts,
15 | protected readonly hookExplorerService: HookExplorerService,
16 | ) {}
17 |
18 | protected async registerGqlServer(mercuriusOptions: Opts) {
19 | const httpAdapter = this.httpAdapterHost.httpAdapter;
20 | const platformName = httpAdapter.getType();
21 |
22 | if (platformName === 'fastify') {
23 | await this.registerFastify(mercuriusOptions);
24 | } else {
25 | throw new Error(`No support for current HttpAdapter: ${platformName}`);
26 | }
27 | }
28 |
29 | protected async registerFastify(mercuriusOptions: Opts) {
30 | const mercurius = loadPackage('mercurius', 'MercuriusModule', () =>
31 | require('mercurius'),
32 | );
33 |
34 | const httpAdapter = this.httpAdapterHost.httpAdapter;
35 | const app: FastifyInstance = httpAdapter.getInstance();
36 |
37 | const options = {
38 | ...mercuriusOptions,
39 | path: this.getNormalizedPath(mercuriusOptions),
40 | };
41 |
42 | if (mercuriusOptions.uploads) {
43 | const mercuriusUpload = loadPackage(
44 | 'mercurius-upload',
45 | 'MercuriusModule',
46 | () => require('mercurius-upload'),
47 | );
48 | await app.register(
49 | mercuriusUpload,
50 | typeof mercuriusOptions.uploads !== 'boolean'
51 | ? mercuriusOptions.uploads
52 | : undefined,
53 | );
54 | }
55 |
56 | if (mercuriusOptions.altair) {
57 | const altairPlugin = loadPackage(
58 | 'altair-fastify-plugin',
59 | 'MercuriusModule',
60 | () => require('altair-fastify-plugin'),
61 | );
62 |
63 | options.graphiql = false;
64 | options.ide = false;
65 |
66 | await app.register(altairPlugin, {
67 | baseURL: '/altair/',
68 | path: '/altair',
69 | ...(typeof mercuriusOptions.altair !== 'boolean' &&
70 | mercuriusOptions.altair),
71 | endpointURL: options.path,
72 | });
73 | }
74 |
75 | await app.register(mercurius, options);
76 |
77 | this.addHooks(app);
78 | }
79 |
80 | protected getNormalizedPath(mercuriusOptions: Opts): string {
81 | const prefix = this.applicationConfig.getGlobalPrefix();
82 | const useGlobalPrefix = prefix && this.options.useGlobalPrefix;
83 | const gqlOptionsPath = normalizeRoutePath(mercuriusOptions.path);
84 | return useGlobalPrefix
85 | ? normalizeRoutePath(prefix) + gqlOptionsPath
86 | : gqlOptionsPath;
87 | }
88 |
89 | protected addHooks(app: FastifyInstance) {
90 | const hooks = this.hookExplorerService.explore();
91 |
92 | hooks.forEach((hook) => {
93 | app.graphql.addHook(hook.name as any, hook.callback as any);
94 | });
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/lib/constants.ts:
--------------------------------------------------------------------------------
1 | export const LOADER_NAME_METADATA = 'mercurius:loader_name';
2 | export const LOADER_PROPERTY_METADATA = 'mercurius:loader_property';
3 | export const REFERENCE_LOADER_METADATA = 'mercurius:reference_loader';
4 |
5 | export const VALIDATOR_METADATA = 'mercurius:validator';
6 |
7 | export const HOOK_METADATA = 'mercurius:hook';
8 |
--------------------------------------------------------------------------------
/lib/decorators/hook.decorator.ts:
--------------------------------------------------------------------------------
1 | import { SetMetadata } from '@nestjs/common';
2 | import { HOOK_METADATA } from '../constants';
3 | import {
4 | onResolutionHookHandler,
5 | preExecutionHookHandler,
6 | preGatewayExecutionHookHandler,
7 | preParsingHookHandler,
8 | preValidationHookHandler,
9 | } from 'mercurius';
10 |
11 | export type HookName =
12 | | 'preParsing'
13 | | 'preValidation'
14 | | 'preExecution'
15 | | 'preGatewayExecution'
16 | | 'onResolution';
17 |
18 | export type HookMap = {
19 | [K in HookName]: K extends 'preParsing'
20 | ? preParsingHookHandler
21 | : K extends 'preValidation'
22 | ? preValidationHookHandler
23 | : K extends 'preExecution'
24 | ? preExecutionHookHandler
25 | : K extends 'preGatewayExecution'
26 | ? preGatewayExecutionHookHandler
27 | : onResolutionHookHandler;
28 | };
29 |
30 | export function GraphQLHook(hookName: T) {
31 | return (
32 | target: Function | Record,
33 | key: any,
34 | descriptor?: TypedPropertyDescriptor,
35 | ) => {
36 | SetMetadata(HOOK_METADATA, hookName)(target, key, descriptor);
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/lib/decorators/index.ts:
--------------------------------------------------------------------------------
1 | export * from './resolve-loader.decorator';
2 | export * from './resolve-reference-loader.decorator';
3 | export * from './hook.decorator';
4 |
--------------------------------------------------------------------------------
/lib/decorators/resolve-loader.decorator.ts:
--------------------------------------------------------------------------------
1 | import { SetMetadata, Type } from '@nestjs/common';
2 | import { isFunction, isObject } from '@nestjs/common/utils/shared.utils';
3 | import {
4 | ReturnTypeFunc,
5 | FieldMiddleware,
6 | Complexity,
7 | GqlTypeReference,
8 | TypeMetadataStorage,
9 | BaseTypeOptions,
10 | } from '@nestjs/graphql';
11 | import { FIELD_RESOLVER_MIDDLEWARE_METADATA } from '@nestjs/graphql/dist/graphql.constants';
12 | import { LazyMetadataStorage } from '@nestjs/graphql/dist/schema-builder/storages/lazy-metadata.storage';
13 | import { TypeOptions } from '@nestjs/graphql/dist/interfaces/type-options.interface';
14 | import { reflectTypeFromMetadata } from '@nestjs/graphql/dist/utils/reflection.utilts';
15 | import { LOADER_NAME_METADATA, LOADER_PROPERTY_METADATA } from '../constants';
16 | import { LoaderMiddleware } from '../interfaces';
17 |
18 | export interface ResolveLoaderOptions extends BaseTypeOptions {
19 | name?: string;
20 | description?: string;
21 | deprecationReason?: string;
22 | complexity?: Complexity;
23 | middleware?: LoaderMiddleware[];
24 | opts?: {
25 | cache?: boolean;
26 | };
27 | }
28 |
29 | export function ResolveLoader(
30 | typeFunc?: ReturnTypeFunc,
31 | options?: ResolveLoaderOptions,
32 | ): MethodDecorator;
33 | export function ResolveLoader(
34 | propertyName?: string,
35 | typeFunc?: ReturnTypeFunc,
36 | options?: ResolveLoaderOptions,
37 | ): MethodDecorator;
38 | export function ResolveLoader(
39 | propertyNameOrFunc?: string | ReturnTypeFunc,
40 | typeFuncOrOptions?: ReturnTypeFunc | ResolveLoaderOptions,
41 | resolveFieldOptions?: ResolveLoaderOptions,
42 | ): MethodDecorator {
43 | return (
44 | target: Function | Record,
45 | key: any,
46 | descriptor?: any,
47 | ) => {
48 | // eslint-disable-next-line prefer-const
49 | let [propertyName, typeFunc, options] = isFunction(propertyNameOrFunc)
50 | ? typeFuncOrOptions && typeFuncOrOptions.name
51 | ? [typeFuncOrOptions.name, propertyNameOrFunc, typeFuncOrOptions]
52 | : [undefined, propertyNameOrFunc, typeFuncOrOptions]
53 | : [propertyNameOrFunc, typeFuncOrOptions, resolveFieldOptions];
54 | SetMetadata(LOADER_NAME_METADATA, propertyName)(target, key, descriptor);
55 | SetMetadata(LOADER_PROPERTY_METADATA, true)(target, key, descriptor);
56 |
57 | SetMetadata(
58 | FIELD_RESOLVER_MIDDLEWARE_METADATA,
59 | (options as ResolveLoaderOptions)?.middleware,
60 | )(target, key, descriptor);
61 |
62 | options = isObject(options)
63 | ? {
64 | name: propertyName as string,
65 | ...options,
66 | }
67 | : propertyName
68 | ? { name: propertyName as string }
69 | : {};
70 |
71 | LazyMetadataStorage.store(
72 | target.constructor as Type,
73 | function resolveLoader() {
74 | let typeOptions: TypeOptions, typeFn: (type?: any) => GqlTypeReference;
75 | try {
76 | const implicitTypeMetadata = reflectTypeFromMetadata({
77 | metadataKey: 'design:returntype',
78 | prototype: target,
79 | propertyKey: key,
80 | explicitTypeFn: typeFunc as ReturnTypeFunc,
81 | typeOptions: options as any,
82 | });
83 | typeOptions = implicitTypeMetadata.options;
84 | typeFn = implicitTypeMetadata.typeFn;
85 | } catch {}
86 |
87 | TypeMetadataStorage.addResolverPropertyMetadata({
88 | kind: 'external',
89 | methodName: key,
90 | schemaName: options.name || key,
91 | target: target.constructor,
92 | typeFn,
93 | typeOptions,
94 | description: (options as ResolveLoaderOptions).description,
95 | deprecationReason: (options as ResolveLoaderOptions)
96 | .deprecationReason,
97 | complexity: (options as ResolveLoaderOptions).complexity,
98 | });
99 | },
100 | );
101 | };
102 | }
103 |
--------------------------------------------------------------------------------
/lib/decorators/resolve-reference-loader.decorator.ts:
--------------------------------------------------------------------------------
1 | import { SetMetadata } from '@nestjs/common';
2 | import { RESOLVER_REFERENCE_METADATA } from '@nestjs/graphql/dist/federation/federation.constants';
3 | import {
4 | LOADER_PROPERTY_METADATA,
5 | REFERENCE_LOADER_METADATA,
6 | } from '../constants';
7 |
8 | /**
9 | * Property reference resolver (method) Decorator.
10 | */
11 | export function ResolveReferenceLoader(): MethodDecorator {
12 | return (
13 | target: Function | Object,
14 | key?: string | symbol,
15 | descriptor?: any,
16 | ) => {
17 | SetMetadata(RESOLVER_REFERENCE_METADATA, true)(target, key, descriptor);
18 | SetMetadata(REFERENCE_LOADER_METADATA, true)(target, key, descriptor);
19 | SetMetadata(LOADER_PROPERTY_METADATA, true)(target, key, descriptor);
20 | };
21 | }
22 |
--------------------------------------------------------------------------------
/lib/factories/graphql.factory.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Logger } from '@nestjs/common';
2 | import { GraphQLSchema } from 'graphql';
3 | import {
4 | GraphQLAstExplorer,
5 | GraphQLSchemaHost,
6 | GraphQLFactory as NestGraphQLFactory,
7 | } from '@nestjs/graphql';
8 | import {
9 | PluginsExplorerService,
10 | ResolversExplorerService,
11 | ScalarsExplorerService,
12 | } from '@nestjs/graphql/dist/services';
13 | import { GraphQLSchemaBuilder } from '@nestjs/graphql/dist/graphql-schema.builder';
14 | import { MercuriusModuleOptions } from '../interfaces';
15 | import { LoadersExplorerService } from '../services';
16 | import { transformFederatedSchema } from '../utils/faderation-factory.util';
17 |
18 | @Injectable()
19 | export class GraphQLFactory extends NestGraphQLFactory {
20 | private readonly logger = new Logger(GraphQLFactory.name);
21 |
22 | constructor(
23 | resolversExplorerService: ResolversExplorerService,
24 | scalarsExplorerService: ScalarsExplorerService,
25 | pluginExplorerService: PluginsExplorerService,
26 | graphqlAstExplorer: GraphQLAstExplorer,
27 | gqlSchemaBuilder: GraphQLSchemaBuilder,
28 | gqlSchemaHost: GraphQLSchemaHost,
29 | protected readonly loaderExplorerService: LoadersExplorerService,
30 | ) {
31 | super(
32 | resolversExplorerService,
33 | scalarsExplorerService,
34 | pluginExplorerService,
35 | graphqlAstExplorer,
36 | gqlSchemaBuilder,
37 | gqlSchemaHost,
38 | );
39 | }
40 |
41 | async mergeOptions(options?: any): Promise {
42 | if (options.federationMetadata) {
43 | options.buildSchemaOptions = {
44 | ...options.buildSchemaOptions,
45 | skipCheck: true,
46 | };
47 | }
48 | const parentOptions = (await super.mergeOptions(
49 | options as any,
50 | )) as unknown as MercuriusModuleOptions & {
51 | plugins: any[];
52 | schema: GraphQLSchema;
53 | };
54 | if (parentOptions.plugins?.length) {
55 | const pluginNames = parentOptions.plugins
56 | .map((p) => p.name)
57 | .filter(Boolean);
58 | this.logger.warn(
59 | `Plugins are not supported by Mercurius, ignoring: ${pluginNames.join(
60 | ', ',
61 | )}`,
62 | );
63 | }
64 | delete parentOptions.plugins;
65 | parentOptions.loaders = this.loaderExplorerService.explore();
66 | if (options.federationMetadata) {
67 | parentOptions.schema = transformFederatedSchema(parentOptions.schema);
68 | }
69 |
70 | return parentOptions;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/lib/factories/params.factory.ts:
--------------------------------------------------------------------------------
1 | import { ParamData } from '@nestjs/common';
2 | import { GqlParamtype } from '@nestjs/graphql/dist/enums/gql-paramtype.enum';
3 | import { isLoaderContext } from '../utils/is-loader-context';
4 | import { GqlParamsFactory } from '@nestjs/graphql/dist/factories/params.factory';
5 |
6 | /**
7 | * Override GqlParamsFactory for Loader resolvers
8 | * in Mercurius Loaders parameters differs from a standard graphql resolver
9 | */
10 | export class LoaderGqlParamsFactory extends GqlParamsFactory {
11 | exchangeKeyForValue(type: number, data: ParamData, args: any) {
12 | if (!args) {
13 | return null;
14 | }
15 |
16 | if (!isLoaderContext(args)) {
17 | return super.exchangeKeyForValue(type, data, args);
18 | }
19 |
20 | switch (type) {
21 | case GqlParamtype.ROOT:
22 | return args[0];
23 | case GqlParamtype.ARGS:
24 | return args[0].map(({ params }) =>
25 | data ? params[data as string] : params,
26 | );
27 | case GqlParamtype.CONTEXT:
28 | return data && args[1] ? args[1][data as string] : args[1];
29 | default:
30 | return null;
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/index.ts:
--------------------------------------------------------------------------------
1 | export * from './mercurius.module';
2 | export * from './mercurius-gateway.module';
3 | export * from './constants';
4 | export * from './interfaces';
5 | export * from './decorators';
6 | export * from './services';
7 | export * from './utils/to-async-iterator';
8 |
--------------------------------------------------------------------------------
/lib/interfaces/base-mercurius-module-options.interface.ts:
--------------------------------------------------------------------------------
1 | import { MercuriusCommonOptions } from 'mercurius';
2 |
3 | export interface BaseMercuriusModuleOptions extends MercuriusCommonOptions {
4 | path?: string;
5 | useGlobalPrefix?: boolean;
6 | uploads?: boolean | FileUploadOptions;
7 | altair?: boolean | any;
8 | }
9 |
10 | export interface FileUploadOptions {
11 | //Max allowed non-file multipart form field size in bytes; enough for your queries (default: 1 MB).
12 | maxFieldSize?: number;
13 | //Max allowed file size in bytes (default: Infinity).
14 | maxFileSize?: number;
15 | //Max allowed number of files (default: Infinity).
16 | maxFiles?: number;
17 | }
18 |
--------------------------------------------------------------------------------
/lib/interfaces/index.ts:
--------------------------------------------------------------------------------
1 | export * from './mercurius-module-options.interface';
2 | export * from './mercurius-gateway-module-options.interface';
3 | export * from './loader.interface';
4 | export * from './reference.interface';
5 |
--------------------------------------------------------------------------------
/lib/interfaces/loader.interface.ts:
--------------------------------------------------------------------------------
1 | import { MercuriusContext } from 'mercurius';
2 | import { FastifyReply } from 'fastify';
3 |
4 | export interface LoaderQuery {
5 | obj: T;
6 | params: P;
7 | }
8 |
9 | export type LoaderCtx = MercuriusContext & {
10 | reply: FastifyReply;
11 | [key: string]: unknown;
12 | };
13 |
14 | export interface LoaderMiddlewareContext<
15 | TSource = any,
16 | TContext extends Record = LoaderCtx
17 | > {
18 | source: TSource;
19 | context: TContext;
20 | }
21 |
22 | export declare type NextFn = () => Promise;
23 | export interface LoaderMiddleware<
24 | TSource extends LoaderQuery[] = any,
25 | TContext extends Record = LoaderCtx,
26 | TOutput = any
27 | > {
28 | (ctx: LoaderMiddlewareContext, next: NextFn):
29 | | Promise
30 | | TOutput;
31 | }
32 |
--------------------------------------------------------------------------------
/lib/interfaces/mercurius-gateway-module-options.interface.ts:
--------------------------------------------------------------------------------
1 | import { BaseMercuriusModuleOptions } from './base-mercurius-module-options.interface';
2 | import { MercuriusGatewayOptions } from 'mercurius';
3 | import { ModuleMetadata, Type } from '@nestjs/common';
4 |
5 | export interface MercuriusGatewayModuleOptions
6 | extends BaseMercuriusModuleOptions,
7 | MercuriusGatewayOptions {}
8 |
9 | export interface MercuriusGatewayOptionsFactory {
10 | createMercuriusGatewayOptions():
11 | | Promise
12 | | MercuriusGatewayModuleOptions;
13 | }
14 |
15 | export interface MercuriusGatewayModuleAsyncOptions
16 | extends Pick {
17 | useExisting?: Type;
18 | useClass?: Type;
19 | useFactory?: (
20 | ...args: any[]
21 | ) => Promise | MercuriusGatewayModuleOptions;
22 | inject?: any[];
23 | }
24 |
--------------------------------------------------------------------------------
/lib/interfaces/mercurius-module-options.interface.ts:
--------------------------------------------------------------------------------
1 | import {
2 | BuildSchemaOptions,
3 | DefinitionsGeneratorOptions,
4 | Enhancer,
5 | } from '@nestjs/graphql';
6 | import { ModuleMetadata, Type } from '@nestjs/common';
7 | import { GraphQLSchema } from 'graphql';
8 | import { IResolverValidationOptions } from '@graphql-tools/utils';
9 | import { MercuriusSchemaOptions } from 'mercurius';
10 | import { BaseMercuriusModuleOptions } from './base-mercurius-module-options.interface';
11 |
12 | export interface MercuriusModuleOptions
13 | extends Omit,
14 | BaseMercuriusModuleOptions {
15 | schema?: GraphQLSchema | string;
16 | path?: string;
17 | typeDefs?: string | string[];
18 | typePaths?: string[];
19 | include?: Function[];
20 | resolverValidationOptions?: IResolverValidationOptions;
21 | directiveResolvers?: any;
22 | schemaDirectives?: Record;
23 | transformSchema?: (
24 | schema: GraphQLSchema,
25 | ) => GraphQLSchema | Promise;
26 | definitions?: {
27 | path?: string;
28 | outputAs?: 'class' | 'interface';
29 | } & DefinitionsGeneratorOptions;
30 | autoSchemaFile?: boolean | string;
31 | buildSchemaOptions?: BuildSchemaOptions;
32 | transformAutoSchemaFile?: boolean;
33 | sortSchema?: boolean;
34 | fieldResolverEnhancers?: Enhancer[];
35 | }
36 |
37 | export interface MercuriusOptionsFactory {
38 | createMercuriusOptions():
39 | | Promise
40 | | MercuriusModuleOptions;
41 | }
42 |
43 | export interface MercuriusModuleAsyncOptions
44 | extends Pick {
45 | useExisting?: Type;
46 | useClass?: Type;
47 | useFactory?: (
48 | ...args: any[]
49 | ) => Promise | MercuriusModuleOptions;
50 | inject?: any[];
51 | }
52 |
--------------------------------------------------------------------------------
/lib/interfaces/reference.interface.ts:
--------------------------------------------------------------------------------
1 | export type Reference = {
2 | [R in K]: string;
3 | } & {
4 | __typename: T;
5 | };
6 |
--------------------------------------------------------------------------------
/lib/mercurius-core.module.ts:
--------------------------------------------------------------------------------
1 | import { Global, Module } from '@nestjs/common';
2 | import { PubSubHost } from './services';
3 |
4 | @Global()
5 | @Module({
6 | providers: [PubSubHost],
7 | exports: [PubSubHost],
8 | })
9 | export class MercuriusCoreModule {}
10 |
--------------------------------------------------------------------------------
/lib/mercurius-gateway.module.ts:
--------------------------------------------------------------------------------
1 | import {
2 | DynamicModule,
3 | Inject,
4 | Module,
5 | OnModuleInit,
6 | Provider,
7 | } from '@nestjs/common';
8 | import { GRAPHQL_GATEWAY_MODULE_OPTIONS } from '@nestjs/graphql/dist/federation/federation.constants';
9 | import { GRAPHQL_MODULE_ID } from '@nestjs/graphql/dist/graphql.constants';
10 | import { generateString } from '@nestjs/graphql/dist/utils';
11 | import { HttpAdapterHost, ApplicationConfig } from '@nestjs/core';
12 | import { BaseMercuriusModule } from './base-mercurius.module';
13 | import {
14 | MercuriusGatewayModuleAsyncOptions,
15 | MercuriusGatewayModuleOptions,
16 | MercuriusGatewayOptionsFactory,
17 | } from './interfaces';
18 | import { HookExplorerService } from './services';
19 | import { MetadataScanner } from '@nestjs/core/metadata-scanner';
20 |
21 | @Module({
22 | providers: [MetadataScanner, HookExplorerService],
23 | })
24 | export class MercuriusGatewayModule
25 | extends BaseMercuriusModule
26 | implements OnModuleInit
27 | {
28 | constructor(
29 | protected readonly httpAdapterHost: HttpAdapterHost,
30 | protected readonly applicationConfig: ApplicationConfig,
31 | @Inject(GRAPHQL_GATEWAY_MODULE_OPTIONS)
32 | protected readonly options: MercuriusGatewayModuleOptions,
33 | protected readonly hookExplorerService: HookExplorerService,
34 | ) {
35 | super(httpAdapterHost, applicationConfig, options, hookExplorerService);
36 | }
37 |
38 | static forRoot(options: MercuriusGatewayModuleOptions): DynamicModule {
39 | return {
40 | module: MercuriusGatewayModule,
41 | providers: [
42 | {
43 | provide: GRAPHQL_GATEWAY_MODULE_OPTIONS,
44 | useValue: options,
45 | },
46 | ],
47 | };
48 | }
49 |
50 | static forRootAsync(
51 | options: MercuriusGatewayModuleAsyncOptions,
52 | ): DynamicModule {
53 | return {
54 | module: MercuriusGatewayModule,
55 | imports: options.imports,
56 | providers: [
57 | ...this.createAsyncProviders(options),
58 | {
59 | provide: GRAPHQL_MODULE_ID,
60 | useValue: generateString(),
61 | },
62 | ],
63 | };
64 | }
65 |
66 | private static createAsyncProviders(
67 | options: MercuriusGatewayModuleAsyncOptions,
68 | ): Provider[] {
69 | if (options.useExisting || options.useFactory) {
70 | return [this.createAsyncOptionsProvider(options)];
71 | }
72 |
73 | return [
74 | this.createAsyncOptionsProvider(options),
75 | {
76 | provide: options.useClass,
77 | useClass: options.useClass,
78 | },
79 | ];
80 | }
81 |
82 | private static createAsyncOptionsProvider(
83 | options: MercuriusGatewayModuleAsyncOptions,
84 | ): Provider {
85 | if (options.useFactory) {
86 | return {
87 | provide: GRAPHQL_GATEWAY_MODULE_OPTIONS,
88 | useFactory: options.useFactory,
89 | inject: options.inject || [],
90 | };
91 | }
92 |
93 | return {
94 | provide: GRAPHQL_GATEWAY_MODULE_OPTIONS,
95 | useFactory: (optionsFactory: MercuriusGatewayOptionsFactory) =>
96 | optionsFactory.createMercuriusGatewayOptions(),
97 | inject: [options.useExisting || options.useClass],
98 | };
99 | }
100 |
101 | async onModuleInit() {
102 | const { httpAdapter } = this.httpAdapterHost || {};
103 | if (!httpAdapter) {
104 | return;
105 | }
106 |
107 | await this.registerGqlServer(this.options);
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/lib/mercurius.module.ts:
--------------------------------------------------------------------------------
1 | import { GraphQLSchema, printSchema } from 'graphql';
2 | import {
3 | DynamicModule,
4 | Inject,
5 | Module,
6 | OnModuleInit,
7 | Provider,
8 | } from '@nestjs/common';
9 | import { ApplicationConfig, HttpAdapterHost } from '@nestjs/core';
10 | import { MetadataScanner } from '@nestjs/core/metadata-scanner';
11 | import {
12 | GraphQLTypesLoader,
13 | GraphQLSchemaBuilderModule,
14 | GraphQLAstExplorer,
15 | GraphQLSchemaHost,
16 | } from '@nestjs/graphql';
17 | import {
18 | PluginsExplorerService,
19 | ResolversExplorerService,
20 | ScalarsExplorerService,
21 | } from '@nestjs/graphql/dist/services';
22 | import {
23 | GRAPHQL_MODULE_OPTIONS,
24 | GRAPHQL_MODULE_ID,
25 | } from '@nestjs/graphql/dist/graphql.constants';
26 | import { GraphQLSchemaBuilder } from '@nestjs/graphql/dist/graphql-schema.builder';
27 | import { extend, generateString } from '@nestjs/graphql/dist/utils';
28 | import {
29 | MercuriusModuleAsyncOptions,
30 | MercuriusModuleOptions,
31 | MercuriusOptionsFactory,
32 | } from './interfaces';
33 | import { HookExplorerService, LoadersExplorerService } from './services';
34 | import { mergeDefaults } from './utils/merge-defaults';
35 | import { MercuriusCoreModule } from './mercurius-core.module';
36 | import { GraphQLFactory } from './factories/graphql.factory';
37 | import { BaseMercuriusModule } from './base-mercurius.module';
38 |
39 | @Module({
40 | imports: [GraphQLSchemaBuilderModule, MercuriusCoreModule],
41 | providers: [
42 | GraphQLFactory,
43 | MetadataScanner,
44 | ResolversExplorerService,
45 | ScalarsExplorerService,
46 | PluginsExplorerService,
47 | GraphQLAstExplorer,
48 | GraphQLTypesLoader,
49 | GraphQLSchemaBuilder,
50 | GraphQLSchemaHost,
51 | LoadersExplorerService,
52 | HookExplorerService,
53 | ],
54 | })
55 | export class MercuriusModule
56 | extends BaseMercuriusModule
57 | implements OnModuleInit
58 | {
59 | constructor(
60 | private readonly graphqlFactory: GraphQLFactory,
61 | private readonly graphqlTypesLoader: GraphQLTypesLoader,
62 | @Inject(GRAPHQL_MODULE_OPTIONS)
63 | protected readonly options: MercuriusModuleOptions,
64 | protected readonly applicationConfig: ApplicationConfig,
65 | protected readonly httpAdapterHost: HttpAdapterHost,
66 | protected readonly hookExplorerService: HookExplorerService,
67 | ) {
68 | super(httpAdapterHost, applicationConfig, options, hookExplorerService);
69 | }
70 |
71 | static forRoot(options: MercuriusModuleOptions) {
72 | options = mergeDefaults(options);
73 | return {
74 | module: MercuriusModule,
75 | providers: [
76 | {
77 | provide: GRAPHQL_MODULE_OPTIONS,
78 | useValue: options,
79 | },
80 | ],
81 | };
82 | }
83 |
84 | static forRootAsync(options: MercuriusModuleAsyncOptions): DynamicModule {
85 | return {
86 | module: MercuriusModule,
87 | imports: options.imports,
88 | providers: [
89 | ...this.createAsyncProviders(options),
90 | {
91 | provide: GRAPHQL_MODULE_ID,
92 | useValue: generateString(),
93 | },
94 | ],
95 | };
96 | }
97 |
98 | private static createAsyncProviders(
99 | options: MercuriusModuleAsyncOptions,
100 | ): Provider[] {
101 | if (options.useExisting || options.useFactory) {
102 | return [this.createAsyncOptionsProvider(options)];
103 | }
104 | return [
105 | this.createAsyncOptionsProvider(options),
106 | {
107 | provide: options.useClass,
108 | useClass: options.useClass,
109 | },
110 | ];
111 | }
112 |
113 | private static createAsyncOptionsProvider(
114 | options: MercuriusModuleAsyncOptions,
115 | ): Provider {
116 | if (options.useFactory) {
117 | return {
118 | provide: GRAPHQL_MODULE_OPTIONS,
119 | useFactory: async (...args: any[]) =>
120 | mergeDefaults(await options.useFactory(...args)),
121 | inject: options.inject || [],
122 | };
123 | }
124 | return {
125 | provide: GRAPHQL_MODULE_OPTIONS,
126 | useFactory: async (optionsFactory: MercuriusOptionsFactory) =>
127 | mergeDefaults(await optionsFactory.createMercuriusOptions()),
128 | inject: [options.useExisting || options.useClass],
129 | };
130 | }
131 |
132 | async onModuleInit() {
133 | if (!this.httpAdapterHost) {
134 | return;
135 | }
136 | const httpAdapter = this.httpAdapterHost.httpAdapter;
137 | if (!httpAdapter) {
138 | return;
139 | }
140 | const typeDefs =
141 | (await this.graphqlTypesLoader.mergeTypesByPaths(
142 | this.options.typePaths,
143 | )) || [];
144 | const mergedTypeDefs = extend(typeDefs, this.options.typeDefs);
145 |
146 | const mercuriusOptions = (await this.graphqlFactory.mergeOptions({
147 | ...this.options,
148 | typeDefs: mergedTypeDefs,
149 | } as any)) as unknown as MercuriusModuleOptions;
150 |
151 | if (
152 | this.options.definitions &&
153 | this.options.definitions.path &&
154 | mercuriusOptions.schema instanceof GraphQLSchema
155 | ) {
156 | await this.graphqlFactory.generateDefinitions(
157 | printSchema(mercuriusOptions.schema),
158 | this.options as any,
159 | );
160 | }
161 | await this.registerGqlServer(mercuriusOptions);
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/lib/services/gql-arguments-host.ts:
--------------------------------------------------------------------------------
1 | import { GqlArgumentsHost as NestGqlArgumentsHost } from '@nestjs/graphql';
2 | import { ArgumentsHost } from '@nestjs/common';
3 | import { isLoaderContext } from '../utils/is-loader-context';
4 |
5 | export class GqlArgumentsHost extends NestGqlArgumentsHost {
6 | private readonly isLoaderContext: boolean;
7 |
8 | constructor(args: any[]) {
9 | super(args);
10 | // All graphql resolvers have 4 args: Root, Args, Context, Info
11 | // Only Mercurius Loaders have only 2 args: Queries and Context
12 | this.isLoaderContext = isLoaderContext(args);
13 | }
14 |
15 | static create(context: ArgumentsHost): GqlArgumentsHost {
16 | const type = context.getType();
17 | const gqlContext = new GqlArgumentsHost(context.getArgs());
18 | gqlContext.setType(type);
19 | return gqlContext;
20 | }
21 |
22 | getContext(): Ctx {
23 | return this.isLoaderContext ? this.getArgs() : super.getContext();
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/lib/services/gql-execution-context.ts:
--------------------------------------------------------------------------------
1 | import { GqlExecutionContext as NestGqlExecutionContext } from '@nestjs/graphql';
2 | import { ExecutionContext, Type } from '@nestjs/common';
3 | import { isLoaderContext } from '../utils/is-loader-context';
4 |
5 | export class GqlExecutionContext extends NestGqlExecutionContext {
6 | private readonly isLoaderContext: boolean;
7 |
8 | constructor(
9 | args: any[],
10 | constructorRef: Type = null,
11 | handler: Function = null,
12 | ) {
13 | super(args, constructorRef, handler);
14 | // All graphql resolvers have 4 args: Root, Args, Context, Info
15 | // Only Mercurius Loaders have only 2 args: Queries and Context
16 | this.isLoaderContext = isLoaderContext(args);
17 | }
18 |
19 | static create(context: ExecutionContext): GqlExecutionContext {
20 | const type = context.getType();
21 | const gqlContext = new GqlExecutionContext(
22 | context.getArgs(),
23 | context.getClass(),
24 | context.getHandler(),
25 | );
26 | gqlContext.setType(type);
27 | return gqlContext;
28 | }
29 |
30 | getContext(): Ctx {
31 | return this.isLoaderContext ? this.getArgs() : super.getContext();
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/services/hook-explorer.service.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable, Optional } from '@nestjs/common';
2 | import { BaseExplorerService } from '@nestjs/graphql/dist/services';
3 | import { ModulesContainer } from '@nestjs/core/injector/modules-container';
4 | import { GRAPHQL_MODULE_OPTIONS } from '@nestjs/graphql/dist/graphql.constants';
5 | import { MercuriusModuleOptions } from '../interfaces';
6 | import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
7 | import { MetadataScanner } from '@nestjs/core/metadata-scanner';
8 | import { extractHookMetadata } from '../utils/extract-hook-metadata.util';
9 |
10 | @Injectable()
11 | export class HookExplorerService extends BaseExplorerService {
12 | constructor(
13 | private readonly modulesContainer: ModulesContainer,
14 | private readonly metadataScanner: MetadataScanner,
15 | @Optional()
16 | @Inject(GRAPHQL_MODULE_OPTIONS)
17 | private readonly gqlOptions?: MercuriusModuleOptions,
18 | ) {
19 | super();
20 | }
21 |
22 | explore() {
23 | const modules = this.getModules(
24 | this.modulesContainer,
25 | this.gqlOptions?.include || [],
26 | );
27 |
28 | return this.flatMap(modules, (instance) => this.filterHooks(instance));
29 | }
30 |
31 | filterHooks(wrapper: InstanceWrapper) {
32 | const { instance } = wrapper;
33 | if (!instance) {
34 | return undefined;
35 | }
36 | const prototype = Object.getPrototypeOf(instance);
37 |
38 | return this.metadataScanner.scanFromPrototype(instance, prototype, (name) =>
39 | extractHookMetadata(instance, prototype, name),
40 | );
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/lib/services/index.ts:
--------------------------------------------------------------------------------
1 | export * from './loaders-explorer.service';
2 | export * from './gql-execution-context';
3 | export * from './gql-arguments-host';
4 | export * from './pub-sub-host';
5 | export * from './hook-explorer.service';
6 |
--------------------------------------------------------------------------------
/lib/services/loaders-explorer.service.ts:
--------------------------------------------------------------------------------
1 | import { head, identity } from 'lodash';
2 | import { Loader, MercuriusLoaders } from 'mercurius';
3 | import { createContextId, REQUEST } from '@nestjs/core';
4 | import { ModulesContainer } from '@nestjs/core/injector/modules-container';
5 | import { MetadataScanner } from '@nestjs/core/metadata-scanner';
6 | import { ExternalContextCreator } from '@nestjs/core/helpers/external-context-creator';
7 | import { REQUEST_CONTEXT_ID } from '@nestjs/core/router/request/request-constants';
8 | import { Injector } from '@nestjs/core/injector/injector';
9 | import { InternalCoreModule } from '@nestjs/core/injector/internal-core-module';
10 | import {
11 | ContextId,
12 | InstanceWrapper,
13 | } from '@nestjs/core/injector/instance-wrapper';
14 | import { Module } from '@nestjs/core/injector/module';
15 | import { Inject, Injectable, Type } from '@nestjs/common';
16 | import { isUndefined } from '@nestjs/common/utils/shared.utils';
17 | import { FieldMiddleware, TypeMetadataStorage } from '@nestjs/graphql';
18 | import { BaseExplorerService } from '@nestjs/graphql/dist/services';
19 | import { ObjectTypeMetadata } from '@nestjs/graphql/dist/schema-builder/metadata/object-type.metadata';
20 | import {
21 | PARAM_ARGS_METADATA,
22 | GRAPHQL_MODULE_OPTIONS,
23 | FIELD_RESOLVER_MIDDLEWARE_METADATA,
24 | } from '@nestjs/graphql/dist/graphql.constants';
25 | import { LoaderGqlParamsFactory } from '../factories/params.factory';
26 | import { LoaderQuery, MercuriusModuleOptions, LoaderCtx } from '../interfaces';
27 | import { extractLoaderMetadata } from '../utils/extract-loader-metadata.util';
28 | import { decorateLoaderResolverWithMiddleware } from '../utils/decorate-loader-resolver.util';
29 | import { GqlParamtype } from '@nestjs/graphql/dist/enums/gql-paramtype.enum';
30 | import { getInterfacesArray } from '@nestjs/graphql/dist/schema-builder/utils/get-interfaces-array.util';
31 |
32 | interface LoaderMetadata {
33 | type: string;
34 | methodName: string;
35 | name: string;
36 | callback: Loader;
37 | interfaces: string[];
38 | opts?: {
39 | cache: boolean;
40 | };
41 | instance: Type;
42 | prototype: any;
43 | wrapper: InstanceWrapper;
44 | isRequestScoped: boolean;
45 | moduleRef: Module;
46 | }
47 |
48 | interface ObjectTypeLoaders {
49 | objectTypeMetadata: ObjectTypeMetadata;
50 | isInterface: boolean;
51 | concretes?: Set;
52 | loaders: Record<
53 | string,
54 | {
55 | loader: Loader;
56 | opts?: { cache: boolean };
57 | }
58 | >;
59 | }
60 |
61 | @Injectable()
62 | export class LoadersExplorerService extends BaseExplorerService {
63 | private readonly gqlParamsFactory = new LoaderGqlParamsFactory();
64 | private readonly injector = new Injector();
65 |
66 | constructor(
67 | private readonly modulesContainer: ModulesContainer,
68 | private readonly metadataScanner: MetadataScanner,
69 | private readonly externalContextCreator: ExternalContextCreator,
70 | @Inject(GRAPHQL_MODULE_OPTIONS)
71 | private readonly gqlOptions: MercuriusModuleOptions,
72 | ) {
73 | super();
74 | }
75 |
76 | explore(): MercuriusLoaders {
77 | const modules = this.getModules(
78 | this.modulesContainer,
79 | this.gqlOptions.include || [],
80 | );
81 |
82 | /**
83 | * Get all loader methods from all application instances
84 | */
85 | const loaders = this.flatMap(modules, (instance, moduleRef) =>
86 | this.filterLoaders(instance, moduleRef),
87 | );
88 |
89 | const objectTypesByInterface = this.getInterfacesImplementations();
90 |
91 | /**
92 | * From the retrieved loaders, create a Loader tree and add useful metadata
93 | * ObjectType: {
94 | * loaders: {
95 | * // Mercurius compatible Loader structure
96 | * [field]: {
97 | * loader: Function,
98 | * opts: Object
99 | * }
100 | * },
101 | * objectTypeMetadata: ObjectTypeMetadata, // current ObjectType metadata
102 | * interfaces: string[] // all interfaces that this ObjectType implements
103 | * }
104 | */
105 | const typeLoaders = loaders.reduce((acc, loader) => {
106 | if (!acc[loader.type]) {
107 | // Get loader type metadata and cache it
108 | const objectTypeMetadata = this.getObjectTypeMetadataByName(
109 | loader.type,
110 | );
111 | const isInterface = !objectTypeMetadata;
112 | acc[loader.type] = {
113 | isInterface,
114 | concretes: isInterface
115 | ? objectTypesByInterface[loader.type] || new Set()
116 | : undefined,
117 | objectTypeMetadata,
118 | loaders: {},
119 | };
120 | }
121 | // Create the Mercurius Loader object for the current loader metadata
122 | acc[loader.type].loaders[loader.name] = this.createLoader(
123 | loader,
124 | acc[loader.type].objectTypeMetadata,
125 | );
126 | return acc;
127 | }, {} as Record);
128 |
129 | /**
130 | * For each entity in the Loader tree, merge its loader functions with the ones defined on its interfaces
131 | */
132 | return Object.entries(typeLoaders).reduce((acc, [type, metadata]) => {
133 | if (metadata.isInterface) {
134 | metadata.concretes.forEach((value) => {
135 | acc[value] = {
136 | ...acc[value],
137 | ...metadata.loaders,
138 | };
139 | });
140 | } else {
141 | acc[type] = {
142 | ...acc[type],
143 | ...metadata.loaders,
144 | };
145 | }
146 | return acc;
147 | }, {} as MercuriusLoaders);
148 | }
149 |
150 | /**
151 | * Check if the passed instance has ResolveLoader decorated methods
152 | * return ResolveLoader methods metadata
153 | * @param wrapper
154 | * @param moduleRef
155 | */
156 | filterLoaders(
157 | wrapper: InstanceWrapper,
158 | moduleRef: Module,
159 | ): Omit[] {
160 | const { instance } = wrapper;
161 | if (!instance) {
162 | return undefined;
163 | }
164 | const prototype = Object.getPrototypeOf(instance);
165 | const predicate = (resolverType: string, isLoaderResolver: boolean) => {
166 | if (!isUndefined(resolverType)) {
167 | return !isLoaderResolver;
168 | }
169 | return true;
170 | };
171 |
172 | const isRequestScoped = !wrapper.isDependencyTreeStatic();
173 |
174 | return this.metadataScanner.scanFromPrototype(
175 | instance,
176 | prototype,
177 | (name) => {
178 | const meta = extractLoaderMetadata(
179 | instance,
180 | prototype,
181 | name,
182 | predicate,
183 | );
184 | if (meta) {
185 | return {
186 | ...meta,
187 | instance,
188 | prototype,
189 | wrapper,
190 | moduleRef,
191 | isRequestScoped,
192 | };
193 | }
194 | },
195 | );
196 | }
197 |
198 | /**
199 | * Get a map of all Interfaces and their ObjectType implementations
200 | * @private
201 | */
202 | private getInterfacesImplementations(): Record> {
203 | return TypeMetadataStorage.getObjectTypesMetadata().reduce((acc, curr) => {
204 | getInterfacesArray(curr.interfaces).forEach((interfaceTarget) => {
205 | const int = TypeMetadataStorage.getInterfaceMetadataByTarget(
206 | interfaceTarget as Type,
207 | );
208 | if (!acc[int.name]) {
209 | acc[int.name] = new Set();
210 | }
211 | acc[int.name].add(curr.name);
212 | });
213 | return acc;
214 | }, {} as Record>);
215 | }
216 |
217 | /**
218 | * Create a Mercurius Loader from the loader metadata
219 | * @param loader
220 | * @param objectTypeMetadata
221 | * @private
222 | */
223 | private createLoader(
224 | loader: Omit,
225 | objectTypeMetadata: ObjectTypeMetadata,
226 | ): { loader: Loader; opts?: { cache: boolean } } {
227 | const fieldOptions = objectTypeMetadata?.properties.find(
228 | (p) => p.schemaName === loader.name,
229 | )?.options as { opts?: { cache: boolean } };
230 |
231 | const createContext = (transform?: Function) =>
232 | this.createContextCallback(
233 | loader.instance,
234 | loader.prototype,
235 | loader.wrapper,
236 | loader.moduleRef,
237 | loader,
238 | loader.isRequestScoped,
239 | transform,
240 | );
241 |
242 | return {
243 | opts: fieldOptions?.opts,
244 | loader: createContext(),
245 | };
246 | }
247 |
248 | private createContextCallback>(
249 | instance: T,
250 | prototype: any,
251 | wrapper: InstanceWrapper,
252 | moduleRef: Module,
253 | resolver: any,
254 | isRequestScoped: boolean,
255 | transform: Function = identity,
256 | ) {
257 | const paramsFactory = this.gqlParamsFactory;
258 | const fieldResolverEnhancers = this.gqlOptions.fieldResolverEnhancers || [];
259 |
260 | const contextOptions = {
261 | guards: fieldResolverEnhancers.includes('guards'),
262 | filters: fieldResolverEnhancers.includes('filters'),
263 | interceptors: fieldResolverEnhancers.includes('interceptors'),
264 | };
265 |
266 | if (isRequestScoped) {
267 | const loaderCallback = async (...args: any[]) => {
268 | const gqlContext = paramsFactory.exchangeKeyForValue(
269 | GqlParamtype.CONTEXT,
270 | undefined,
271 | args,
272 | );
273 | let contextId: ContextId;
274 | if (gqlContext && gqlContext[REQUEST_CONTEXT_ID]) {
275 | contextId = gqlContext[REQUEST_CONTEXT_ID];
276 | } else if (
277 | gqlContext &&
278 | gqlContext.req &&
279 | gqlContext.req[REQUEST_CONTEXT_ID]
280 | ) {
281 | contextId = gqlContext.req[REQUEST_CONTEXT_ID];
282 | } else {
283 | contextId = createContextId();
284 | Object.defineProperty(gqlContext, REQUEST_CONTEXT_ID, {
285 | value: contextId,
286 | enumerable: false,
287 | configurable: false,
288 | writable: false,
289 | });
290 | }
291 |
292 | this.registerContextProvider(gqlContext, contextId);
293 | const contextInstance = await this.injector.loadPerContext(
294 | instance,
295 | moduleRef,
296 | moduleRef.providers,
297 | contextId,
298 | );
299 | const callback = this.externalContextCreator.create(
300 | contextInstance,
301 | transform(contextInstance[resolver.methodName]),
302 | resolver.methodName,
303 | PARAM_ARGS_METADATA,
304 | paramsFactory,
305 | contextId,
306 | wrapper.id,
307 | contextOptions,
308 | 'graphql',
309 | );
310 | return callback(...args);
311 | };
312 |
313 | return this.registerFieldMiddlewareIfExists(
314 | loaderCallback,
315 | instance,
316 | resolver.methodName,
317 | );
318 | }
319 |
320 | const loaderCallback = this.externalContextCreator.create(
321 | instance,
322 | prototype[resolver.methodName],
323 | resolver.methodName,
324 | PARAM_ARGS_METADATA,
325 | paramsFactory,
326 | undefined,
327 | undefined,
328 | contextOptions,
329 | 'graphql',
330 | );
331 |
332 | return this.registerFieldMiddlewareIfExists(
333 | loaderCallback,
334 | instance,
335 | resolver.methodName,
336 | );
337 | }
338 |
339 | private registerContextProvider(request: T, contextId: ContextId) {
340 | const coreModuleArray = [...this.modulesContainer.entries()]
341 | .filter(
342 | ([key, { metatype }]) =>
343 | metatype && metatype.name === InternalCoreModule.name,
344 | )
345 | .map(([key, value]) => value);
346 |
347 | const coreModuleRef = head(coreModuleArray);
348 | if (!coreModuleRef) {
349 | return;
350 | }
351 | const wrapper = coreModuleRef.getProviderByKey(REQUEST);
352 | wrapper.setInstanceByContextId(contextId, {
353 | instance: request,
354 | isResolved: true,
355 | });
356 | }
357 |
358 | private registerFieldMiddlewareIfExists<
359 | TSource extends LoaderQuery[] = any,
360 | TContext extends LoaderCtx = LoaderCtx,
361 | TArgs = { [argName: string]: any },
362 | TOutput = any
363 | >(resolverFn: Loader, instance: object, methodKey: string) {
364 | const fieldMiddleware = Reflect.getMetadata(
365 | FIELD_RESOLVER_MIDDLEWARE_METADATA,
366 | instance[methodKey],
367 | );
368 |
369 | const middlewareFunctions = ((this.gqlOptions?.buildSchemaOptions
370 | ?.fieldMiddleware || []) as FieldMiddleware[]).concat(
371 | fieldMiddleware || [],
372 | );
373 |
374 | if (middlewareFunctions?.length === 0) {
375 | return resolverFn;
376 | }
377 |
378 | const originalResolveFnFactory = (...args: [TSource, TContext]) => () =>
379 | resolverFn(...args);
380 |
381 | return decorateLoaderResolverWithMiddleware(
382 | originalResolveFnFactory,
383 | middlewareFunctions,
384 | );
385 | }
386 |
387 | private getObjectTypeMetadataByName(
388 | name: string,
389 | ): ObjectTypeMetadata | undefined {
390 | return TypeMetadataStorage.getObjectTypesMetadata().find(
391 | (m) => m.name === name,
392 | );
393 | }
394 | }
395 |
--------------------------------------------------------------------------------
/lib/services/pub-sub-host.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { HttpAdapterHost } from '@nestjs/core';
3 | import { PubSub } from 'mercurius';
4 | import { FastifyInstance } from 'fastify';
5 |
6 | @Injectable()
7 | export class PubSubHost {
8 | constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
9 |
10 | getInstance(): PubSub | undefined {
11 | return this.httpAdapterHost.httpAdapter.getInstance()
12 | .graphql?.pubsub;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/lib/utils/decorate-loader-resolver.util.ts:
--------------------------------------------------------------------------------
1 | import { LoaderQuery, LoaderMiddleware, LoaderCtx } from '../interfaces';
2 |
3 | export function decorateLoaderResolverWithMiddleware<
4 | TSource extends LoaderQuery[] = any,
5 | TContext extends LoaderCtx = LoaderCtx,
6 | TOutput = any
7 | >(
8 | originalResolveFnFactory: (...args: [TSource, TContext]) => Function,
9 | middlewareFunctions: LoaderMiddleware[] = [],
10 | ) {
11 | return (root: TSource, context: TContext): TOutput | Promise => {
12 | let index = -1;
13 |
14 | const run = async (currentIndex: number): Promise => {
15 | if (currentIndex <= index) {
16 | throw new Error('next() called multiple times');
17 | }
18 |
19 | index = currentIndex;
20 | let middlewareFn: LoaderMiddleware;
21 |
22 | if (currentIndex === middlewareFunctions.length) {
23 | middlewareFn = originalResolveFnFactory(
24 | root,
25 | context,
26 | ) as LoaderMiddleware;
27 | } else {
28 | middlewareFn = middlewareFunctions[currentIndex];
29 | }
30 |
31 | let tempResult: TOutput = undefined;
32 | const result = await middlewareFn(
33 | {
34 | context,
35 | source: root,
36 | },
37 | async () => {
38 | tempResult = await run(currentIndex + 1);
39 | return tempResult;
40 | },
41 | );
42 |
43 | return result !== undefined ? result : tempResult;
44 | };
45 | return run(0);
46 | };
47 | }
48 |
--------------------------------------------------------------------------------
/lib/utils/extract-hook-metadata.util.ts:
--------------------------------------------------------------------------------
1 | import 'reflect-metadata';
2 | import {
3 | RESOLVER_NAME_METADATA,
4 | RESOLVER_TYPE_METADATA,
5 | } from '@nestjs/graphql/dist/graphql.constants';
6 | import { HOOK_METADATA, LOADER_PROPERTY_METADATA } from '../constants';
7 | import {
8 | RESOLVER_REFERENCE_KEY,
9 | RESOLVER_REFERENCE_METADATA,
10 | } from '@nestjs/graphql/dist/federation/federation.constants';
11 | import { HookMap, HookName } from '../decorators';
12 |
13 | export interface HookMetadata {
14 | name: N;
15 | methodName: string;
16 | callback: HookMap[N];
17 | }
18 |
19 | export function extractHookMetadata(
20 | instance: Record,
21 | prototype: any,
22 | methodName: string,
23 | ) {
24 | const callback = prototype[methodName];
25 |
26 | const hookName: HookName = Reflect.getMetadata(HOOK_METADATA, callback);
27 |
28 | if (!hookName) {
29 | return;
30 | }
31 |
32 | return {
33 | name: hookName,
34 | methodName,
35 | callback: callback.bind(instance),
36 | } as HookMetadata;
37 | }
38 |
--------------------------------------------------------------------------------
/lib/utils/extract-loader-metadata.util.ts:
--------------------------------------------------------------------------------
1 | import 'reflect-metadata';
2 | import {
3 | RESOLVER_NAME_METADATA,
4 | RESOLVER_TYPE_METADATA,
5 | } from '@nestjs/graphql/dist/graphql.constants';
6 | import { LOADER_PROPERTY_METADATA } from '../constants';
7 | import {
8 | RESOLVER_REFERENCE_KEY,
9 | RESOLVER_REFERENCE_METADATA,
10 | } from '@nestjs/graphql/dist/federation/federation.constants';
11 |
12 | export function extractLoaderMetadata(
13 | instance: Record,
14 | prototype: any,
15 | methodName: string,
16 | filterPredicate: (resolverType: string, isLoaderResolver: boolean) => boolean,
17 | ): any {
18 | const callback = prototype[methodName];
19 | const resolverType =
20 | Reflect.getMetadata(RESOLVER_TYPE_METADATA, callback) ||
21 | Reflect.getMetadata(RESOLVER_TYPE_METADATA, instance.constructor);
22 |
23 | const isLoaderResolver = !!Reflect.getMetadata(
24 | LOADER_PROPERTY_METADATA,
25 | callback,
26 | );
27 |
28 | const isReferenceResolver = !!Reflect.getMetadata(
29 | RESOLVER_REFERENCE_METADATA,
30 | callback,
31 | );
32 |
33 | const resolverName = Reflect.getMetadata(RESOLVER_NAME_METADATA, callback);
34 |
35 | if (filterPredicate(resolverType, isLoaderResolver)) {
36 | return null;
37 | }
38 |
39 | const name = isReferenceResolver
40 | ? RESOLVER_REFERENCE_KEY
41 | : resolverName || methodName;
42 |
43 | return {
44 | type: resolverType,
45 | methodName,
46 | name,
47 | };
48 | }
49 |
--------------------------------------------------------------------------------
/lib/utils/faderation-factory.util.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Inspired by the official Mercurius federation `buildFederationSchema`
3 | * https://github.com/mercurius-js/mercurius/blob/master/lib/federation.js
4 | *
5 | */
6 | import {
7 | extendSchema,
8 | GraphQLObjectType,
9 | GraphQLSchema,
10 | isObjectType,
11 | parse,
12 | } from 'graphql';
13 | import { MER_ERR_GQL_GATEWAY_INVALID_SCHEMA } from 'mercurius/lib/errors';
14 | import { loadPackage } from '@nestjs/common/utils/load-package.util';
15 |
16 | const BASE_FEDERATION_TYPES = `
17 | scalar _Any
18 | scalar _FieldSet
19 | directive @external on FIELD_DEFINITION
20 | directive @requires(fields: _FieldSet!) on FIELD_DEFINITION
21 | directive @provides(fields: _FieldSet!) on FIELD_DEFINITION
22 | directive @key(fields: _FieldSet!) on OBJECT | INTERFACE
23 | directive @extends on OBJECT | INTERFACE
24 | `;
25 |
26 | export const FEDERATION_SCHEMA = `
27 | ${BASE_FEDERATION_TYPES}
28 | type _Service {
29 | sdl: String
30 | }
31 | `;
32 |
33 | export function gatherDirectives(type) {
34 | let directives = [];
35 | for (const node of type.extensionASTNodes || []) {
36 | if (node.directives) {
37 | directives = directives.concat(node.directives);
38 | }
39 | }
40 |
41 | if (type.astNode && type.astNode.directives) {
42 | directives = directives.concat(type.astNode.directives);
43 | }
44 |
45 | return directives;
46 | }
47 |
48 | export function typeIncludesDirective(type, directiveName) {
49 | return gatherDirectives(type).some(
50 | (directive) => directive.name.value === directiveName,
51 | );
52 | }
53 |
54 | function addTypeNameToResult(result, typename) {
55 | if (result !== null && typeof result === 'object') {
56 | Object.defineProperty(result, '__typename', {
57 | value: typename,
58 | });
59 | }
60 | return result;
61 | }
62 |
63 | /**
64 | * Inspired by https://github.com/mercurius-js/mercurius/blob/master/lib/federation.js#L231
65 | * Accept a GraphQLSchema to transform instead of a plain string containing a graphql schema
66 | * @param schema
67 | */
68 | export function transformFederatedSchema(schema: GraphQLSchema) {
69 | // FIXME remove this dependency
70 | // but graphql#printSchema does not print necessary federation directives
71 | const { printSubgraphSchema } = loadPackage(
72 | '@apollo/subgraph',
73 | 'FederationFactory',
74 | () => require('@apollo/subgraph'),
75 | );
76 |
77 | // Workaround for https://github.com/mercurius-js/mercurius/issues/273
78 | const schemaString = printSubgraphSchema(schema)
79 | .replace('type Query {', 'type Query @extends {')
80 | .replace('type Mutation {', 'type Mutation @extends {')
81 | .replace('type Subscription {', 'type Subscription @extends {');
82 |
83 | schema = extendSchema(schema, parse(FEDERATION_SCHEMA), {
84 | assumeValidSDL: true,
85 | });
86 |
87 | if (!schema.getType('Query')) {
88 | schema = new GraphQLSchema({
89 | ...schema.toConfig(),
90 | query: new GraphQLObjectType({
91 | name: 'Query',
92 | fields: {},
93 | }),
94 | });
95 | }
96 |
97 | schema = extendSchema(
98 | schema,
99 | parse(`
100 | extend type Query {
101 | _service: _Service!
102 | }
103 | `),
104 | {
105 | assumeValid: true,
106 | },
107 | );
108 |
109 | const query = schema.getType('Query') as GraphQLObjectType;
110 | const queryFields = query.getFields();
111 |
112 | queryFields._service = {
113 | ...queryFields._service,
114 | resolve: () => ({ sdl: schemaString }),
115 | };
116 |
117 | const entityTypes = Object.values(schema.getTypeMap()).filter(
118 | (type) => isObjectType(type) && typeIncludesDirective(type, 'key'),
119 | );
120 |
121 | if (entityTypes.length > 0) {
122 | schema = extendSchema(
123 | schema,
124 | parse(`
125 | union _Entity = ${entityTypes.join(' | ')}
126 | extend type Query {
127 | _entities(representations: [_Any!]!): [_Entity]!
128 | }
129 | `),
130 | {
131 | assumeValid: true,
132 | },
133 | );
134 |
135 | const query = schema.getType('Query') as GraphQLObjectType;
136 | const queryFields = query.getFields();
137 | queryFields._entities = {
138 | ...queryFields._entities,
139 | resolve: (_source, { representations }, context, info) => {
140 | return representations.map((reference) => {
141 | const { __typename } = reference;
142 |
143 | const type = info.schema.getType(__typename);
144 | if (!type || !isObjectType(type)) {
145 | throw new MER_ERR_GQL_GATEWAY_INVALID_SCHEMA(__typename);
146 | }
147 |
148 | const resolveReference = (type as any).resolveReference
149 | ? (type as any).resolveReference
150 | : function defaultResolveReference() {
151 | return reference;
152 | };
153 |
154 | const result = resolveReference(reference, {}, context, info);
155 |
156 | if (result && 'then' in result && typeof result.then === 'function') {
157 | return result.then((x) => addTypeNameToResult(x, __typename));
158 | }
159 |
160 | return addTypeNameToResult(result, __typename);
161 | });
162 | },
163 | };
164 | }
165 |
166 | return schema;
167 | }
168 |
--------------------------------------------------------------------------------
/lib/utils/is-loader-context.ts:
--------------------------------------------------------------------------------
1 | export function isLoaderContext(args: any[]) {
2 | return args.filter(Boolean).length === 2;
3 | }
4 |
--------------------------------------------------------------------------------
/lib/utils/merge-defaults.ts:
--------------------------------------------------------------------------------
1 | import { MercuriusModuleOptions } from '../interfaces';
2 | import { isFunction } from '@nestjs/common/utils/shared.utils';
3 |
4 | // TODO better define this
5 | const defaultOptions: MercuriusModuleOptions = {
6 | graphiql: true,
7 | routes: true,
8 | path: '/graphql',
9 | fieldResolverEnhancers: [],
10 | cache: true,
11 | };
12 |
13 | export function mergeDefaults(
14 | options: MercuriusModuleOptions,
15 | defaults: MercuriusModuleOptions = defaultOptions,
16 | ): MercuriusModuleOptions {
17 | const moduleOptions = {
18 | ...defaults,
19 | ...options,
20 | };
21 | if (!moduleOptions.context) {
22 | moduleOptions.context = (req, reply) => ({ req });
23 | } else if (isFunction(moduleOptions.context)) {
24 | moduleOptions.context = async (req, reply) => {
25 | const ctx = await (options.context as Function)(req, reply);
26 | return assignReqProperty(ctx, req);
27 | };
28 | } else {
29 | moduleOptions.context = (req, reply) => {
30 | return assignReqProperty(options.context as Record, req);
31 | };
32 | }
33 | return moduleOptions;
34 | }
35 |
36 | function assignReqProperty(
37 | ctx: Record | undefined,
38 | req: unknown,
39 | ) {
40 | if (!ctx) {
41 | return { req };
42 | }
43 | if (
44 | typeof ctx !== 'object' ||
45 | (ctx && ctx.req && typeof ctx.req === 'object')
46 | ) {
47 | return ctx;
48 | }
49 | ctx.req = req;
50 | return ctx;
51 | }
52 |
--------------------------------------------------------------------------------
/lib/utils/to-async-iterator.ts:
--------------------------------------------------------------------------------
1 | import { Readable } from 'stream';
2 |
3 | export type PubSubSubscribe =
4 | Promise>
5 | | (Readable & AsyncIterableIterator);
6 |
7 | export async function toAsyncIterator(subPromise: PubSubSubscribe) {
8 | return (await subPromise)[Symbol.asyncIterator]();
9 | }
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nestjs-mercurius",
3 | "version": "0.21.0",
4 | "main": "index.js",
5 | "repository": "https://@github.com:Davide-Gheri/nestjs-mercurius.git",
6 | "author": "Davide Gheri ",
7 | "license": "MIT",
8 | "scripts": {
9 | "format": "prettier **/**/*.ts --ignore-path ./.prettierignore --write",
10 | "lint": "eslint 'lib/**/*.ts'",
11 | "build": "rm -rf dist && tsc -p tsconfig.json",
12 | "precommit": "lint-staged",
13 | "prepublish:npm": "npm run build",
14 | "publish:npm": "npm publish --access public",
15 | "prepublish:next": "npm run build",
16 | "publish:next": "npm publish --access public --tag next",
17 | "prerelease": "npm run build",
18 | "release": "release-it",
19 | "test": "uvu -r ts-node/register tests/e2e",
20 | "postinstall": "husky install",
21 | "prepublishOnly": "pinst --disable",
22 | "postpublish": "pinst --enable"
23 | },
24 | "devDependencies": {
25 | "@apollo/gateway": "^0.42.3",
26 | "@apollo/subgraph": "^0.1.2",
27 | "@commitlint/cli": "^12.1.4",
28 | "@commitlint/config-angular": "^12.1.4",
29 | "@graphql-tools/utils": "^7.10.0",
30 | "@nestjs/common": "^8.0.4",
31 | "@nestjs/core": "^8.0.4",
32 | "@nestjs/graphql": "^9.1.1",
33 | "@nestjs/platform-fastify": "^8.0.4",
34 | "@nestjs/testing": "^8.0.4",
35 | "@release-it/conventional-changelog": "^3.0.1",
36 | "@types/graphql-upload": "^8.0.6",
37 | "@types/jest": "^26.0.22",
38 | "@types/supertest": "^2.0.11",
39 | "@typescript-eslint/eslint-plugin": "^4.28.4",
40 | "@typescript-eslint/parser": "^4.33.0",
41 | "altair-fastify-plugin": "^4.1.0",
42 | "apollo-server-core": "^3.0.0",
43 | "class-validator": "^0.13.1",
44 | "eslint": "^7.31.0",
45 | "eslint-config-prettier": "^8.1.0",
46 | "eslint-plugin-import": "^2.23.4",
47 | "fastify": "^3.22.1",
48 | "graphql": "^15.5.1",
49 | "graphql-scalars": "^1.10.0",
50 | "husky": "^7.0.1",
51 | "lint-staged": "^11.1.0",
52 | "mercurius": "^8.8.0",
53 | "mercurius-integration-testing": "^3.2.0",
54 | "mercurius-upload": "^2.0.0",
55 | "pinst": "^2.1.6",
56 | "prettier": "^2.3.2",
57 | "reflect-metadata": "^0.1.13",
58 | "release-it": "^14.10.0",
59 | "rxjs": "^7.2.0",
60 | "supertest": "^6.1.4",
61 | "ts-morph": "^11.0.3",
62 | "ts-node": "^9.1.1",
63 | "typescript": "^4.3.5",
64 | "uvu": "^0.5.1",
65 | "ws": "^8.2.2"
66 | },
67 | "peerDependencies": {
68 | "@apollo/subgraph": "~0.1.2",
69 | "@nestjs/common": "^8.0.4",
70 | "@nestjs/core": "^8.0.4",
71 | "@nestjs/graphql": "~9.1.1 | ^8.0.2",
72 | "altair-fastify-plugin": "^4.1.0",
73 | "apollo-server-core": "^3.0.0",
74 | "graphql": "^15.5.1",
75 | "mercurius": "^8.8.0",
76 | "mercurius-upload": "^2.0.0",
77 | "reflect-metadata": "^0.1.13"
78 | },
79 | "peerDependenciesMeta": {
80 | "@apollo/subgraph": {
81 | "optional": true
82 | },
83 | "altair-fastify-plugin": {
84 | "optional": true
85 | },
86 | "mercurius-upload": {
87 | "optional": true
88 | }
89 | },
90 | "lint-staged": {
91 | "*.ts": [
92 | "prettier --write"
93 | ]
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/tests/code-first/animal.interface.ts:
--------------------------------------------------------------------------------
1 | import { Field, ID, InterfaceType } from '@nestjs/graphql';
2 | import { Species } from './species.enum';
3 |
4 | @InterfaceType({
5 | resolveType(animal) {
6 | switch (animal.species) {
7 | case Species.DOG:
8 | return import('./types/dog').then((module) => module.Dog);
9 | case Species.CAT:
10 | return import('./types/cat').then((module) => module.Cat);
11 | }
12 | },
13 | })
14 | export class Animal {
15 | @Field(() => ID)
16 | id: number;
17 |
18 | @Field(() => Species)
19 | species: Species;
20 | }
21 |
--------------------------------------------------------------------------------
/tests/code-first/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { MercuriusModule } from '../../lib';
3 | import { CatService } from './services/cat.service';
4 | import { DogService } from './services/dog.service';
5 | import { CatResolver } from './resolvers/cat.resolver';
6 | import { AnimalResolver } from './resolvers/animal.resolver';
7 | import { UpperCaseDirective } from '../example/directives/upper-case.directive';
8 |
9 | @Module({
10 | imports: [
11 | MercuriusModule.forRoot({
12 | autoSchemaFile: true,
13 | schemaDirectives: {
14 | uppercase: UpperCaseDirective,
15 | },
16 | subscription: true,
17 | }),
18 | ],
19 | providers: [CatService, DogService, AnimalResolver, CatResolver],
20 | exports: [CatService],
21 | })
22 | export class AppModule {}
23 |
--------------------------------------------------------------------------------
/tests/code-first/directives/upper-case.directive.ts:
--------------------------------------------------------------------------------
1 | import { SchemaDirectiveVisitor } from '@graphql-tools/utils';
2 | import {
3 | GraphQLField,
4 | GraphQLInterfaceType,
5 | GraphQLObjectType,
6 | defaultFieldResolver,
7 | } from 'graphql';
8 |
9 | export class UpperCaseDirective extends SchemaDirectiveVisitor {
10 | name = 'uppercase';
11 | visitFieldDefinition(
12 | field: GraphQLField,
13 | _details: { objectType: GraphQLObjectType | GraphQLInterfaceType },
14 | ): GraphQLField | void | null {
15 | const { resolve = defaultFieldResolver } = field;
16 | field.resolve = async function (...args) {
17 | const result = await resolve.apply(this, args);
18 | if (typeof result === 'string') {
19 | return result.toUpperCase();
20 | }
21 | return result;
22 | };
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/code-first/inputs/create-cat.input.ts:
--------------------------------------------------------------------------------
1 | import { Field, InputType, Int } from '@nestjs/graphql';
2 |
3 | @InputType()
4 | export class CreateCatInput {
5 | @Field(() => Int, { defaultValue: 9 })
6 | lives: number;
7 |
8 | @Field()
9 | name: string;
10 | }
11 |
--------------------------------------------------------------------------------
/tests/code-first/inputs/create-dog.input.ts:
--------------------------------------------------------------------------------
1 | import { Field, InputType, Int } from '@nestjs/graphql';
2 |
3 | @InputType()
4 | export class CreateDogInput {
5 | @Field(() => Int)
6 | age: number;
7 | }
8 |
--------------------------------------------------------------------------------
/tests/code-first/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 | import {
4 | FastifyAdapter,
5 | NestFastifyApplication,
6 | } from '@nestjs/platform-fastify';
7 |
8 | async function bootstrap() {
9 | const app = await NestFactory.create(
10 | AppModule,
11 | new FastifyAdapter(),
12 | );
13 | await app.listen(3000);
14 | }
15 | bootstrap();
16 |
--------------------------------------------------------------------------------
/tests/code-first/resolvers/animal.resolver.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Args,
3 | createUnionType,
4 | Parent,
5 | Query,
6 | ResolveField,
7 | Resolver,
8 | } from '@nestjs/graphql';
9 | import { Animal } from '../animal.interface';
10 | import { Dog } from '../types/dog';
11 | import { Cat } from '../types/cat';
12 | import { Species } from '../species.enum';
13 | import { CatService } from '../services/cat.service';
14 | import { DogService } from '../services/dog.service';
15 | import { LoaderQuery, ResolveLoader } from '../../../lib';
16 |
17 | export const DomesticAnimal = createUnionType({
18 | name: 'DomesticAnimal',
19 | types: () => [Dog, Cat],
20 | resolveType: (value: Dog | Cat) => {
21 | switch (value.species) {
22 | case Species.CAT:
23 | return Cat;
24 | case Species.DOG:
25 | return Dog;
26 | default:
27 | return null;
28 | }
29 | },
30 | });
31 |
32 | @Resolver(() => Animal)
33 | export class AnimalResolver {
34 | constructor(
35 | private readonly catService: CatService,
36 | private readonly dogService: DogService,
37 | ) {}
38 |
39 | @Query(() => [Animal])
40 | animals() {
41 | return [...this.dogService.dogs(), ...this.catService.cats()];
42 | }
43 |
44 | @Query(() => [DomesticAnimal])
45 | domesticAnimals(
46 | @Args({ name: 'species', type: () => Species, nullable: true })
47 | species?: Species,
48 | ) {
49 | switch (species) {
50 | case Species.DOG:
51 | return this.dogService.dogs();
52 | case Species.CAT:
53 | return this.catService.cats();
54 | default:
55 | return [...this.dogService.dogs(), ...this.catService.cats()];
56 | }
57 | }
58 |
59 | @ResolveField(() => Boolean)
60 | hasPaws(@Parent() animal: Animal) {
61 | return true;
62 | }
63 |
64 | @ResolveLoader(() => String)
65 | aField(@Parent() queries: LoaderQuery[]) {
66 | return queries.map(({ obj }) => 'lorem');
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/tests/code-first/resolvers/cat.resolver.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Args,
3 | Context,
4 | ID,
5 | Parent,
6 | Query,
7 | Resolver,
8 | Subscription,
9 | } from '@nestjs/graphql';
10 | import { Cat } from '../types/cat';
11 | import { CatService } from '../services/cat.service';
12 | import { ParseIntPipe } from '@nestjs/common';
13 | import { LoaderQuery, ResolveLoader, toAsyncIterator } from '../../../lib';
14 | import { PubSub } from 'mercurius';
15 |
16 | @Resolver(() => Cat)
17 | export class CatResolver {
18 | constructor(private readonly catService: CatService) {}
19 |
20 | @Query(() => [Cat])
21 | cats() {
22 | return this.catService.cats();
23 | }
24 |
25 | @Query(() => Cat, { nullable: true })
26 | cat(@Args({ name: 'id', type: () => ID }, ParseIntPipe) id: number) {
27 | return this.catService.cat(id);
28 | }
29 |
30 | @ResolveLoader(() => Boolean)
31 | hasFur(@Parent() queries: LoaderQuery[]) {
32 | return queries.map(({ obj }) => obj.lives > 1);
33 | }
34 |
35 | @Subscription(() => Cat, {
36 | resolve: (payload) => payload,
37 | })
38 | onCatSub(@Context('pubsub') pubSub: PubSub) {
39 | return toAsyncIterator(pubSub.subscribe('CAT_SUB_TOPIC'));
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/tests/code-first/services/cat.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { Cat } from '../types/cat';
3 | import { Species } from '../species.enum';
4 | import { CreateCatInput } from '../inputs/create-cat.input';
5 |
6 | export const cats: Cat[] = [
7 | {
8 | id: 1,
9 | species: Species.CAT,
10 | lives: 9,
11 | name: 'fufy',
12 | },
13 | {
14 | id: 2,
15 | species: Species.CAT,
16 | lives: 9,
17 | name: 'tigger',
18 | },
19 | ];
20 |
21 | let nextId = 3;
22 |
23 | @Injectable()
24 | export class CatService {
25 | cats() {
26 | return cats;
27 | }
28 |
29 | cat(id: number) {
30 | return cats.find((c) => c.id === id);
31 | }
32 |
33 | createCat(data: CreateCatInput) {
34 | const cat: Cat = {
35 | ...data,
36 | species: Species.CAT,
37 | id: nextId,
38 | };
39 | nextId++;
40 | cats.push(cat);
41 | return cat;
42 | }
43 |
44 | deleteCat(id: number) {
45 | const idx = cats.findIndex((c) => c.id === id);
46 | if (idx < 0) {
47 | return false;
48 | }
49 | cats.splice(idx, 1);
50 | return true;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/tests/code-first/services/dog.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { Species } from '../species.enum';
3 | import { Dog } from '../types/dog';
4 | import { CreateDogInput } from '../inputs/create-dog.input';
5 |
6 | export const dogs: Dog[] = [
7 | {
8 | id: 1,
9 | species: Species.DOG,
10 | age: 2,
11 | },
12 | {
13 | id: 2,
14 | species: Species.DOG,
15 | age: 5,
16 | },
17 | ];
18 |
19 | let nextId = 3;
20 |
21 | @Injectable()
22 | export class DogService {
23 | dogs() {
24 | return dogs;
25 | }
26 |
27 | dog(id: number) {
28 | dogs.find((c) => c.id === id);
29 | }
30 |
31 | createDog(data: CreateDogInput) {
32 | const dog: Dog = {
33 | ...data,
34 | species: Species.DOG,
35 | id: nextId,
36 | };
37 | nextId++;
38 | dogs.push(dog);
39 | return dogs;
40 | }
41 |
42 | deleteDog(id: number) {
43 | const idx = dogs.findIndex((c) => c.id === id);
44 | if (idx < 0) {
45 | return false;
46 | }
47 | dogs.splice(idx, 1);
48 | return true;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/tests/code-first/species.enum.ts:
--------------------------------------------------------------------------------
1 | import { registerEnumType } from '@nestjs/graphql';
2 |
3 | export enum Species {
4 | CAT = 'CAT',
5 | DOG = 'DOG',
6 | }
7 |
8 | registerEnumType(Species, {
9 | name: 'Species',
10 | });
11 |
--------------------------------------------------------------------------------
/tests/code-first/types/cat.ts:
--------------------------------------------------------------------------------
1 | import { Directive, Field, ID, Int, ObjectType } from '@nestjs/graphql';
2 | import { Animal } from '../animal.interface';
3 | import { Species } from '../species.enum';
4 |
5 | @ObjectType({
6 | implements: [Animal],
7 | })
8 | export class Cat implements Animal {
9 | @Field(() => ID)
10 | id: number;
11 |
12 | @Directive('@uppercase')
13 | @Field()
14 | name: string;
15 |
16 | @Field(() => Species)
17 | species: Species;
18 |
19 | @Field(() => Int)
20 | lives: number;
21 | }
22 |
--------------------------------------------------------------------------------
/tests/code-first/types/dog.ts:
--------------------------------------------------------------------------------
1 | import { Field, ID, Int, ObjectType } from '@nestjs/graphql';
2 | import { Animal } from '../animal.interface';
3 | import { Species } from '../species.enum';
4 |
5 | @ObjectType({
6 | implements: [Animal],
7 | })
8 | export class Dog implements Animal {
9 | @Field(() => ID)
10 | id: number;
11 |
12 | @Field(() => Species)
13 | species: Species;
14 |
15 | @Field(() => Int)
16 | age: number;
17 | }
18 |
--------------------------------------------------------------------------------
/tests/e2e/code-first-federation.spec.ts:
--------------------------------------------------------------------------------
1 | import { suite } from 'uvu';
2 | import * as assert from 'uvu/assert';
3 | import { Test } from '@nestjs/testing';
4 | import * as request from 'supertest';
5 | import {
6 | FastifyAdapter,
7 | NestFastifyApplication,
8 | } from '@nestjs/platform-fastify';
9 | import { createTestClient } from '../utils/create-test-client';
10 | import { AppModule } from '../federation/userService/app.module';
11 | import { getIntrospectionQuery } from 'graphql';
12 |
13 | interface Context {
14 | app: NestFastifyApplication;
15 | mercuriusClient: ReturnType;
16 | }
17 |
18 | const graphqlSuite = suite('Code-first federation service');
19 |
20 | graphqlSuite.before.each(async (ctx) => {
21 | const module = await Test.createTestingModule({
22 | imports: [AppModule],
23 | }).compile();
24 |
25 | const app = module.createNestApplication(
26 | new FastifyAdapter(),
27 | );
28 | await app.init();
29 | ctx.app = app;
30 | ctx.mercuriusClient = createTestClient(module);
31 | });
32 |
33 | graphqlSuite.after.each(async (ctx) => {
34 | await ctx.app.close();
35 | });
36 |
37 | graphqlSuite(
38 | 'should define specific federation directives',
39 | async ({ app }) => {
40 | const {
41 | body: { data },
42 | } = await request(app.getHttpServer())
43 | .post('/graphql')
44 | .send({
45 | operationName: null,
46 | variables: null,
47 | query: getIntrospectionQuery(),
48 | })
49 | .expect(200);
50 |
51 | assert.ok(data && data.__schema);
52 | const schema = data.__schema;
53 |
54 | const federationDirectives = ['key', 'external', 'provides', 'requires'];
55 | const schemaDirectives = schema.directives.map((d) => d.name);
56 |
57 | federationDirectives.forEach((dir) => {
58 | assert.ok(schemaDirectives.includes(dir), `Missing directive ${dir}`);
59 | });
60 | },
61 | );
62 |
63 | graphqlSuite('should expose _service Query', async ({ app }) => {
64 | const {
65 | body: { data },
66 | } = await request(app.getHttpServer())
67 | .post('/graphql')
68 | .send({
69 | operationName: null,
70 | variables: null,
71 | query: `
72 | {
73 | _service {
74 | sdl
75 | }
76 | }
77 | `,
78 | })
79 | .expect(200);
80 |
81 | assert.ok(data && data._service.sdl);
82 | assert.type(data._service.sdl, 'string');
83 | });
84 |
85 | graphqlSuite('should expose _entities Query', async ({ app }) => {
86 | await request(app.getHttpServer())
87 | .post('/graphql')
88 | .send({
89 | operationName: null,
90 | variables: {
91 | representations: [
92 | {
93 | __typename: 'User',
94 | id: '1',
95 | },
96 | ],
97 | },
98 | query: `
99 | query($representations: [_Any!]!) {
100 | _entities(representations: $representations) {
101 | ...on User { id name }
102 | }
103 | }
104 | `,
105 | })
106 | .expect(200, {
107 | data: {
108 | _entities: [
109 | {
110 | id: '1',
111 | name: 'u1',
112 | },
113 | ],
114 | },
115 | });
116 | });
117 |
118 | graphqlSuite.run();
119 |
--------------------------------------------------------------------------------
/tests/e2e/code-first.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | FastifyAdapter,
3 | NestFastifyApplication,
4 | } from '@nestjs/platform-fastify';
5 | import { createTestClient } from '../utils/create-test-client';
6 | import { suite } from 'uvu';
7 | import * as assert from 'uvu/assert';
8 | import { Test } from '@nestjs/testing';
9 | import { AppModule } from '../code-first/app.module';
10 | import * as Websocket from 'ws';
11 | import { FastifyInstance } from 'fastify';
12 | import { cats } from '../code-first/services/cat.service';
13 |
14 | interface Context {
15 | app: NestFastifyApplication;
16 | mercuriusClient: ReturnType;
17 | }
18 |
19 | const expectedCats = [
20 | {
21 | id: '1',
22 | species: 'CAT',
23 | lives: 9,
24 | },
25 | {
26 | id: '2',
27 | species: 'CAT',
28 | lives: 9,
29 | },
30 | ];
31 |
32 | const expectedDogs = [
33 | {
34 | id: '1',
35 | species: 'DOG',
36 | age: 2,
37 | },
38 | {
39 | id: '2',
40 | species: 'DOG',
41 | age: 5,
42 | },
43 | ];
44 |
45 | const gqlSuite = suite('Code-first');
46 |
47 | gqlSuite.before.each(async (ctx) => {
48 | const module = await Test.createTestingModule({
49 | imports: [AppModule],
50 | }).compile();
51 |
52 | const app = module.createNestApplication(
53 | new FastifyAdapter(),
54 | );
55 | await app.init();
56 | ctx.app = app;
57 | ctx.mercuriusClient = createTestClient(module);
58 | });
59 |
60 | gqlSuite.after.each(async (ctx) => {
61 | await ctx.app.close();
62 | });
63 |
64 | gqlSuite('should return a list of cats', async ({ mercuriusClient }) => {
65 | const response = await mercuriusClient.query(`
66 | query cats{
67 | cats {
68 | id
69 | species
70 | lives
71 | }
72 | }
73 | `);
74 |
75 | assert.equal(response.data, {
76 | cats: expectedCats,
77 | });
78 | });
79 |
80 | gqlSuite('should return Loader values', async ({ mercuriusClient }) => {
81 | const response = await mercuriusClient.query(
82 | `
83 | query cat($id: ID!) {
84 | cat(id: $id) {
85 | hasFur
86 | }
87 | }
88 | `,
89 | {
90 | variables: { id: 1 },
91 | },
92 | );
93 |
94 | assert.equal(response.data, {
95 | cat: {
96 | hasFur: true,
97 | },
98 | });
99 | });
100 |
101 | gqlSuite('should apply schema directives', async ({ mercuriusClient }) => {
102 | const response = await mercuriusClient.query(
103 | `
104 | query cat($id: ID!) {
105 | cat(id: $id) {
106 | name
107 | }
108 | }
109 | `,
110 | {
111 | variables: { id: 1 },
112 | },
113 | );
114 |
115 | assert.equal(response.data, {
116 | cat: {
117 | name: 'FUFY',
118 | },
119 | });
120 | });
121 |
122 | gqlSuite(
123 | 'should return interface fieldResolver value',
124 | async ({ mercuriusClient }) => {
125 | const response = await mercuriusClient.query(
126 | `
127 | query cat($id: ID!) {
128 | cat(id: $id) {
129 | hasPaws
130 | }
131 | }
132 | `,
133 | {
134 | variables: { id: 1 },
135 | },
136 | );
137 |
138 | assert.equal(response.data, {
139 | cat: {
140 | hasPaws: true,
141 | },
142 | });
143 | },
144 | );
145 |
146 | gqlSuite(
147 | 'should return interface loader value',
148 | async ({ mercuriusClient }) => {
149 | const response = await mercuriusClient.query(
150 | `
151 | query cat($id: ID!) {
152 | cat(id: $id) {
153 | aField
154 | }
155 | }
156 | `,
157 | {
158 | variables: { id: 1 },
159 | },
160 | );
161 |
162 | assert.equal(response.data, {
163 | cat: {
164 | aField: 'lorem',
165 | },
166 | });
167 | },
168 | );
169 |
170 | gqlSuite('should return union', async ({ mercuriusClient }) => {
171 | const response = await mercuriusClient.query(`
172 | query {
173 | domesticAnimals {
174 | ... on Animal {
175 | id
176 | species
177 | hasPaws
178 | }
179 | ...on Dog {
180 | age
181 | }
182 | ... on Cat {
183 | lives
184 | }
185 | }
186 | }
187 | `);
188 |
189 | assert.equal(response.data, {
190 | domesticAnimals: [
191 | ...expectedDogs.map((d) => ({
192 | ...d,
193 | hasPaws: true,
194 | })),
195 | ...expectedCats.map((c) => ({
196 | ...c,
197 | hasPaws: true,
198 | })),
199 | ],
200 | });
201 | });
202 |
203 | gqlSuite('should filter union return', async ({ mercuriusClient }) => {
204 | const response = await mercuriusClient.query(`
205 | query {
206 | domesticAnimals(species: DOG) {
207 | ... on Animal {
208 | id
209 | species
210 | hasPaws
211 | }
212 | ...on Dog {
213 | age
214 | }
215 | ... on Cat {
216 | lives
217 | }
218 | }
219 | }
220 | `);
221 |
222 | assert.equal(response.data, {
223 | domesticAnimals: [
224 | ...expectedDogs.map((d) => ({
225 | ...d,
226 | hasPaws: true,
227 | })),
228 | ],
229 | });
230 | });
231 |
232 | gqlSuite(
233 | 'should return the interface resolved types',
234 | async ({ mercuriusClient }) => {
235 | const response = await mercuriusClient.query(`
236 | query {
237 | animals {
238 | ... on Animal {
239 | id
240 | species
241 | hasPaws
242 | }
243 | ...on Dog {
244 | age
245 | }
246 | ... on Cat {
247 | lives
248 | }
249 | }
250 | }
251 | `);
252 |
253 | assert.equal(response.data, {
254 | animals: [
255 | ...expectedDogs.map((d) => ({
256 | ...d,
257 | hasPaws: true,
258 | })),
259 | ...expectedCats.map((c) => ({
260 | ...c,
261 | hasPaws: true,
262 | })),
263 | ],
264 | });
265 | },
266 | );
267 |
268 | gqlSuite('subscriber should work', async ({ app }) => {
269 | return new Promise((resolve, reject) => {
270 | app.listen(0, (err) => {
271 | if (err) {
272 | return reject(err);
273 | }
274 | const port = app.getHttpServer().address().port;
275 | const fastifyApp = app.getHttpAdapter().getInstance() as FastifyInstance;
276 |
277 | const ws = new Websocket(`ws://localhost:${port}/graphql`, 'graphql-ws');
278 |
279 | const client = Websocket.createWebSocketStream(ws, {
280 | encoding: 'utf8',
281 | objectMode: true,
282 | });
283 | client.setEncoding('utf8');
284 | client.write(
285 | JSON.stringify({
286 | type: 'connection_init',
287 | }),
288 | );
289 |
290 | client.write(
291 | JSON.stringify({
292 | id: 1,
293 | type: 'start',
294 | payload: {
295 | query: `
296 | subscription {
297 | onCatSub {
298 | id
299 | lives
300 | name
301 | hasFur
302 | }
303 | }
304 | `,
305 | },
306 | }),
307 | );
308 |
309 | client.on('data', (chunk) => {
310 | const data = JSON.parse(chunk);
311 |
312 | if (data.type === 'connection_ack') {
313 | fastifyApp.graphql.pubsub.publish({
314 | topic: 'CAT_SUB_TOPIC',
315 | payload: cats[0],
316 | });
317 | } else if (data.id === 1) {
318 | const expectedCat = expectedCats[0];
319 | const receivedCat = data.payload.data?.onCatSub;
320 | assert.ok(receivedCat);
321 | assert.equal(expectedCat.id, receivedCat.id);
322 | assert.type(receivedCat.hasFur, 'boolean');
323 |
324 | client.end();
325 | }
326 | });
327 |
328 | client.on('end', () => {
329 | client.destroy();
330 | app.close().then(resolve).catch(reject);
331 | });
332 | });
333 | });
334 | });
335 |
336 | gqlSuite.run();
337 |
--------------------------------------------------------------------------------
/tests/e2e/gateway.spec.ts:
--------------------------------------------------------------------------------
1 | import {
2 | FastifyAdapter,
3 | NestFastifyApplication,
4 | } from '@nestjs/platform-fastify';
5 | import { createTestClient } from '../utils/create-test-client';
6 | import { suite } from 'uvu';
7 | import { Test } from '@nestjs/testing';
8 | import { AppModule as GatewayModule } from '../federation/gateway/app.module';
9 | import { AppModule as UserModule } from '../federation/userService/app.module';
10 | import { AppModule as PostModule } from '../federation/postService/app.module';
11 | import { NestFactory } from '@nestjs/core';
12 | import { getIntrospectionQuery } from 'graphql';
13 | import { INestApplication } from '@nestjs/common';
14 | import * as request from 'supertest';
15 |
16 | interface Context {
17 | app: NestFastifyApplication;
18 | mercuriusClient: ReturnType;
19 | services: INestApplication[];
20 | }
21 |
22 | const graphqlSuite = suite('Gateway');
23 |
24 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
25 |
26 | graphqlSuite.before.each(async (ctx) => {
27 | const userApp = await NestFactory.create(UserModule, new FastifyAdapter());
28 | const postApp = await NestFactory.create(PostModule, new FastifyAdapter());
29 |
30 | await Promise.all([userApp.listen(3001), postApp.listen(3002)]);
31 |
32 | ctx.services = [userApp, postApp];
33 |
34 | await sleep(500);
35 |
36 | const module = await Test.createTestingModule({
37 | imports: [GatewayModule],
38 | }).compile();
39 |
40 | const app = module.createNestApplication(
41 | new FastifyAdapter(),
42 | );
43 | await app.init();
44 | ctx.app = app;
45 | ctx.mercuriusClient = createTestClient(module);
46 | });
47 |
48 | graphqlSuite.after.each(async (ctx) => {
49 | await ctx.app.close();
50 | await Promise.all(ctx.services.map((svc) => svc.close()));
51 | });
52 |
53 | graphqlSuite('Should start', async (t) => {
54 | await request(t.app.getHttpServer())
55 | .post('/graphql')
56 | .send({
57 | query: getIntrospectionQuery(),
58 | })
59 | .expect(200);
60 | });
61 |
62 | graphqlSuite('Should call service query', async (t) => {
63 | await request(t.app.getHttpServer())
64 | .post('/graphql')
65 | .send({
66 | query: `
67 | {
68 | users {
69 | id
70 | name
71 | }
72 | }
73 | `,
74 | })
75 | .expect(200, {
76 | data: {
77 | users: [
78 | {
79 | id: '1',
80 | name: 'u1',
81 | },
82 | {
83 | id: '2',
84 | name: 'u2',
85 | },
86 | ],
87 | },
88 | });
89 | });
90 |
91 | graphqlSuite('Should call another service query', async (t) => {
92 | await request(t.app.getHttpServer())
93 | .post('/graphql')
94 | .send({
95 | query: `
96 | {
97 | posts {
98 | id
99 | title
100 | authorId
101 | }
102 | }
103 | `,
104 | })
105 | .expect(200, {
106 | data: {
107 | posts: [
108 | {
109 | id: '1',
110 | title: 'p1',
111 | authorId: 1,
112 | },
113 | {
114 | id: '2',
115 | title: 'p2',
116 | authorId: 1,
117 | },
118 | ],
119 | },
120 | });
121 | });
122 |
123 | graphqlSuite.run();
124 |
--------------------------------------------------------------------------------
/tests/e2e/graphql-async-class.spec.ts:
--------------------------------------------------------------------------------
1 | import { suite } from 'uvu';
2 | import * as request from 'supertest';
3 | import { AsyncClassApplicationModule } from '../graphql/async-options-class.module';
4 | import {
5 | FastifyAdapter,
6 | NestFastifyApplication,
7 | } from '@nestjs/platform-fastify';
8 | import { Test } from '@nestjs/testing';
9 | import { FastifyInstance } from 'fastify';
10 |
11 | interface Context {
12 | app: NestFastifyApplication;
13 | }
14 |
15 | const graphqlSuite = suite('GraphQL (async class)');
16 |
17 | graphqlSuite.before.each(async (ctx) => {
18 | const module = await Test.createTestingModule({
19 | imports: [AsyncClassApplicationModule],
20 | }).compile();
21 |
22 | const app = module.createNestApplication(
23 | new FastifyAdapter(),
24 | );
25 |
26 | await app.init();
27 | const fastify: FastifyInstance = app.getHttpAdapter().getInstance();
28 | await fastify.ready();
29 | ctx.app = app;
30 | });
31 |
32 | graphqlSuite.after.each(async ({ app }) => {
33 | await app.close();
34 | });
35 |
36 | graphqlSuite(`should return query result`, async ({ app }) => {
37 | await request(app.getHttpServer())
38 | .post('/graphql')
39 | .send({
40 | operationName: null,
41 | variables: {},
42 | query: '{\n getCats {\n id\n }\n}\n',
43 | })
44 | .expect(200, {
45 | data: {
46 | getCats: [
47 | {
48 | id: 1,
49 | },
50 | ],
51 | },
52 | });
53 | });
54 |
55 | graphqlSuite.run();
56 |
--------------------------------------------------------------------------------
/tests/e2e/graphql-async.spec.ts:
--------------------------------------------------------------------------------
1 | import { suite } from 'uvu';
2 | import * as request from 'supertest';
3 | import {
4 | FastifyAdapter,
5 | NestFastifyApplication,
6 | } from '@nestjs/platform-fastify';
7 | import { Test } from '@nestjs/testing';
8 | import { FastifyInstance } from 'fastify';
9 | import { AsyncApplicationModule } from '../graphql/async-options.module';
10 |
11 | interface Context {
12 | app: NestFastifyApplication;
13 | }
14 |
15 | const graphqlSuite = suite('GraphQL (async configuration)');
16 |
17 | graphqlSuite.before.each(async (ctx) => {
18 | const module = await Test.createTestingModule({
19 | imports: [AsyncApplicationModule],
20 | }).compile();
21 |
22 | const app = module.createNestApplication(
23 | new FastifyAdapter(),
24 | );
25 |
26 | await app.init();
27 | const fastify: FastifyInstance = app.getHttpAdapter().getInstance();
28 | await fastify.ready();
29 | ctx.app = app;
30 | });
31 |
32 | graphqlSuite.after.each(async ({ app }) => {
33 | await app.close();
34 | });
35 |
36 | graphqlSuite(`should return query result`, async ({ app }) => {
37 | await request(app.getHttpServer())
38 | .post('/graphql')
39 | .send({
40 | operationName: null,
41 | variables: {},
42 | query: '{\n getCats {\n id\n }\n}\n',
43 | })
44 | .expect(200, {
45 | data: {
46 | getCats: [
47 | {
48 | id: 1,
49 | },
50 | ],
51 | },
52 | });
53 | });
54 |
55 | graphqlSuite.run();
56 |
--------------------------------------------------------------------------------
/tests/e2e/graphql-request-scoped.spec.ts:
--------------------------------------------------------------------------------
1 | import { suite } from 'uvu';
2 | import * as assert from 'uvu/assert';
3 | import { Test } from '@nestjs/testing';
4 | import {
5 | FastifyAdapter,
6 | NestFastifyApplication,
7 | } from '@nestjs/platform-fastify';
8 | import { FastifyInstance } from 'fastify';
9 | import { join } from 'path';
10 | import * as request from 'supertest';
11 | import { MercuriusModule } from '../../lib';
12 | import { CatsRequestScopedService } from '../graphql/cats/cats-request-scoped.service';
13 | import { CatsModule } from '../graphql/cats/cats.module';
14 |
15 | interface Context {
16 | app: NestFastifyApplication;
17 | }
18 |
19 | const graphqlSuite = suite('GraphQL request scoped');
20 |
21 | graphqlSuite.before.each(async (ctx) => {
22 | const module = await Test.createTestingModule({
23 | imports: [
24 | CatsModule.enableRequestScope(),
25 | MercuriusModule.forRoot({
26 | typePaths: [join(__dirname, '..', 'graphql', '**', '*.graphql')],
27 | }),
28 | ],
29 | }).compile();
30 |
31 | const app = module.createNestApplication(
32 | new FastifyAdapter(),
33 | );
34 |
35 | await app.init();
36 | const fastify: FastifyInstance = app.getHttpAdapter().getInstance();
37 | await fastify.ready();
38 | ctx.app = app;
39 |
40 | const performHttpCall = (end) =>
41 | request(app.getHttpServer())
42 | .post('/graphql')
43 | .send({
44 | operationName: null,
45 | variables: {},
46 | query: '{\n getCats {\n id\n }\n}\n',
47 | })
48 | .expect(200, {
49 | data: {
50 | getCats: [
51 | {
52 | id: 1,
53 | },
54 | ],
55 | },
56 | })
57 | .end((err, res) => {
58 | if (err) return end(err);
59 | end();
60 | });
61 |
62 | await new Promise((resolve) => performHttpCall(resolve));
63 | await new Promise((resolve) => performHttpCall(resolve));
64 | await new Promise((resolve) => performHttpCall(resolve));
65 | });
66 |
67 | graphqlSuite.after.each(async ({ app }) => {
68 | await app.close();
69 | });
70 |
71 | graphqlSuite('should create resolver for each incoming request', () => {
72 | assert.equal(CatsRequestScopedService.COUNTER, 3);
73 | });
74 |
75 | graphqlSuite.run();
76 |
--------------------------------------------------------------------------------
/tests/e2e/graphql.spec.ts:
--------------------------------------------------------------------------------
1 | import { suite } from 'uvu';
2 | import { Test } from '@nestjs/testing';
3 | import {
4 | FastifyAdapter,
5 | NestFastifyApplication,
6 | } from '@nestjs/platform-fastify';
7 | import * as request from 'supertest';
8 | import { ApplicationModule } from '../graphql/app.module';
9 | import { FastifyInstance } from 'fastify';
10 |
11 | interface Context {
12 | app: NestFastifyApplication;
13 | }
14 |
15 | const graphqlSuite = suite('GraphQL');
16 |
17 | graphqlSuite.before.each(async (ctx) => {
18 | const module = await Test.createTestingModule({
19 | imports: [ApplicationModule],
20 | }).compile();
21 |
22 | const app = module.createNestApplication(
23 | new FastifyAdapter(),
24 | );
25 |
26 | await app.init();
27 | const fastify: FastifyInstance = app.getHttpAdapter().getInstance();
28 | await fastify.ready();
29 | ctx.app = app;
30 | });
31 |
32 | graphqlSuite.after.each(async ({ app }) => {
33 | await app.close();
34 | });
35 |
36 | graphqlSuite(`should return query result`, async ({ app }) => {
37 | await request(app.getHttpServer())
38 | .post('/graphql')
39 | .send({
40 | operationName: null,
41 | variables: {},
42 | query: `
43 | {
44 | getCats {
45 | id,
46 | color,
47 | weight
48 | }
49 | }`,
50 | })
51 | .expect(200, {
52 | data: {
53 | getCats: [
54 | {
55 | id: 1,
56 | color: 'black',
57 | weight: 5,
58 | },
59 | ],
60 | },
61 | });
62 | });
63 |
64 | graphqlSuite.run();
65 |
--------------------------------------------------------------------------------
/tests/e2e/hook.spec.ts:
--------------------------------------------------------------------------------
1 | import { suite } from 'uvu';
2 | import * as assert from 'uvu/assert';
3 | import { Injectable } from '@nestjs/common';
4 | import { AppModule } from '../code-first/app.module';
5 | import { Test } from '@nestjs/testing';
6 | import {
7 | FastifyAdapter,
8 | NestFastifyApplication,
9 | } from '@nestjs/platform-fastify';
10 | import { createTestClient } from '../utils/create-test-client';
11 | import { GraphQLHook } from '../../lib';
12 | import { CatService } from '../code-first/services/cat.service';
13 |
14 | interface Context {
15 | app: NestFastifyApplication;
16 | mercuriusClient: ReturnType;
17 | }
18 |
19 | const gqlSuite = suite('Hook');
20 |
21 | gqlSuite('should call hooks', async () => {
22 | @Injectable()
23 | class HookService {
24 | constructor(private readonly catService: CatService) {}
25 |
26 | @GraphQLHook('preParsing')
27 | async preParsing(schema, source) {
28 | assert.snapshot(source, 'query { cats { id } }', 'SNAPSHOT_NOT_MATCH');
29 | }
30 | }
31 |
32 | const module = await Test.createTestingModule({
33 | imports: [AppModule],
34 | providers: [HookService],
35 | }).compile();
36 |
37 | const app = module.createNestApplication(
38 | new FastifyAdapter(),
39 | );
40 | await app.init();
41 | const mercuriusClient = createTestClient(module);
42 |
43 | const { errors } = await mercuriusClient.query('query { cats { id } }');
44 | // Nestjs swallows assert errors, need to check for the response
45 | assert.equal(errors, undefined);
46 | await app.close();
47 | });
48 |
49 | gqlSuite.run();
50 |
--------------------------------------------------------------------------------
/tests/example/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get } from '@nestjs/common';
2 |
3 | @Controller()
4 | export class AppController {
5 | @Get()
6 | home(): string {
7 | return 'ok';
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tests/example/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { AppController } from './app.controller';
3 | import { MercuriusModule } from '../../lib';
4 | import { ImageResolver } from './resolvers/image.resolver';
5 | import { HashScalar } from './scalars/hash.scalar';
6 | import { JSONResolver } from 'graphql-scalars';
7 | import { UpperCaseDirective } from './directives/upper-case.directive';
8 | import { UserModule } from './modules/user/user.module';
9 |
10 | @Module({
11 | imports: [
12 | MercuriusModule.forRootAsync({
13 | useFactory: () => {
14 | return {
15 | autoSchemaFile: './schema.graphql',
16 | fieldResolverEnhancers: ['guards', 'interceptors', 'filters'],
17 | graphiql: true,
18 | resolvers: {
19 | JSON: JSONResolver,
20 | },
21 | schemaDirectives: {
22 | uppercase: UpperCaseDirective,
23 | },
24 | buildSchemaOptions: {
25 | fieldMiddleware: [
26 | async (ctx, next) => {
27 | const value = await next();
28 |
29 | const { info } = ctx;
30 | const extensions =
31 | info?.parentType.getFields()[info.fieldName].extensions;
32 | //...
33 |
34 | return value;
35 | },
36 | ],
37 | },
38 | // altair: true,
39 | context: (request, reply) => {
40 | return {
41 | headers: request.headers,
42 | };
43 | },
44 | subscription: {
45 | context: (connection, request) => {
46 | return {
47 | headers: request.headers,
48 | };
49 | },
50 | },
51 | };
52 | },
53 | }),
54 | UserModule,
55 | ],
56 | controllers: [AppController],
57 | providers: [ImageResolver, HashScalar],
58 | })
59 | export class AppModule {}
60 |
--------------------------------------------------------------------------------
/tests/example/decorators/header.decorator.ts:
--------------------------------------------------------------------------------
1 | import { createParamDecorator, ExecutionContext } from '@nestjs/common';
2 | import { GqlExecutionContext } from '../../../lib';
3 |
4 | export const Header = createParamDecorator(
5 | (name: string, ctx: ExecutionContext) => {
6 | return GqlExecutionContext.create(ctx).getContext().headers[name];
7 | },
8 | );
9 |
--------------------------------------------------------------------------------
/tests/example/directives/upper-case.directive.ts:
--------------------------------------------------------------------------------
1 | import { SchemaDirectiveVisitor } from '@graphql-tools/utils';
2 | import {
3 | GraphQLField,
4 | GraphQLInterfaceType,
5 | GraphQLObjectType,
6 | defaultFieldResolver,
7 | } from 'graphql';
8 |
9 | export class UpperCaseDirective extends SchemaDirectiveVisitor {
10 | name = 'uppercase';
11 | visitFieldDefinition(
12 | field: GraphQLField,
13 | _details: { objectType: GraphQLObjectType | GraphQLInterfaceType },
14 | ): GraphQLField | void | null {
15 | const { resolve = defaultFieldResolver } = field;
16 | field.resolve = async function (...args) {
17 | const result = await resolve.apply(this, args);
18 | if (typeof result === 'string') {
19 | return result.toUpperCase();
20 | }
21 | return result;
22 | };
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/example/filters/forbidden-exception.filter.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ArgumentsHost,
3 | Catch,
4 | ExceptionFilter,
5 | ForbiddenException,
6 | } from '@nestjs/common';
7 | import { GqlArgumentsHost } from '../../../lib';
8 |
9 | @Catch(ForbiddenException)
10 | export class ForbiddenExceptionFilter implements ExceptionFilter {
11 | catch(exception: any, host: ArgumentsHost): any {
12 | const gqlHost = GqlArgumentsHost.create(host);
13 |
14 | throw exception;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/example/guards/auth.guard.ts:
--------------------------------------------------------------------------------
1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
2 | import { GqlExecutionContext } from '../../../lib';
3 |
4 | @Injectable()
5 | export class AuthGuard implements CanActivate {
6 | async canActivate(context: ExecutionContext): Promise {
7 | const ctx = GqlExecutionContext.create(context);
8 |
9 | const { authorization } = ctx.getContext().headers;
10 |
11 | return authorization && authorization === 'ok';
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tests/example/interceptors/log.interceptor.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CallHandler,
3 | ExecutionContext,
4 | Logger,
5 | NestInterceptor,
6 | } from '@nestjs/common';
7 | import { Observable } from 'rxjs';
8 | import { GqlExecutionContext } from '../../../lib';
9 | import { tap } from 'rxjs/operators';
10 |
11 | export class LogInterceptor implements NestInterceptor {
12 | private logger = new Logger(LogInterceptor.name, { timestamp: true });
13 |
14 | intercept(
15 | context: ExecutionContext,
16 | next: CallHandler,
17 | ): Observable {
18 | const ctx = GqlExecutionContext.create(context);
19 |
20 | this.logger.warn(
21 | `Before - ${ctx.getClass().name}@${ctx.getHandler().name}`,
22 | );
23 |
24 | return next
25 | .handle()
26 | .pipe(
27 | tap(() =>
28 | this.logger.warn(
29 | `After - ${ctx.getClass().name}@${ctx.getHandler().name}`,
30 | ),
31 | ),
32 | );
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tests/example/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 | import {
4 | FastifyAdapter,
5 | NestFastifyApplication,
6 | } from '@nestjs/platform-fastify';
7 |
8 | async function bootstrap() {
9 | const app = await NestFactory.create(
10 | AppModule,
11 | new FastifyAdapter(),
12 | );
13 | await app.listen(3000);
14 | }
15 | bootstrap();
16 |
--------------------------------------------------------------------------------
/tests/example/modules/user/inputs/create-user.input.ts:
--------------------------------------------------------------------------------
1 | import { InputType, OmitType } from '@nestjs/graphql';
2 | import { UserType } from '../../../types/user.type';
3 |
4 | @InputType()
5 | export class CreateUserInput extends OmitType(UserType, ['id'], InputType) {}
6 |
--------------------------------------------------------------------------------
/tests/example/modules/user/inputs/full-name.args.ts:
--------------------------------------------------------------------------------
1 | import { ArgsType, Field } from '@nestjs/graphql';
2 |
3 | @ArgsType()
4 | export class FullNameArgs {
5 | @Field({ nullable: true })
6 | filter?: string;
7 | }
8 |
--------------------------------------------------------------------------------
/tests/example/modules/user/resolvers/customer.resolver.ts:
--------------------------------------------------------------------------------
1 | import { Query, Resolver } from '@nestjs/graphql';
2 | import { CustomerType } from '../../../types/customer.type';
3 | import { CustomerService } from '../services/customer.service';
4 |
5 | @Resolver(() => CustomerType)
6 | export class CustomerResolver {
7 | constructor(private readonly customerService: CustomerService) {}
8 |
9 | @Query(() => [CustomerType])
10 | customers() {
11 | return this.customerService.customers();
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tests/example/modules/user/resolvers/person.resolver.ts:
--------------------------------------------------------------------------------
1 | import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
2 | import { Person } from '../../../types/person.interface';
3 | import { LoaderQuery, ResolveLoader } from '../../../../../lib';
4 |
5 | @Resolver(() => Person)
6 | export class PersonResolver {
7 | @ResolveField(() => String)
8 | uniqueName(@Parent() person: Person) {
9 | return `${person.id}__${person.name}`;
10 | }
11 |
12 | @ResolveLoader(() => String)
13 | uniqueId(@Parent() queries: LoaderQuery[]) {
14 | return queries.map(({ obj }) => `${obj.name}__${obj.id}`);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/example/modules/user/resolvers/search.resolver.ts:
--------------------------------------------------------------------------------
1 | import { createUnionType, Query, Resolver } from '@nestjs/graphql';
2 | import { PostType } from '../../../types/post.type';
3 | import { UserType } from '../../../types/user.type';
4 | import { users } from '../services/user.service';
5 | import { posts } from '../services/post.service';
6 |
7 | function isUser(value: PostType | UserType): value is UserType {
8 | return 'birthDay' in value;
9 | }
10 |
11 | function isPost(value: PostType | UserType): value is PostType {
12 | return 'title' in value;
13 | }
14 |
15 | const SearchUnion = createUnionType({
16 | name: 'SearchUnion',
17 | types: () => [PostType, UserType],
18 | resolveType: (value: PostType | UserType) => {
19 | if (isUser(value)) {
20 | return UserType;
21 | }
22 | if (isPost(value)) {
23 | return PostType;
24 | }
25 | return null;
26 | },
27 | });
28 |
29 | @Resolver()
30 | export class SearchResolver {
31 | @Query(() => [SearchUnion])
32 | search() {
33 | return [...users, ...posts];
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/tests/example/modules/user/resolvers/user.resolver.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Args,
3 | Context,
4 | Int,
5 | Parent,
6 | Query,
7 | ResolveField,
8 | Resolver,
9 | Mutation,
10 | Subscription,
11 | ID,
12 | } from '@nestjs/graphql';
13 | import { UserType } from '../../../types/user.type';
14 | import { PostType } from '../../../types/post.type';
15 | import {
16 | LoaderQuery,
17 | ResolveLoader,
18 | toAsyncIterator,
19 | } from '../../../../../lib';
20 | import { UserService } from '../services/user.service';
21 | import { PostService } from '../services/post.service';
22 | import { CreateUserInput } from '../inputs/create-user.input';
23 | import { MercuriusContext } from 'mercurius';
24 | import {
25 | ParseIntPipe,
26 | UseFilters,
27 | UseInterceptors,
28 | UseGuards,
29 | } from '@nestjs/common';
30 | import { LogInterceptor } from '../../../interceptors/log.interceptor';
31 | import { ForbiddenExceptionFilter } from '../../../filters/forbidden-exception.filter';
32 | import { Header } from '../../../decorators/header.decorator';
33 | import { FullNameArgs } from '../inputs/full-name.args';
34 | import { AuthGuard } from '../../../guards/auth.guard';
35 |
36 | function calculateAge(birthday: Date): number {
37 | const ageDifMs = Date.now() - birthday.getTime();
38 | const ageDate = new Date(ageDifMs);
39 | return Math.abs(ageDate.getUTCFullYear() - 1970);
40 | }
41 |
42 | @UseFilters(ForbiddenExceptionFilter)
43 | @Resolver(() => UserType)
44 | export class UserResolver {
45 | constructor(
46 | private readonly userService: UserService,
47 | private readonly postService: PostService,
48 | ) {}
49 |
50 | @Query(() => [UserType], {
51 | complexity: (options) => options.childComplexity + 5,
52 | })
53 | users() {
54 | return this.userService.users();
55 | }
56 |
57 | @Query(() => UserType, { nullable: true })
58 | user(@Args({ name: 'id', type: () => ID }, ParseIntPipe) id: number) {
59 | return this.userService.find(id);
60 | }
61 |
62 | @Mutation(() => UserType)
63 | createUser(
64 | @Args({ name: 'input', type: () => CreateUserInput }) data: CreateUserInput,
65 | @Context() ctx: MercuriusContext,
66 | ) {
67 | const user = this.userService.create(data);
68 |
69 | return user;
70 | }
71 |
72 | @ResolveField(() => Int, { complexity: 5 })
73 | async age(
74 | @Parent() user: UserType,
75 | @Context('headers') headers: Record,
76 | ) {
77 | return calculateAge(user.birthDay);
78 | }
79 |
80 | @ResolveLoader(() => String, {
81 | nullable: true,
82 | complexity: (options) => {
83 | return 5;
84 | },
85 | middleware: [
86 | async (ctx, next) => {
87 | const results = await next();
88 | return results.map((res) => res || 'Missing');
89 | },
90 | ],
91 | opts: {
92 | cache: false,
93 | },
94 | })
95 | async fullName(
96 | @Args({ type: () => FullNameArgs }) args: FullNameArgs[],
97 | @Parent() p: LoaderQuery[],
98 | @Context() ctx: Record,
99 | @Header('authorization') auth?: string,
100 | ) {
101 | return p.map(({ obj }) => {
102 | if (obj.name && obj.lastName) {
103 | return `${obj.name} ${obj.lastName}`;
104 | }
105 | return obj.name || obj.lastName;
106 | });
107 | }
108 |
109 | // @UseGuards(AuthGuard)
110 | @UseInterceptors(LogInterceptor)
111 | @ResolveLoader(() => [PostType])
112 | async posts(@Parent() queries: LoaderQuery[]) {
113 | return this.postService.userPostLoader(queries.map((q) => q.obj.id));
114 | }
115 |
116 | @UseInterceptors(LogInterceptor)
117 | @Subscription(() => UserType)
118 | async userAdded(@Context() ctx: MercuriusContext) {
119 | return ctx.pubsub.subscribe('USER_ADDED');
120 | }
121 |
122 | @Subscription(() => String, {
123 | resolve: (payload) => {
124 | return payload.userAdded.id;
125 | },
126 | })
127 | async userAddedId(@Context() ctx: MercuriusContext) {
128 | return ctx.pubsub.subscribe('USER_ADDED');
129 | }
130 |
131 | @Subscription(() => UserType, {
132 | filter: (payload, variables: { id: string }) => {
133 | return payload.userAdded.id === variables.id;
134 | },
135 | resolve: (payload) => {
136 | return payload.userAdded;
137 | },
138 | })
139 | async specificUserAdded(
140 | @Args({ name: 'id', type: () => ID }) id: string,
141 | @Context() ctx: MercuriusContext,
142 | ) {
143 | return toAsyncIterator(ctx.pubsub.subscribe('USER_ADDED'));
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/tests/example/modules/user/services/customer.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { CustomerType } from '../../../types/customer.type';
3 |
4 | const customers: CustomerType[] = [
5 | {
6 | id: '1',
7 | name: 'C1',
8 | },
9 | {
10 | id: '2',
11 | name: 'C2',
12 | },
13 | ];
14 |
15 | @Injectable()
16 | export class CustomerService {
17 | customers() {
18 | return customers;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tests/example/modules/user/services/post.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { PostType } from '../../../types/post.type';
3 | import { groupBy } from 'lodash';
4 |
5 | export const posts: PostType[] = [
6 | {
7 | id: '1',
8 | title: 'Post1',
9 | authorId: '1',
10 | },
11 | {
12 | id: '2',
13 | title: 'Post2',
14 | authorId: '1',
15 | },
16 | {
17 | id: '3',
18 | title: 'Post3',
19 | authorId: '2',
20 | },
21 | ];
22 |
23 | @Injectable()
24 | export class PostService {
25 | posts() {
26 | return posts;
27 | }
28 |
29 | userPostLoader(userIds: string[]) {
30 | const groupedPosts = groupBy(
31 | posts.filter((post) => userIds.includes(post.authorId)),
32 | 'authorId',
33 | );
34 |
35 | return userIds.map((id) => groupedPosts[id] || []);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tests/example/modules/user/services/user.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { UserType } from '../../../types/user.type';
3 | import { CreateUserInput } from '../inputs/create-user.input';
4 | import { GraphQLHook, PubSubHost } from '../../../../../lib';
5 |
6 | export const users: UserType[] = [
7 | {
8 | id: '1',
9 | name: 'foo',
10 | lastName: 'bar',
11 | birthDay: new Date(),
12 | secret: 'supersecret',
13 | meta: {
14 | foo: 'bar',
15 | },
16 | },
17 | {
18 | id: '2',
19 | birthDay: new Date(),
20 | },
21 | {
22 | id: '3',
23 | name: 'baz',
24 | birthDay: new Date(),
25 | },
26 | {
27 | id: '4',
28 | name: 'bep',
29 | birthDay: new Date(),
30 | },
31 | ];
32 | let nextId = 5;
33 |
34 | @Injectable()
35 | export class UserService {
36 | constructor(private readonly pubSubHost: PubSubHost) {}
37 |
38 | @GraphQLHook('preParsing')
39 | async preParse() {}
40 |
41 | users() {
42 | return users;
43 | }
44 |
45 | find(id: number) {
46 | return users.find((user) => user.id === id.toString());
47 | }
48 |
49 | create(data: CreateUserInput) {
50 | const user = {
51 | ...data,
52 | id: nextId.toString(),
53 | };
54 | nextId++;
55 | users.push(user);
56 |
57 | this.pubSubHost.getInstance()?.publish({
58 | topic: 'USER_ADDED',
59 | payload: {
60 | userAdded: user,
61 | },
62 | });
63 |
64 | return user;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/tests/example/modules/user/user.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { PersonResolver } from './resolvers/person.resolver';
3 | import { UserResolver } from './resolvers/user.resolver';
4 | import { SearchResolver } from './resolvers/search.resolver';
5 | import { PostService } from './services/post.service';
6 | import { UserService } from './services/user.service';
7 | import { CustomerService } from './services/customer.service';
8 | import { CustomerResolver } from './resolvers/customer.resolver';
9 |
10 | @Module({
11 | providers: [
12 | PostService,
13 | UserService,
14 | CustomerService,
15 | PersonResolver,
16 | UserResolver,
17 | SearchResolver,
18 | CustomerResolver,
19 | ],
20 | })
21 | export class UserModule {}
22 |
--------------------------------------------------------------------------------
/tests/example/resolvers/image.resolver.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs';
2 | import * as util from 'util';
3 | import * as stream from 'stream';
4 | import * as path from 'path';
5 | import { GraphQLUpload, FileUpload } from 'graphql-upload';
6 | import { Args, Mutation, Resolver } from '@nestjs/graphql';
7 | import { OnModuleInit } from '@nestjs/common';
8 |
9 | const pipeline = util.promisify(stream.pipeline);
10 |
11 | @Resolver()
12 | export class ImageResolver implements OnModuleInit {
13 | private readonly uploadDir = path.join(process.cwd(), 'uploads');
14 |
15 | async onModuleInit() {
16 | if (!fs.existsSync(this.uploadDir)) {
17 | await fs.promises.mkdir(this.uploadDir);
18 | }
19 | }
20 |
21 | @Mutation(() => Boolean)
22 | async uploadImage(
23 | @Args({ name: 'file', type: () => GraphQLUpload })
24 | image: Promise,
25 | ) {
26 | const { filename, createReadStream } = await image;
27 | const rs = createReadStream();
28 | const ws = fs.createWriteStream(path.join(this.uploadDir, filename));
29 | await pipeline(rs, ws);
30 |
31 | return true;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tests/example/scalars/hash.scalar.ts:
--------------------------------------------------------------------------------
1 | import { CustomScalar, Scalar } from '@nestjs/graphql';
2 | import * as crypto from 'crypto';
3 | import { Kind, ValueNode } from 'graphql';
4 |
5 | const algo = 'aes-256-ctr';
6 | const key = Buffer.from(
7 | crypto.createHash('sha256').update('secret').digest().slice(0, 32),
8 | );
9 | const iv = Buffer.from(
10 | crypto.createHash('sha256').update('secret').digest().slice(0, 16),
11 | );
12 |
13 | @Scalar('Hash', () => HashScalar)
14 | export class HashScalar implements CustomScalar {
15 | description = 'Hash scalar';
16 |
17 | parseValue(value: string): string {
18 | console.log('hash parseV');
19 | return crypto
20 | .createDecipheriv(algo, key, iv)
21 | .update(value, 'base64')
22 | .toString();
23 | }
24 |
25 | serialize(value: string): string {
26 | console.log('hash serialize');
27 | return crypto
28 | .createCipheriv(algo, key, iv)
29 | .update(value)
30 | .toString('base64');
31 | }
32 |
33 | parseLiteral(ast: ValueNode): string {
34 | console.log('hash parseL');
35 | if (ast.kind === Kind.STRING) {
36 | return this.parseValue(ast.value);
37 | }
38 | return null;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/tests/example/types/customer.type.ts:
--------------------------------------------------------------------------------
1 | import { Extensions, Field, ID, ObjectType } from '@nestjs/graphql';
2 | import { Person } from './person.interface';
3 |
4 | @ObjectType('Customer', {
5 | implements: Person,
6 | })
7 | export class CustomerType implements Person {
8 | @Field(() => ID)
9 | id: string;
10 |
11 | @Extensions({ role: 'ADMIN' })
12 | @Field(() => String, { nullable: true })
13 | name?: string;
14 | }
15 |
--------------------------------------------------------------------------------
/tests/example/types/person.interface.ts:
--------------------------------------------------------------------------------
1 | import { Directive, Field, ID, InterfaceType } from '@nestjs/graphql';
2 |
3 | @InterfaceType('IPerson')
4 | export abstract class Person {
5 | @Field(() => ID)
6 | id: string;
7 |
8 | @Directive('@uppercase')
9 | @Field(() => String, { nullable: true })
10 | name?: string;
11 | }
12 |
--------------------------------------------------------------------------------
/tests/example/types/post.type.ts:
--------------------------------------------------------------------------------
1 | import { Field, ID, ObjectType, registerEnumType } from '@nestjs/graphql';
2 |
3 | export enum Status {
4 | PUBLISHED,
5 | DRAFT,
6 | }
7 |
8 | registerEnumType(Status, {
9 | name: 'Status',
10 | });
11 |
12 | @ObjectType()
13 | export class PostType {
14 | @Field(() => ID)
15 | id: string;
16 |
17 | @Field()
18 | title: string;
19 |
20 | @Field()
21 | authorId: string;
22 |
23 | @Field(() => Status, { defaultValue: Status.DRAFT })
24 | status?: Status;
25 | }
26 |
--------------------------------------------------------------------------------
/tests/example/types/user.type.ts:
--------------------------------------------------------------------------------
1 | import { Extensions, Field, ID, ObjectType } from '@nestjs/graphql';
2 | import { HashScalar } from '../scalars/hash.scalar';
3 | import { JSONResolver } from 'graphql-scalars';
4 | import { Person } from './person.interface';
5 |
6 | @ObjectType('User', {
7 | implements: [Person],
8 | })
9 | export class UserType implements Person {
10 | @Field(() => ID)
11 | id: string;
12 |
13 | @Extensions({ role: 'ADMIN' })
14 | @Field(() => String, { nullable: true })
15 | name?: string;
16 |
17 | @Field({ defaultValue: 'noone' })
18 | lastName?: string;
19 |
20 | @Field(() => Date)
21 | birthDay: Date;
22 |
23 | @Field(() => HashScalar, { nullable: true })
24 | secret?: string;
25 |
26 | @Field(() => JSONResolver, { nullable: true })
27 | meta?: any;
28 | }
29 |
--------------------------------------------------------------------------------
/tests/federation/gateway.mjs:
--------------------------------------------------------------------------------
1 | import Fastify from 'fastify';
2 | import mercurius from 'mercurius';
3 |
4 | const gateway = Fastify();
5 |
6 | gateway.register(mercurius, {
7 | graphiql: true,
8 | subscription: true,
9 | logLevel: 'trace',
10 | gateway: {
11 | pollingInterval: 1000,
12 | services: [
13 | {
14 | name: 'users',
15 | url: 'http://localhost:3001/graphql',
16 | wsUrl: 'ws://localhost:3001/graphql',
17 | },
18 | {
19 | name: 'posts',
20 | url: 'http://localhost:3002/graphql',
21 | wsUrl: 'ws://localhost:3002/graphql',
22 | }
23 | ],
24 | },
25 | });
26 |
27 | gateway.listen(3333).then(() => console.log('Mercurius Gateway listening on :3333'));
28 |
--------------------------------------------------------------------------------
/tests/federation/gateway/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { MercuriusGatewayModule } from '../../../lib';
3 |
4 | @Module({
5 | imports: [
6 | MercuriusGatewayModule.forRootAsync({
7 | useFactory: () => ({
8 | graphiql: true,
9 | subscription: true,
10 | gateway: {
11 | services: [
12 | {
13 | name: 'user',
14 | url: 'http://localhost:3001/graphql',
15 | wsUrl: 'ws://localhost:3001/graphql',
16 | mandatory: true,
17 | },
18 | {
19 | name: 'post',
20 | url: 'http://localhost:3002/graphql',
21 | wsUrl: 'ws://localhost:3002/graphql',
22 | mandatory: true,
23 | },
24 | ],
25 | },
26 | }),
27 | }),
28 | ],
29 | })
30 | export class AppModule {}
31 |
--------------------------------------------------------------------------------
/tests/federation/gateway/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 | import {
4 | FastifyAdapter,
5 | NestFastifyApplication,
6 | } from '@nestjs/platform-fastify';
7 |
8 | async function bootstrap() {
9 | const app = await NestFactory.create(
10 | AppModule,
11 | new FastifyAdapter(),
12 | );
13 | await app.listen(3333);
14 | }
15 | bootstrap();
16 |
--------------------------------------------------------------------------------
/tests/federation/postService/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { MercuriusModule } from '../../../lib';
3 | import { UserResolver } from './user.resolver';
4 | import { PostResolver } from './post.resolver';
5 | import { User } from './user';
6 |
7 | @Module({
8 | imports: [
9 | MercuriusModule.forRootAsync({
10 | useFactory: () => ({
11 | autoSchemaFile: './schema.graphql',
12 | fieldResolverEnhancers: ['guards', 'interceptors', 'filters'],
13 | federationMetadata: true,
14 | buildSchemaOptions: {
15 | orphanedTypes: [User],
16 | },
17 | // altair: true,
18 |
19 | context: (request, reply) => {
20 | return {
21 | headers: request.headers,
22 | };
23 | },
24 | subscription: {
25 | context: (connection, request) => {
26 | return {
27 | headers: request.headers,
28 | };
29 | },
30 | },
31 | }),
32 | }),
33 | ],
34 | providers: [UserResolver, PostResolver],
35 | })
36 | export class AppModule {}
37 |
--------------------------------------------------------------------------------
/tests/federation/postService/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 | import {
4 | FastifyAdapter,
5 | NestFastifyApplication,
6 | } from '@nestjs/platform-fastify';
7 |
8 | async function bootstrap() {
9 | const app = await NestFactory.create(
10 | AppModule,
11 | new FastifyAdapter(),
12 | );
13 | await app.listen(3002);
14 | }
15 | bootstrap();
16 |
--------------------------------------------------------------------------------
/tests/federation/postService/post.resolver.ts:
--------------------------------------------------------------------------------
1 | import { Parent, Query, ResolveField, Resolver } from '@nestjs/graphql';
2 | import { Post } from './post';
3 | import { User } from './user';
4 |
5 | export const posts: Post[] = [
6 | {
7 | id: 1,
8 | title: 'p1',
9 | authorId: 1,
10 | publishedAt: new Date(),
11 | },
12 | {
13 | id: 2,
14 | title: 'p2',
15 | authorId: 1,
16 | publishedAt: new Date(),
17 | },
18 | ];
19 |
20 | @Resolver(() => Post)
21 | export class PostResolver {
22 | @Query(() => [Post])
23 | posts() {
24 | return posts;
25 | }
26 |
27 | @ResolveField(() => User)
28 | author(@Parent() post: Post) {
29 | return { __typename: 'User', id: post.authorId };
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/tests/federation/postService/post.ts:
--------------------------------------------------------------------------------
1 | import { Directive, Field, ID, ObjectType } from '@nestjs/graphql';
2 |
3 | @Directive('@key(fields: "id")')
4 | @ObjectType()
5 | export class Post {
6 | @Field(() => ID)
7 | id: number;
8 |
9 | @Field()
10 | title: string;
11 |
12 | @Field()
13 | authorId: number;
14 |
15 | @Field({ nullable: true })
16 | public?: boolean;
17 |
18 | @Field(() => Date)
19 | publishedAt: Date;
20 | }
21 |
--------------------------------------------------------------------------------
/tests/federation/postService/user.resolver.ts:
--------------------------------------------------------------------------------
1 | import { Parent, ResolveField, Resolver } from '@nestjs/graphql';
2 | import { User } from './user';
3 | import { Post } from './post';
4 | import { posts } from './post.resolver';
5 |
6 | @Resolver(() => User)
7 | export class UserResolver {
8 | @ResolveField(() => [Post])
9 | posts(@Parent() user: User) {
10 | const userPosts = posts.filter(
11 | (p) => p.authorId.toString() === user.id.toString(),
12 | );
13 | return userPosts;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tests/federation/postService/user.ts:
--------------------------------------------------------------------------------
1 | import { Directive, Field, ID, ObjectType } from '@nestjs/graphql';
2 |
3 | @Directive('@extends')
4 | @Directive('@key(fields: "id")')
5 | @ObjectType()
6 | export class User {
7 | @Directive('@external')
8 | @Field(() => ID)
9 | id: string;
10 | }
11 |
--------------------------------------------------------------------------------
/tests/federation/userService/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { MercuriusModule } from '../../../lib';
3 | import { UserResolver } from './user.resolver';
4 |
5 | @Module({
6 | imports: [
7 | MercuriusModule.forRootAsync({
8 | useFactory: () => ({
9 | autoSchemaFile: './schema.graphql',
10 | fieldResolverEnhancers: ['guards', 'interceptors', 'filters'],
11 | federationMetadata: true,
12 | // altair: true,
13 |
14 | context: (request, reply) => {
15 | return {
16 | headers: request.headers,
17 | };
18 | },
19 | subscription: {
20 | context: (connection, request) => {
21 | return {
22 | headers: request.headers,
23 | };
24 | },
25 | },
26 | }),
27 | }),
28 | ],
29 | providers: [UserResolver],
30 | })
31 | export class AppModule {}
32 |
--------------------------------------------------------------------------------
/tests/federation/userService/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 | import {
4 | FastifyAdapter,
5 | NestFastifyApplication,
6 | } from '@nestjs/platform-fastify';
7 |
8 | async function bootstrap() {
9 | const app = await NestFactory.create(
10 | AppModule,
11 | new FastifyAdapter(),
12 | );
13 | await app.listen(3001);
14 | }
15 | bootstrap();
16 |
--------------------------------------------------------------------------------
/tests/federation/userService/user.resolver.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Args,
3 | Context,
4 | InputType,
5 | Mutation,
6 | OmitType,
7 | Query,
8 | Resolver,
9 | Subscription,
10 | } from '@nestjs/graphql';
11 | import { User } from './user';
12 | import { PubSub } from 'mercurius';
13 | import {
14 | LoaderQuery,
15 | Reference,
16 | ResolveReferenceLoader,
17 | toAsyncIterator,
18 | } from '../../../lib';
19 |
20 | const users: User[] = [
21 | {
22 | id: 1,
23 | name: 'u1',
24 | },
25 | {
26 | id: 2,
27 | name: 'u2',
28 | },
29 | ];
30 |
31 | let nextId = 3;
32 |
33 | @InputType()
34 | class CreateUserInput extends OmitType(User, ['id'], InputType) {}
35 |
36 | @Resolver(() => User)
37 | export class UserResolver {
38 | @Query(() => [User])
39 | users() {
40 | return users;
41 | }
42 |
43 | @ResolveReferenceLoader()
44 | resolveReference(refs: LoaderQuery>[]) {
45 | const refIds = refs.map(({ obj }) => obj.id.toString());
46 |
47 | return users.filter((u) => refIds.includes(u.id.toString()));
48 | }
49 |
50 | @Mutation(() => User)
51 | createUser(
52 | @Args('input') data: CreateUserInput,
53 | @Context('pubsub') pubSub: PubSub,
54 | ) {
55 | const user = {
56 | ...data,
57 | id: nextId,
58 | };
59 | users.push(user);
60 | pubSub.publish({
61 | topic: 'createUser',
62 | payload: { user },
63 | });
64 |
65 | nextId++;
66 |
67 | return user;
68 | }
69 |
70 | @Subscription(() => User, {
71 | resolve: (payload) => payload.user,
72 | })
73 | onCreateUser(@Context('pubsub') pubSub: PubSub) {
74 | return toAsyncIterator(pubSub.subscribe('createUser'));
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/tests/federation/userService/user.ts:
--------------------------------------------------------------------------------
1 | import { Directive, Field, ID, ObjectType } from '@nestjs/graphql';
2 |
3 | @Directive('@key(fields: "id")')
4 | @ObjectType()
5 | export class User {
6 | @Field(() => ID)
7 | id: number;
8 |
9 | @Field()
10 | name: string;
11 |
12 | @Field({ defaultValue: true })
13 | isActive?: boolean;
14 | }
15 |
--------------------------------------------------------------------------------
/tests/graphql/app.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { join } from 'path';
3 | import { MercuriusModule } from '../../lib';
4 | import { CatsModule } from './cats/cats.module';
5 |
6 | @Module({
7 | imports: [
8 | CatsModule,
9 | MercuriusModule.forRoot({
10 | typePaths: [join(__dirname, '**', '*.graphql')],
11 | }),
12 | ],
13 | })
14 | export class ApplicationModule {}
15 |
--------------------------------------------------------------------------------
/tests/graphql/async-options-class.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { join } from 'path';
3 | import {
4 | MercuriusModule,
5 | MercuriusModuleOptions,
6 | MercuriusOptionsFactory,
7 | } from '../../lib';
8 | import { CatsModule } from './cats/cats.module';
9 |
10 | class ConfigService implements MercuriusOptionsFactory {
11 | createMercuriusOptions(): MercuriusModuleOptions {
12 | return {
13 | typePaths: [join(__dirname, '**', '*.graphql')],
14 | };
15 | }
16 | }
17 |
18 | @Module({
19 | imports: [
20 | CatsModule,
21 | MercuriusModule.forRootAsync({
22 | useClass: ConfigService,
23 | }),
24 | ],
25 | })
26 | export class AsyncClassApplicationModule {}
27 |
--------------------------------------------------------------------------------
/tests/graphql/async-options.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { join } from 'path';
3 | import { MercuriusModule } from '../../lib';
4 | import { CatsModule } from './cats/cats.module';
5 |
6 | @Module({
7 | imports: [
8 | CatsModule,
9 | MercuriusModule.forRootAsync({
10 | useFactory: async () => ({
11 | typePaths: [join(__dirname, '**', '*.graphql')],
12 | }),
13 | }),
14 | ],
15 | })
16 | export class AsyncApplicationModule {}
17 |
--------------------------------------------------------------------------------
/tests/graphql/cats/cats-request-scoped.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, Scope } from '@nestjs/common';
2 | import { Cat } from './interfaces/cat.interface';
3 |
4 | @Injectable({ scope: Scope.REQUEST })
5 | export class CatsRequestScopedService {
6 | static COUNTER = 0;
7 | private readonly cats: Cat[] = [{ id: 1, name: 'Cat', age: 5 }];
8 |
9 | constructor() {
10 | CatsRequestScopedService.COUNTER++;
11 | }
12 |
13 | create(cat: Cat): Cat {
14 | this.cats.push(cat);
15 | return cat;
16 | }
17 |
18 | findAll(): Cat[] {
19 | return this.cats;
20 | }
21 |
22 | findOneById(id: number): Cat {
23 | return this.cats.find(cat => cat.id === id);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/graphql/cats/cats.guard.ts:
--------------------------------------------------------------------------------
1 | import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
2 | import { GqlExecutionContext } from '@nestjs/graphql';
3 |
4 | @Injectable()
5 | export class CatsGuard implements CanActivate {
6 | canActivate(context: ExecutionContext): boolean {
7 | const ctx = GqlExecutionContext.create(context);
8 | return true;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tests/graphql/cats/cats.module.ts:
--------------------------------------------------------------------------------
1 | import { DynamicModule, Module, Scope } from '@nestjs/common';
2 | import { CatsRequestScopedService } from './cats-request-scoped.service';
3 | import { CatsResolvers } from './cats.resolvers';
4 | import { CatsService } from './cats.service';
5 |
6 | @Module({
7 | providers: [CatsService, CatsResolvers],
8 | })
9 | export class CatsModule {
10 | static enableRequestScope(): DynamicModule {
11 | return {
12 | module: CatsModule,
13 | providers: [
14 | {
15 | provide: CatsService,
16 | useClass: CatsRequestScopedService,
17 | scope: Scope.REQUEST,
18 | },
19 | ],
20 | };
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/graphql/cats/cats.resolvers.ts:
--------------------------------------------------------------------------------
1 | import { ParseIntPipe, UseGuards } from '@nestjs/common';
2 | import {
3 | Args,
4 | Mutation,
5 | Query,
6 | ResolveField,
7 | Resolver,
8 | } from '@nestjs/graphql';
9 | import { CatsGuard } from './cats.guard';
10 | import { CatsService } from './cats.service';
11 | import { Cat } from './interfaces/cat.interface';
12 |
13 | @Resolver('Cat')
14 | export class CatsResolvers {
15 | constructor(private readonly catsService: CatsService) {}
16 |
17 | @Query()
18 | @UseGuards(CatsGuard)
19 | async getCats() {
20 | return await this.catsService.findAll();
21 | }
22 |
23 | @ResolveField('color')
24 | getColor() {
25 | return 'black';
26 | }
27 |
28 | @ResolveField()
29 | weight() {
30 | return 5;
31 | }
32 |
33 | @Query('cat')
34 | async findOneById(
35 | @Args('id', ParseIntPipe)
36 | id: number,
37 | ): Promise {
38 | return await this.catsService.findOneById(id);
39 | }
40 |
41 | @Mutation('createCat')
42 | async create(@Args() args: Cat): Promise {
43 | const createdCat = await this.catsService.create(args);
44 | return createdCat;
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/tests/graphql/cats/cats.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { Cat } from './interfaces/cat.interface';
3 |
4 | @Injectable()
5 | export class CatsService {
6 | static COUNTER = 0;
7 | private readonly cats: Cat[] = [{ id: 1, name: 'Cat', age: 5 }];
8 |
9 | constructor() {
10 | CatsService.COUNTER++;
11 | }
12 |
13 | create(cat: Cat): Cat {
14 | this.cats.push(cat);
15 | return cat;
16 | }
17 |
18 | findAll(): Cat[] {
19 | return this.cats;
20 | }
21 |
22 | findOneById(id: number): Cat {
23 | return this.cats.find(cat => cat.id === id);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/graphql/cats/cats.types.graphql:
--------------------------------------------------------------------------------
1 | type Query {
2 | getCats: [Cat]
3 | cat(id: ID!): Cat
4 | }
5 |
6 | type Mutation {
7 | createCat(name: String): Cat
8 | }
9 |
10 | type Cat {
11 | id: Int
12 | name: String
13 | age: Int
14 | color: String
15 | weight: Int
16 | }
17 |
--------------------------------------------------------------------------------
/tests/graphql/cats/interfaces/cat.interface.ts:
--------------------------------------------------------------------------------
1 | export interface Cat {
2 | readonly id: number;
3 | readonly name: string;
4 | readonly age: number;
5 | }
6 |
--------------------------------------------------------------------------------
/tests/graphql/common/scalars/date.scalar.ts:
--------------------------------------------------------------------------------
1 | import { Kind } from 'graphql';
2 | import { Scalar } from '@nestjs/graphql';
3 |
4 | @Scalar('Date')
5 | export class DateScalar {
6 | description = 'Date custom scalar type';
7 |
8 | parseValue(value) {
9 | return new Date(value); // value from the client
10 | }
11 |
12 | serialize(value) {
13 | return value.getTime(); // value sent to the client
14 | }
15 |
16 | parseLiteral(ast) {
17 | if (ast.kind === Kind.INT) {
18 | return parseInt(ast.value, 10); // ast value is always in string format
19 | }
20 | return null;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/graphql/config.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ConfigService } from './config.service';
3 |
4 | @Module({
5 | providers: [ConfigService],
6 | exports: [ConfigService],
7 | })
8 | export class ConfigModule {}
9 |
--------------------------------------------------------------------------------
/tests/graphql/config.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { join } from 'path';
3 | import { GqlModuleOptions, GqlOptionsFactory } from '@nestjs/graphql';
4 |
5 | @Injectable()
6 | export class ConfigService implements GqlOptionsFactory {
7 | createGqlOptions(): GqlModuleOptions {
8 | return {
9 | typePaths: [join(__dirname, '**', '*.graphql')],
10 | };
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tests/graphql/hello/cats.types.graphql:
--------------------------------------------------------------------------------
1 | type Query {
2 | getCats: [Cat]
3 | cat(id: ID!): Cat
4 | }
5 |
6 | type Mutation {
7 | createCat(name: String): Cat
8 | }
9 |
10 | type Subscription {
11 | catCreated: Cat
12 | }
13 |
14 | type Cat {
15 | id: Int
16 | name: String
17 | age: Int
18 | color: String
19 | weight: Int
20 | }
21 |
--------------------------------------------------------------------------------
/tests/graphql/hello/dto/test.dto.ts:
--------------------------------------------------------------------------------
1 | import { IsString, IsNotEmpty, IsNumber } from 'class-validator';
2 |
3 | export class TestDto {
4 | @IsString()
5 | @IsNotEmpty()
6 | string: string;
7 |
8 | @IsNumber()
9 | number: number;
10 | }
11 |
--------------------------------------------------------------------------------
/tests/graphql/hello/guards/request-scoped.guard.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CanActivate,
3 | ExecutionContext,
4 | Inject,
5 | Injectable,
6 | Scope,
7 | } from '@nestjs/common';
8 | import { Observable } from 'rxjs';
9 |
10 | @Injectable({ scope: Scope.REQUEST })
11 | export class Guard implements CanActivate {
12 | static COUNTER = 0;
13 | static REQUEST_SCOPED_DATA = [];
14 |
15 | constructor(@Inject('REQUEST_ID') private requestId: number) {
16 | Guard.COUNTER++;
17 | }
18 |
19 | canActivate(
20 | context: ExecutionContext,
21 | ): boolean | Promise | Observable {
22 | Guard.REQUEST_SCOPED_DATA.push(this.requestId);
23 | return true;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/graphql/hello/hello.module.ts:
--------------------------------------------------------------------------------
1 | import { DynamicModule, Inject, Module, Provider, Scope } from '@nestjs/common';
2 | import { join } from 'path';
3 | import { GraphQLModule } from '@nestjs/graphql';
4 | import { HelloResolver } from './hello.resolver';
5 | import { HelloService } from './hello.service';
6 | import { UsersService } from './users/users.service';
7 |
8 | @Module({
9 | imports: [
10 | GraphQLModule.forRoot({
11 | typePaths: [join(__dirname, '*.graphql')],
12 | }),
13 | ],
14 | providers: [
15 | HelloResolver,
16 | HelloService,
17 | UsersService,
18 | {
19 | provide: 'REQUEST_ID',
20 | useFactory: () => 1,
21 | scope: Scope.REQUEST,
22 | },
23 | ],
24 | })
25 | export class HelloModule {
26 | constructor(@Inject('META') private readonly meta) {}
27 |
28 | static forRoot(meta: Provider): DynamicModule {
29 | return {
30 | module: HelloModule,
31 | providers: [meta],
32 | };
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/tests/graphql/hello/hello.resolver.ts:
--------------------------------------------------------------------------------
1 | import { UseGuards, UseInterceptors } from '@nestjs/common';
2 | import { Query, Resolver } from '@nestjs/graphql';
3 | import { Guard } from './guards/request-scoped.guard';
4 | import { HelloService } from './hello.service';
5 | import { Interceptor } from './interceptors/logging.interceptor';
6 | import { UsersService } from './users/users.service';
7 |
8 | @Resolver()
9 | export class HelloResolver {
10 | static COUNTER = 0;
11 | constructor(
12 | private readonly helloService: HelloService,
13 | private readonly usersService: UsersService,
14 | ) {
15 | HelloResolver.COUNTER++;
16 | }
17 |
18 | @Query()
19 | @UseGuards(Guard)
20 | @UseInterceptors(Interceptor)
21 | getCats(): any[] {
22 | return this.helloService.getCats();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/graphql/hello/hello.service.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable, Scope } from '@nestjs/common';
2 |
3 | @Injectable({ scope: Scope.REQUEST })
4 | export class HelloService {
5 | constructor(@Inject('META') private readonly meta) {}
6 |
7 | getCats(): any[] {
8 | return [{ id: 1, name: 'Cat', age: 5 }];
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/tests/graphql/hello/interceptors/logging.interceptor.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CallHandler,
3 | ExecutionContext,
4 | Inject,
5 | Injectable,
6 | NestInterceptor,
7 | Scope,
8 | } from '@nestjs/common';
9 | import { Observable } from 'rxjs';
10 |
11 | @Injectable({ scope: Scope.REQUEST })
12 | export class Interceptor implements NestInterceptor {
13 | static COUNTER = 0;
14 | static REQUEST_SCOPED_DATA = [];
15 |
16 | constructor(@Inject('REQUEST_ID') private requestId: number) {
17 | Interceptor.COUNTER++;
18 | }
19 |
20 | intercept(context: ExecutionContext, call: CallHandler): Observable {
21 | Interceptor.REQUEST_SCOPED_DATA.push(this.requestId);
22 | return call.handle();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/graphql/hello/users/user-by-id.pipe.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ArgumentMetadata,
3 | Inject,
4 | Injectable,
5 | PipeTransform,
6 | } from '@nestjs/common';
7 | import { UsersService } from './users.service';
8 |
9 | @Injectable()
10 | export class UserByIdPipe implements PipeTransform {
11 | static COUNTER = 0;
12 | static REQUEST_SCOPED_DATA = [];
13 |
14 | constructor(
15 | @Inject('REQUEST_ID') private requestId: number,
16 | private readonly usersService: UsersService,
17 | ) {
18 | UserByIdPipe.COUNTER++;
19 | }
20 |
21 | transform(value: string, metadata: ArgumentMetadata) {
22 | UserByIdPipe.REQUEST_SCOPED_DATA.push(this.requestId);
23 | return this.usersService.findById(value);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tests/graphql/hello/users/users.service.ts:
--------------------------------------------------------------------------------
1 | import { Inject, Injectable, Scope } from '@nestjs/common';
2 |
3 | @Injectable({ scope: Scope.REQUEST })
4 | export class UsersService {
5 | static COUNTER = 0;
6 | constructor(@Inject('META') private readonly meta) {
7 | UsersService.COUNTER++;
8 | }
9 |
10 | findById(id: string) {
11 | return { id };
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tests/graphql/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { ApplicationModule } from './app.module';
3 | import { FastifyAdapter } from '@nestjs/platform-fastify';
4 |
5 | async function bootstrap() {
6 | const app = await NestFactory.create(ApplicationModule, new FastifyAdapter());
7 | await app.listen(3000);
8 | }
9 | bootstrap();
10 |
--------------------------------------------------------------------------------
/tests/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "@nestjs/schematics",
3 | "sourceRoot": "example",
4 | "monorepo": true,
5 | "compilerOptions": {
6 | "webpack": false,
7 | "tsConfigPath": "tsconfig.build.json"
8 | },
9 | "projects": {
10 | "example": {
11 | "type": "application",
12 | "root": "example",
13 | "entryFile": "tests/example/main",
14 | "sourceRoot": "example"
15 | },
16 | "code-first": {
17 | "type": "application",
18 | "root": "code-first",
19 | "entryFile": "tests/code-first/main",
20 | "sourceRoot": "code-first"
21 | },
22 | "graphql": {
23 | "type": "application",
24 | "root": "graphql",
25 | "entryFile": "tests/graphql/main",
26 | "sourceRoot": "graphql"
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/schema.graphql:
--------------------------------------------------------------------------------
1 | # ------------------------------------------------------
2 | # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
3 | # ------------------------------------------------------
4 |
5 | type User {
6 | id: ID!
7 | name: String!
8 | isActive: Boolean!
9 | }
10 |
11 | type Query {
12 | users: [User!]!
13 | }
14 |
15 | type Mutation {
16 | createUser(input: CreateUserInput!): User!
17 | }
18 |
19 | input CreateUserInput {
20 | name: String!
21 | isActive: Boolean = true
22 | }
23 |
24 | type Subscription {
25 | onCreateUser: User!
26 | }
27 |
--------------------------------------------------------------------------------
/tests/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/tests/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "es2017",
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "baseUrl": "./",
13 | "incremental": true
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tests/utils/create-test-client.ts:
--------------------------------------------------------------------------------
1 | import { TestingModule } from '@nestjs/testing';
2 | import { createMercuriusTestClient } from 'mercurius-integration-testing';
3 | import { HttpAdapterHost } from '@nestjs/core';
4 |
5 | export function createTestClient(
6 | testingModule: TestingModule,
7 | ): ReturnType {
8 | const httpAdapterHost = testingModule.get(HttpAdapterHost);
9 | const app = httpAdapterHost.httpAdapter.getInstance();
10 |
11 | return createMercuriusTestClient(app);
12 | }
13 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "noImplicitAny": false,
6 | "removeComments": true,
7 | "noLib": false,
8 | "skipLibCheck": true,
9 | "emitDecoratorMetadata": true,
10 | "experimentalDecorators": true,
11 | "target": "es6",
12 | "sourceMap": false,
13 | "outDir": "./dist",
14 | "rootDir": "./lib"
15 | },
16 | "include": [
17 | "lib/**/*"
18 | ],
19 | "exclude": [
20 | "node_modules",
21 | "**/*.spec.ts"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------