├── .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 | --------------------------------------------------------------------------------