├── .eslintrc.cjs ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── tests.yml │ └── website.yml ├── .gitignore ├── .husky ├── .gitignore └── pre-commit ├── .npmignore ├── .prettierignore ├── .yarnrc ├── LICENSE ├── README.md ├── babel.config.js ├── examples ├── combining-local-and-remote-schemas │ ├── README.md │ ├── package.json │ ├── sandbox.config.json │ ├── src │ │ ├── gateway.ts │ │ ├── index.ts │ │ └── services │ │ │ ├── local │ │ │ └── schema.ts │ │ │ ├── products │ │ │ ├── index.ts │ │ │ └── schema.ts │ │ │ └── storefronts │ │ │ ├── index.ts │ │ │ ├── schema.graphql │ │ │ └── schema.ts │ ├── tests │ │ └── combining-local-and-remote-schemas.test.ts │ └── tsconfig.json ├── computed-fields │ ├── README.md │ ├── package.json │ ├── src │ │ ├── gateway.ts │ │ ├── index.ts │ │ └── services │ │ │ ├── metadata.ts │ │ │ └── products.ts │ ├── tests │ │ └── computed-fields.test.ts │ └── tsconfig.json ├── continuous-integration-testing │ ├── README.md │ ├── babel.config.js │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── config.json │ │ ├── index.ts │ │ ├── remote_schemas │ │ │ ├── products.graphql │ │ │ ├── reviews.graphql │ │ │ └── users.graphql │ │ └── schema_builder.ts │ ├── test │ │ ├── gateway.test.ts │ │ ├── mock_services │ │ │ ├── products.ts │ │ │ ├── reviews.ts │ │ │ └── users.ts │ │ └── test_helper.ts │ └── tsconfig.json ├── custom-merge-resolvers │ ├── README.md │ ├── package.json │ ├── src │ │ ├── gateway.ts │ │ ├── index.ts │ │ └── services │ │ │ ├── info.ts │ │ │ ├── inventory.ts │ │ │ └── pricing.ts │ ├── tests │ │ └── custom-merge-resolvers.test.ts │ └── tsconfig.json ├── federation-supergraph │ ├── README.md │ ├── package.json │ ├── src │ │ ├── example-query.ts │ │ ├── example-subscription.ts │ │ ├── gateway.ts │ │ ├── index.ts │ │ ├── services │ │ │ ├── accounts.graphql │ │ │ ├── accounts.ts │ │ │ ├── inventory.graphql │ │ │ ├── inventory.ts │ │ │ ├── products.graphql │ │ │ ├── products.ts │ │ │ ├── reviews.graphql │ │ │ └── reviews.ts │ │ ├── supergraph.graphql │ │ └── supergraph.yaml │ ├── tests │ │ ├── __snapshots__ │ │ │ └── federation-supergraph.spec.ts.snap │ │ └── federation-supergraph.spec.ts │ └── tsconfig.json ├── federation-to-stitching-sdl │ ├── README.md │ ├── package.json │ ├── src │ │ ├── gateway.ts │ │ ├── index.ts │ │ └── services │ │ │ ├── products │ │ │ ├── index.ts │ │ │ └── server.ts │ │ │ ├── reviews │ │ │ ├── index.ts │ │ │ └── server.ts │ │ │ └── users │ │ │ ├── index.ts │ │ │ └── server.ts │ ├── tests │ │ ├── __snapshots__ │ │ │ └── federation-services.test.ts.snap │ │ └── federation-services.test.ts │ └── tsconfig.json ├── graphql-upload │ ├── README.md │ ├── file.txt │ ├── package.json │ ├── src │ │ ├── gateway.ts │ │ ├── index.ts │ │ └── services │ │ │ ├── resize-images │ │ │ ├── index.ts │ │ │ └── server.ts │ │ │ └── upload-files │ │ │ ├── files │ │ │ ├── file.txt │ │ │ └── yoga.png │ │ │ ├── index.ts │ │ │ └── server.ts │ ├── tests │ │ ├── __snapshots__ │ │ │ └── graphql-upload.spec.ts.snap │ │ └── graphql-upload.spec.ts │ └── tsconfig.json ├── hot-schema-reloading │ ├── README.md │ ├── package.json │ ├── src │ │ ├── SchemaLoader.ts │ │ ├── gateway.ts │ │ ├── index.ts │ │ └── services │ │ │ ├── endpoints.ts │ │ │ ├── inventory │ │ │ ├── index.ts │ │ │ └── server.ts │ │ │ └── products │ │ │ ├── index.ts │ │ │ └── server.ts │ ├── tests │ │ ├── __snapshots__ │ │ │ └── hot-schema-reloading.spec.ts.snap │ │ └── hot-schema-reloading.spec.ts │ └── tsconfig.json ├── mutations-and-subscriptions │ ├── README.md │ ├── package.json │ ├── src │ │ ├── gateway.ts │ │ ├── index.ts │ │ └── services │ │ │ ├── posts │ │ │ ├── index.ts │ │ │ └── server.ts │ │ │ └── users │ │ │ ├── index.ts │ │ │ └── server.ts │ ├── tests │ │ └── mutations-and-subscriptions.test.ts │ └── tsconfig.json ├── persistent-connection │ ├── package.json │ ├── src │ │ ├── gateway.ts │ │ ├── index.ts │ │ └── services │ │ │ ├── inventory │ │ │ ├── index.ts │ │ │ └── server.ts │ │ │ └── products │ │ │ ├── index.ts │ │ │ └── server.ts │ ├── tests │ │ └── persistent-connection.test.ts │ └── tsconfig.json ├── public-and-private-apis │ ├── README.md │ ├── gateway.ts │ ├── index.ts │ ├── package.json │ ├── services │ │ ├── accounts.ts │ │ ├── products.ts │ │ └── reviews.ts │ ├── tests │ │ ├── __snapshots__ │ │ │ └── public-and-private-apis.test.ts.snap │ │ └── public-and-private-apis.test.ts │ └── tsconfig.json ├── stitching-directives-sdl │ ├── README.md │ ├── package.json │ ├── src │ │ ├── gateway.ts │ │ ├── index.ts │ │ └── services │ │ │ ├── accounts │ │ │ ├── index.ts │ │ │ └── server.ts │ │ │ ├── inventory │ │ │ ├── index.ts │ │ │ └── server.ts │ │ │ ├── products │ │ │ ├── index.ts │ │ │ └── server.ts │ │ │ └── reviews │ │ │ ├── index.ts │ │ │ └── server.ts │ ├── tests │ │ └── stitching-directives-sdl.test.ts │ └── tsconfig.json ├── subservice-languages │ ├── javascript │ │ ├── gateway.js │ │ ├── index.js │ │ ├── package.json │ │ ├── services │ │ │ ├── accounts │ │ │ │ ├── index.js │ │ │ │ └── server.js │ │ │ ├── inventory │ │ │ │ ├── index.js │ │ │ │ └── server.js │ │ │ ├── products │ │ │ │ ├── index.ts │ │ │ │ └── server.ts │ │ │ └── reviews │ │ │ │ ├── index.js │ │ │ │ └── server.js │ │ ├── tests │ │ │ ├── __snapshots__ │ │ │ │ └── gateway.spec.ts.snap │ │ │ └── gateway.spec.ts │ │ └── tsconfig.json │ └── ruby │ │ ├── .gitignore │ │ ├── Gemfile │ │ ├── Gemfile.lock │ │ ├── gateway.ts │ │ ├── index.ts │ │ ├── lib │ │ ├── base_schema.rb │ │ └── graphql_server.rb │ │ ├── package.json │ │ ├── services │ │ ├── accounts.rb │ │ ├── products.rb │ │ └── reviews.rb │ │ ├── tests │ │ ├── __snapshots__ │ │ │ └── ruby.test.ts.snap │ │ └── ruby.test.ts │ │ └── tsconfig.json ├── type-merging-arrays │ ├── README.md │ ├── package.json │ ├── src │ │ ├── gateway.ts │ │ ├── index.ts │ │ ├── services │ │ │ ├── manufacturers │ │ │ │ ├── index.ts │ │ │ │ └── server.ts │ │ │ ├── products │ │ │ │ ├── index.ts │ │ │ │ └── server.ts │ │ │ └── storefronts │ │ │ │ ├── index.ts │ │ │ │ └── server.ts │ │ └── tests │ │ │ └── type-merging-arrays.test.ts │ └── tsconfig.json ├── type-merging-interfaces │ ├── README.md │ ├── package.json │ ├── src │ │ ├── gateway.ts │ │ ├── index.ts │ │ └── services │ │ │ ├── products.ts │ │ │ └── storefronts.ts │ ├── tests │ │ └── type-merging-interfaces.test.ts │ └── tsconfig.json ├── type-merging-multiple-keys │ ├── README.md │ ├── package.json │ ├── src │ │ ├── gateway.ts │ │ ├── index.ts │ │ └── services │ │ │ ├── catalog.ts │ │ │ ├── reviews.ts │ │ │ └── vendors.ts │ ├── tests │ │ └── type-merging-multiple-keys.test.ts │ └── tsconfig.json ├── type-merging-nullables │ ├── README.md │ ├── package.json │ ├── src │ │ ├── gateway.ts │ │ ├── index.ts │ │ └── services │ │ │ ├── products.ts │ │ │ ├── reviews.ts │ │ │ └── users.ts │ ├── tests │ │ └── type-merging-nullables.test.ts │ └── tsconfig.json ├── type-merging-single-records │ ├── README.md │ ├── package.json │ ├── src │ │ ├── gateway.ts │ │ ├── index.ts │ │ ├── services │ │ │ ├── manufacturers │ │ │ │ ├── index.ts │ │ │ │ └── server.ts │ │ │ ├── products │ │ │ │ ├── index.ts │ │ │ │ └── server.ts │ │ │ └── storefronts │ │ │ │ ├── index.ts │ │ │ │ └── server.ts │ │ └── tests │ │ │ └── type-merging-single-records.test.ts │ └── tsconfig.json └── versioning-schema-releases │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── repo.template.json │ ├── src │ ├── github_client.ts │ ├── index.ts │ ├── schema_registry.ts │ └── services │ │ ├── inventory.ts │ │ ├── products.ts │ │ └── registry.ts │ └── tsconfig.json ├── jest.config.js ├── package.json ├── prettier.config.mjs ├── renovate.json ├── tsconfig.json ├── website ├── next-env.d.ts ├── next-sitemap.config.js ├── next.config.ts ├── package.json ├── postcss.config.js ├── public │ └── assets │ │ ├── banner-1.jpg │ │ ├── distributed-graph.png │ │ ├── needle.svg │ │ └── stitching-flow.png ├── src │ ├── app │ │ ├── [...mdxPath] │ │ │ └── page.tsx │ │ ├── giscus.tsx │ │ ├── icon.svg │ │ ├── layout.tsx │ │ └── page.tsx │ ├── content │ │ ├── _meta.ts │ │ ├── docs │ │ │ ├── _meta.ts │ │ │ ├── approaches │ │ │ │ ├── _meta.ts │ │ │ │ ├── index.mdx │ │ │ │ ├── schema-extensions.mdx │ │ │ │ ├── stitching-directives.mdx │ │ │ │ └── type-merging.mdx │ │ │ ├── getting-started │ │ │ │ ├── _meta.ts │ │ │ │ ├── adding-transforms.mdx │ │ │ │ ├── basic-example.mdx │ │ │ │ ├── duplicate-types.mdx │ │ │ │ ├── error-handling.mdx │ │ │ │ └── remote-subschemas.mdx │ │ │ ├── index.mdx │ │ │ └── transforms │ │ │ │ ├── _meta.ts │ │ │ │ ├── cleanup.mdx │ │ │ │ ├── custom-transforms.mdx │ │ │ │ ├── filtering.mdx │ │ │ │ ├── index.mdx │ │ │ │ ├── modifying.mdx │ │ │ │ ├── operational.mdx │ │ │ │ └── renaming.mdx │ │ └── handbook │ │ │ ├── _meta.ts │ │ │ ├── appendices │ │ │ ├── _meta.ts │ │ │ └── batching-arrays-and-queries.mdx │ │ │ ├── architecture │ │ │ ├── _meta.ts │ │ │ ├── continuous-integration-testing.mdx │ │ │ ├── hot-schema-reloading.mdx │ │ │ ├── persistent-connection.mdx │ │ │ ├── public-and-private-apis.mdx │ │ │ └── versioning-schema-releases.mdx │ │ │ ├── foundation │ │ │ ├── _meta.ts │ │ │ ├── combining-local-and-remote-schemas.mdx │ │ │ ├── computed-fields.mdx │ │ │ ├── custom-merge-resolvers.mdx │ │ │ ├── mutations-and-subscriptions.mdx │ │ │ ├── stitching-directives-sdl.mdx │ │ │ ├── type-merging-arrays.mdx │ │ │ ├── type-merging-interfaces.mdx │ │ │ ├── type-merging-multiple-keys.mdx │ │ │ ├── type-merging-nullables.mdx │ │ │ └── type-merging-single-records.mdx │ │ │ ├── index.mdx │ │ │ └── other-integrations │ │ │ ├── _meta.ts │ │ │ ├── federation-supergraph.mdx │ │ │ ├── federation-to-stitching-sdl.mdx │ │ │ ├── graphql-upload.mdx │ │ │ └── subservice-languages │ │ │ ├── _meta.ts │ │ │ ├── index.mdx │ │ │ ├── javascript.mdx │ │ │ └── ruby.mdx │ └── mdx-components.js ├── tailwind.config.ts └── tsconfig.json └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.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://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: 'npm' # See documentation for possible values 9 | directory: '/' # Location of package manifests 10 | schedule: 11 | interval: 'weekly' 12 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | env: 4 | NODE_NO_WARNINGS: true 5 | NODE_OPTIONS: '--max-old-space-size=8192' 6 | CI: true 7 | 8 | on: 9 | push: 10 | branches: 11 | - master 12 | pull_request: 13 | branches: 14 | - master 15 | 16 | concurrency: 17 | group: ${{ github.workflow }}-${{ github.ref }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | prettier-check: 22 | name: prettier 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout Master 26 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 27 | 28 | - name: Setup env 29 | uses: the-guild-org/shared-config/setup@main 30 | with: 31 | nodeVersion: 18 32 | 33 | - name: Prettier Check 34 | run: yarn prettier:check 35 | lint: 36 | name: lint 37 | uses: the-guild-org/shared-config/.github/workflows/lint.yml@main 38 | with: 39 | script: yarn ci:lint 40 | secrets: 41 | githubToken: ${{ secrets.GITHUB_TOKEN }} 42 | unit: 43 | name: unit 44 | runs-on: ubuntu-latest 45 | steps: 46 | - name: Checkout Master 47 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 48 | 49 | - name: Setup Ruby 50 | uses: ruby/setup-ruby@v1 51 | with: 52 | ruby-version: 2.7 53 | 54 | - name: Setup env 55 | uses: the-guild-org/shared-config/setup@main 56 | with: 57 | nodeVersion: 18 58 | 59 | - name: Cache Jest 60 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4 61 | with: 62 | path: .cache/jest 63 | key: ${{ runner.os }}-jest-${{ hashFiles('yarn.lock') }} 64 | restore-keys: | 65 | ${{ runner.os }}-jest- 66 | 67 | - name: Test 68 | run: yarn test --ci 69 | -------------------------------------------------------------------------------- /.github/workflows/website.yml: -------------------------------------------------------------------------------- 1 | name: website 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | deployment: 17 | runs-on: ubuntu-latest 18 | if: 19 | github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 20 | 'push' 21 | steps: 22 | - name: checkout 23 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 24 | with: 25 | fetch-depth: 0 26 | 27 | - uses: the-guild-org/shared-config/setup@main 28 | name: setup env 29 | with: 30 | nodeVersion: 20 31 | packageManager: yarn 32 | - uses: the-guild-org/shared-config/website-cf@main 33 | name: build and deploy website 34 | env: 35 | NEXT_BASE_PATH: ${{ github.ref == 'refs/heads/master' && '/graphql/stitching' || '' }} 36 | SITE_URL: 37 | ${{ github.ref == 'refs/heads/master' && 'https://the-guild.dev/graphql/stitching' || '' 38 | }} 39 | with: 40 | cloudflareApiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} 41 | cloudflareAccountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 42 | githubToken: ${{ secrets.GITHUB_TOKEN }} 43 | projectName: schema-stitching 44 | prId: ${{ github.event.pull_request.number }} 45 | websiteDirectory: ./ 46 | buildScript: cd website && yarn build 47 | artifactDir: website/out 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next/ 62 | 63 | dist/ 64 | build/ 65 | temp 66 | .idea/ 67 | .bob/ 68 | .cache 69 | .DS_Store 70 | 71 | test-results/ 72 | junit.xml 73 | 74 | package-lock.json 75 | website/public/_redirects 76 | website/public/sitemap.xml 77 | website/out/ 78 | -------------------------------------------------------------------------------- /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | yarn lint-staged 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | node_modules 3 | tests 4 | !dist 5 | .circleci 6 | .prettierrc 7 | bump.js 8 | jest.config.js 9 | tsconfig.json 10 | yarn.lock 11 | yarn-error.log 12 | bundle-test 13 | *.tgz 14 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .husky/_/ 2 | .changeset/*-dependencies.md 3 | .dist/ 4 | .bob/ 5 | website/.next/ 6 | website/out/ 7 | website/public/ 8 | .husky/ 9 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | --ignore-engines true 2 | --network-timeout 1000000000 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Arda TANRIKULU 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GraphQLConf 2024 Banner: September 10-12, San Francisco. Hosted by the GraphQL Foundation](https://github.com/user-attachments/assets/bdb8cd5d-5186-4ece-b06b-b00a499b7868)](https://graphql.org/conf/2024/?utm_source=github&utm_medium=schema_stitching&utm_campaign=readme) 2 | 3 | # Schema Stitching 4 | 5 | [![npm version](https://badge.fury.io/js/%40graphql-tools%2Fstitch.svg)](https://badge.fury.io/js/%40graphql-tools%2Fstitch) 6 | 7 | ## Documentation 8 | 9 | [https://the-guild.dev/graphql/stitching](https://the-guild.dev/graphql/stitching) 10 | 11 | ## Contributions 12 | 13 | Contributions, issues and feature requests are very welcome. If you are using this package and fixed 14 | a bug for yourself, please consider submitting a PR! 15 | 16 | And if this is your first time contributing to this project, please do read our 17 | [Contributor Workflow Guide](https://github.com/the-guild-org/Stack/blob/master/CONTRIBUTING.md) 18 | before you get started off. 19 | 20 | ## Code of Conduct 21 | 22 | Help us keep GraphQL ESLint open and inclusive. Please read and follow our 23 | [Code of Conduct](https://github.com/the-guild-org/Stack/blob/master/CODE_OF_CONDUCT.md) as adopted 24 | from [Contributor Covenant](https://contributor-covenant.org). 25 | 26 | ## License 27 | 28 | Released under the [MIT license](./LICENSE). 29 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: process.versions.node.split('.')[0] } }], 4 | '@babel/preset-typescript', 5 | ], 6 | plugins: [ 7 | 'babel-plugin-transform-typescript-metadata', 8 | ['@babel/plugin-proposal-decorators', { legacy: true }], 9 | 'babel-plugin-parameter-decorator', 10 | '@babel/plugin-proposal-class-properties', 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /examples/combining-local-and-remote-schemas/README.md: -------------------------------------------------------------------------------- 1 | # Combining local and remote schemas 2 | 3 | [See the details about this example on the website](https://the-guild.dev/graphql/stitching/handbook/foundation/combining-local-and-remote-schemas) 4 | -------------------------------------------------------------------------------- /examples/combining-local-and-remote-schemas/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "combining-local-and-remote-schemas", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "concurrently \"yarn:start-*\"", 8 | "start-gateway": "ts-node-dev src/index.ts", 9 | "start-products": "ts-node-dev src/services/products/index.ts", 10 | "start-storefronts": "ts-node-dev src/services/storefronts/index.ts" 11 | }, 12 | "dependencies": { 13 | "@graphql-tools/executor-http": "^2.0.0", 14 | "@graphql-tools/stitch": "^9.0.0", 15 | "@graphql-tools/utils": "^10.0.0", 16 | "@graphql-tools/wrap": "^10.0.0", 17 | "@types/node": "^22.0.0", 18 | "@types/wait-on": "^5.3.1", 19 | "concurrently": "^9.0.0", 20 | "graphql": "^16.6.0", 21 | "graphql-yoga": "^5.0.0", 22 | "ts-node": "^10.9.1", 23 | "ts-node-dev": "^2.0.0", 24 | "typescript": "^5.0.0", 25 | "wait-on": "^8.0.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/combining-local-and-remote-schemas/sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "node", 3 | "container": { 4 | "node": "16", 5 | "port": 4000 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/combining-local-and-remote-schemas/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { gatewayApp } from './gateway'; 3 | 4 | const server = createServer(gatewayApp); 5 | server.listen(4000, () => console.log('gateway running at http://localhost:4000/graphql')); 6 | -------------------------------------------------------------------------------- /examples/combining-local-and-remote-schemas/src/services/local/schema.ts: -------------------------------------------------------------------------------- 1 | import { createSchema } from 'graphql-yoga'; 2 | 3 | export const localSchema = createSchema({ 4 | typeDefs: /* GraphQL */ ` 5 | type Query { 6 | errorCodes: [String!]! 7 | } 8 | `, 9 | resolvers: { 10 | Query: { 11 | errorCodes: () => ['NOT_FOUND', 'GRAPHQL_PARSE_FAILED', 'GRAPHQL_VALIDATION_FAILED'], 12 | }, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /examples/combining-local-and-remote-schemas/src/services/products/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { createYoga } from 'graphql-yoga'; 3 | import { schema } from './schema'; 4 | 5 | const yoga = createYoga({ 6 | schema, 7 | }); 8 | 9 | const server = createServer(yoga); 10 | 11 | server.listen(4001, () => console.log('products running at http://localhost:4001/graphql')); 12 | -------------------------------------------------------------------------------- /examples/combining-local-and-remote-schemas/src/services/products/schema.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql'; 2 | import { createSchema } from 'graphql-yoga'; 3 | 4 | // data fixtures 5 | const products = [ 6 | { upc: '1', name: 'Cookbook', price: 15.99 }, 7 | { upc: '2', name: 'Toothbrush', price: 3.99 }, 8 | ]; 9 | 10 | export const schema = createSchema({ 11 | typeDefs: /* GraphQL */ ` 12 | type Product { 13 | name: String! 14 | price: Float! 15 | upc: ID! 16 | } 17 | 18 | type Query { 19 | product(upc: ID!): Product 20 | } 21 | `, 22 | resolvers: { 23 | Query: { 24 | product: (root, { upc }) => 25 | products.find(p => p.upc === upc) || 26 | new GraphQLError('Record not found', { 27 | extensions: { 28 | code: 'NOT_FOUND', 29 | }, 30 | }), 31 | }, 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /examples/combining-local-and-remote-schemas/src/services/storefronts/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { createYoga } from 'graphql-yoga'; 3 | import { schema } from './schema'; 4 | 5 | const yoga = createYoga({ 6 | schema, 7 | }); 8 | 9 | const server = createServer(yoga); 10 | 11 | server.listen(4002, () => console.log('storefronts running at http://localhost:4002/graphql')); 12 | -------------------------------------------------------------------------------- /examples/combining-local-and-remote-schemas/src/services/storefronts/schema.graphql: -------------------------------------------------------------------------------- 1 | type Storefront { 2 | id: ID! 3 | name: String! 4 | } 5 | 6 | type Query { 7 | storefront(id: ID!): Storefront 8 | _sdl: String! 9 | } 10 | -------------------------------------------------------------------------------- /examples/combining-local-and-remote-schemas/src/services/storefronts/schema.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql'; 2 | import { createSchema } from 'graphql-yoga'; 3 | 4 | // data fixtures 5 | const storefronts = [ 6 | { id: '1', name: 'The Product Store' }, 7 | { id: '2', name: 'eShoppe' }, 8 | ]; 9 | 10 | const typeDefs = /* GraphQL */ ` 11 | type Storefront { 12 | id: ID! 13 | name: String! 14 | } 15 | 16 | type Query { 17 | storefront(id: ID!): Storefront 18 | _sdl: String! 19 | } 20 | `; 21 | 22 | export const schema = createSchema({ 23 | typeDefs, 24 | resolvers: { 25 | Query: { 26 | storefront: (root, { id }) => 27 | storefronts.find(s => s.id === id) || 28 | new GraphQLError('Record not found', { 29 | extensions: { 30 | code: 'NOT_FOUND', 31 | }, 32 | }), 33 | _sdl: () => typeDefs, 34 | }, 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /examples/combining-local-and-remote-schemas/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "lib": ["esnext", "DOM"], 10 | "skipLibCheck": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/computed-fields/README.md: -------------------------------------------------------------------------------- 1 | # Computed fields 2 | 3 | [See the details about this example on the website](https://the-guild.dev/graphql/stitching/handbook/foundation/computed-fields) 4 | -------------------------------------------------------------------------------- /examples/computed-fields/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "computed-fields", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "private": "true", 6 | "scripts": { 7 | "start": "ts-node-dev src/index.ts" 8 | }, 9 | "dependencies": { 10 | "@graphql-tools/stitch": "^9.0.0", 11 | "@graphql-tools/wrap": "^10.0.0", 12 | "@types/node": "^22.0.0", 13 | "graphql": "^16.6.0", 14 | "graphql-yoga": "^5.0.0", 15 | "ts-node": "^10.9.1", 16 | "ts-node-dev": "^2.0.0", 17 | "typescript": "^5.0.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/computed-fields/src/gateway.ts: -------------------------------------------------------------------------------- 1 | import { createYoga } from 'graphql-yoga'; 2 | import { stitchSchemas } from '@graphql-tools/stitch'; 3 | import { RemoveObjectFieldDeprecations } from '@graphql-tools/wrap'; 4 | import { metadataSchema } from './services/metadata'; 5 | import { productsSchema } from './services/products'; 6 | 7 | function makeGatewaySchema() { 8 | return stitchSchemas({ 9 | subschemas: [ 10 | { 11 | schema: metadataSchema, 12 | transforms: [new RemoveObjectFieldDeprecations('gateway access only')], 13 | batch: true, 14 | merge: { 15 | Product: { 16 | // selectionSet: '{ upc }', << technically not necessary here! 17 | fields: { 18 | category: { selectionSet: '{ categoryId }', computed: true }, 19 | metadata: { selectionSet: '{ metadataIds }', computed: true }, 20 | }, 21 | fieldName: '_products', 22 | key: ({ categoryId, metadataIds }) => ({ categoryId, metadataIds }), 23 | argsFromKeys: keys => ({ keys }), 24 | }, 25 | }, 26 | }, 27 | { 28 | schema: productsSchema, 29 | batch: true, 30 | }, 31 | ], 32 | }); 33 | } 34 | 35 | export const gatewayApp = createYoga({ 36 | schema: makeGatewaySchema(), 37 | maskedErrors: false, 38 | graphiql: { 39 | title: 'Computed fields', 40 | defaultQuery: /* GraphQL */ ` 41 | query { 42 | products(upcs: [1, 2, 3, 4]) { 43 | name 44 | price 45 | category { 46 | name 47 | } 48 | metadata { 49 | __typename 50 | name 51 | ... on GeoLocation { 52 | name 53 | lat 54 | lon 55 | } 56 | ... on SportsTeam { 57 | location { 58 | name 59 | lat 60 | lon 61 | } 62 | } 63 | ... on TelevisionSeries { 64 | season 65 | } 66 | } 67 | } 68 | } 69 | `, 70 | }, 71 | }); 72 | -------------------------------------------------------------------------------- /examples/computed-fields/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { gatewayApp } from './gateway'; 3 | 4 | createServer(gatewayApp).listen(4000, () => 5 | console.log(`gateway running at http://localhost:4000/graphql`), 6 | ); 7 | -------------------------------------------------------------------------------- /examples/computed-fields/src/services/products.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql'; 2 | import { createSchema } from 'graphql-yoga'; 3 | 4 | // data fixtures 5 | const products = [ 6 | { upc: '1', name: 'iPhone', price: 699.99, categoryId: null, metadataIds: [] }, 7 | { 8 | upc: '2', 9 | name: 'The Best Baking Cookbook', 10 | price: 15.99, 11 | categoryId: '2', 12 | metadataIds: ['3', '4'], 13 | }, 14 | { upc: '3', name: 'Argentina Guidebook', price: 24.99, categoryId: '3', metadataIds: ['5'] }, 15 | { upc: '4', name: 'Soccer Jersey', price: 47.99, categoryId: '1', metadataIds: ['1', '2'] }, 16 | ]; 17 | 18 | export const productsSchema = createSchema({ 19 | typeDefs: /* GraphQL */ ` 20 | type Product { 21 | categoryId: ID 22 | metadataIds: [ID!] 23 | name: String! 24 | price: Float! 25 | upc: ID! 26 | } 27 | 28 | type Query { 29 | products(upcs: [ID!]!): [Product]! 30 | } 31 | `, 32 | resolvers: { 33 | Query: { 34 | products: (root, { upcs }) => 35 | upcs.map( 36 | (upc: string) => 37 | products.find(p => p.upc === upc) || 38 | new GraphQLError('Record not found', { 39 | extensions: { 40 | code: 'NOT_FOUND', 41 | }, 42 | }), 43 | ), 44 | }, 45 | }, 46 | }); 47 | -------------------------------------------------------------------------------- /examples/computed-fields/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "lib": ["esnext", "DOM"], 10 | "skipLibCheck": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/continuous-integration-testing/README.md: -------------------------------------------------------------------------------- 1 | # Continuous Integration Testing 2 | 3 | [See the details about this example on the website](https://the-guild.dev/graphql/stitching/handbook/architecture/continuous-integration-testing) 4 | -------------------------------------------------------------------------------- /examples/continuous-integration-testing/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: process.versions.node.split('.')[0] } }], 4 | '@babel/preset-typescript', 5 | ], 6 | plugins: ['@babel/plugin-proposal-class-properties'], 7 | }; 8 | -------------------------------------------------------------------------------- /examples/continuous-integration-testing/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'node', 3 | restoreMocks: true, 4 | reporters: ['default'], 5 | collectCoverage: false, 6 | transform: { 7 | '^.+\\.mjs?$': 'babel-jest', 8 | '^.+\\.ts?$': 'babel-jest', 9 | '^.+\\.js$': 'babel-jest', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /examples/continuous-integration-testing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "continuous-integration-testing", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "ts-node-dev src/index.ts", 8 | "test": "jest test" 9 | }, 10 | "dependencies": { 11 | "@graphql-tools/delegate": "^10.0.0", 12 | "@graphql-tools/executor-http": "^2.0.0", 13 | "@graphql-tools/stitch": "^9.0.0", 14 | "@graphql-tools/stitching-directives": "^3.0.0", 15 | "graphql": "^16.6.0", 16 | "graphql-yoga": "^5.0.0" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "7.27.3", 20 | "@babel/plugin-proposal-class-properties": "7.18.6", 21 | "@babel/preset-env": "7.27.2", 22 | "@babel/preset-typescript": "7.27.1", 23 | "@graphql-tools/mock": "^9.0.0", 24 | "@graphql-tools/utils": "^10.0.0", 25 | "@types/jest": "^29.2.6", 26 | "@types/node": "^22.0.0", 27 | "jest": "^29.0.0", 28 | "typescript": "5.8.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/continuous-integration-testing/src/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "services": { 3 | "products": { 4 | "url": "http://localhost:4001/graphql" 5 | }, 6 | "reviews": { 7 | "url": "http://localhost:4002/graphql" 8 | }, 9 | "users": { 10 | "url": "http://localhost:4003/graphql" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/continuous-integration-testing/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'node:http'; 2 | import { createYoga } from 'graphql-yoga'; 3 | import { buildGatewaySchema, buildSubschemaConfigs } from './schema_builder'; 4 | 5 | const schema = buildGatewaySchema(buildSubschemaConfigs()); 6 | 7 | const server = createServer(createYoga({ schema })); 8 | 9 | server.listen(4000, () => console.log('gateway running at http://localhost:4000/graphql')); 10 | -------------------------------------------------------------------------------- /examples/continuous-integration-testing/src/remote_schemas/products.graphql: -------------------------------------------------------------------------------- 1 | directive @key(selectionSet: String!) on OBJECT 2 | directive @merge( 3 | keyField: String 4 | keyArg: String 5 | additionalArgs: String 6 | key: [String!] 7 | argsExpr: String 8 | ) on FIELD_DEFINITION 9 | directive @computed(selectionSet: String!) on FIELD_DEFINITION 10 | 11 | type Product { 12 | upc: ID! 13 | name: String! 14 | price: Int! 15 | weight: Int! 16 | } 17 | 18 | type Query { 19 | products(upcs: [ID!]!): [Product]! @merge(keyField: "upc") 20 | _sdl: String! 21 | } 22 | -------------------------------------------------------------------------------- /examples/continuous-integration-testing/src/remote_schemas/reviews.graphql: -------------------------------------------------------------------------------- 1 | directive @key(selectionSet: String!) on OBJECT 2 | directive @merge( 3 | keyField: String 4 | keyArg: String 5 | additionalArgs: String 6 | key: [String!] 7 | argsExpr: String 8 | ) on FIELD_DEFINITION 9 | directive @computed(selectionSet: String!) on FIELD_DEFINITION 10 | 11 | type Review { 12 | id: ID! 13 | body: String 14 | author: User 15 | product: Product 16 | } 17 | 18 | type User { 19 | id: ID! 20 | totalReviews: Int! 21 | reviews: [Review] 22 | } 23 | 24 | type Product { 25 | upc: ID! 26 | reviews: [Review] 27 | } 28 | 29 | type Query { 30 | review(id: ID!): Review 31 | _users(ids: [ID!]!): [User]! @merge(keyField: "id") 32 | _products(upcs: [ID!]!): [Product]! @merge(keyField: "upc") 33 | _sdl: String! 34 | } 35 | -------------------------------------------------------------------------------- /examples/continuous-integration-testing/src/remote_schemas/users.graphql: -------------------------------------------------------------------------------- 1 | directive @key(selectionSet: String!) on OBJECT 2 | directive @merge( 3 | keyField: String 4 | keyArg: String 5 | additionalArgs: String 6 | key: [String!] 7 | argsExpr: String 8 | ) on FIELD_DEFINITION 9 | directive @computed(selectionSet: String!) on FIELD_DEFINITION 10 | 11 | type User { 12 | id: ID! 13 | name: String! 14 | username: String! 15 | } 16 | 17 | type Query { 18 | user(id: ID!): User @merge(keyField: "id") 19 | _sdl: String! 20 | } 21 | -------------------------------------------------------------------------------- /examples/continuous-integration-testing/src/schema_builder.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs'; 2 | import { join } from 'node:path'; 3 | import { buildSchema } from 'graphql'; 4 | import { SubschemaConfig } from '@graphql-tools/delegate'; 5 | import { buildHTTPExecutor } from '@graphql-tools/executor-http'; 6 | import { stitchSchemas } from '@graphql-tools/stitch'; 7 | import { stitchingDirectives } from '@graphql-tools/stitching-directives'; 8 | import config from './config.json'; 9 | 10 | export function buildSubschemaConfigs(): Record { 11 | return Object.entries(config.services).reduce((memo, [name, settings]) => { 12 | const sdl = readFileSync(join(__dirname, `./remote_schemas/${name}.graphql`), 'utf-8'); 13 | memo[name] = { 14 | schema: buildSchema(sdl), 15 | executor: buildHTTPExecutor({ endpoint: settings.url }), 16 | batch: true, 17 | }; 18 | return memo; 19 | }, {}); 20 | } 21 | 22 | export function buildGatewaySchema(subschemasByName: Record) { 23 | const { stitchingDirectivesTransformer } = stitchingDirectives(); 24 | 25 | return stitchSchemas({ 26 | subschemaConfigTransforms: [stitchingDirectivesTransformer], 27 | subschemas: Object.values(subschemasByName), 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /examples/continuous-integration-testing/test/mock_services/products.ts: -------------------------------------------------------------------------------- 1 | const products = [ 2 | { upc: '1', name: 'gizmo' }, 3 | { upc: '2', name: 'widget' }, 4 | ]; 5 | 6 | export const resolvers = { 7 | Query: { 8 | products: (_, { upcs }) => upcs.map(upc => products.find(p => p.upc === upc)), 9 | }, 10 | }; 11 | 12 | export const mocks = { 13 | Int: () => 23, 14 | }; 15 | -------------------------------------------------------------------------------- /examples/continuous-integration-testing/test/mock_services/reviews.ts: -------------------------------------------------------------------------------- 1 | import { IResolvers } from '@graphql-tools/utils'; 2 | 3 | const reviews = [ 4 | { id: '1', body: 'great', author: { id: '1' }, product: { upc: '1' } }, 5 | { id: '2', body: 'awful', author: { id: '2' }, product: { upc: '2' } }, 6 | ]; 7 | 8 | export const resolvers: IResolvers = { 9 | Query: { 10 | review: (_, { id }) => reviews.find(r => r.id === id), 11 | _users: (_, { ids }) => ids.map((id: string) => ({ id })), 12 | _products: (_, { upcs }) => upcs.map((upc: string) => ({ upc })), 13 | }, 14 | Product: { 15 | reviews: p => reviews.filter(r => r.product.upc === p.upc), 16 | }, 17 | User: { 18 | reviews: u => reviews.filter(r => r.author.id === u.id), 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /examples/continuous-integration-testing/test/mock_services/users.ts: -------------------------------------------------------------------------------- 1 | const users = [ 2 | { id: '1', username: 'hansolo' }, 3 | { id: '2', username: 'yoda' }, 4 | ]; 5 | 6 | export const resolvers = { 7 | Query: { 8 | user: (_, { id }) => users.find(u => u.id === id), 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /examples/continuous-integration-testing/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "lib": ["esnext", "DOM"], 10 | "skipLibCheck": true, 11 | "resolveJsonModule": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/custom-merge-resolvers/README.md: -------------------------------------------------------------------------------- 1 | # Custom merge resolvers 2 | 3 | [See the details about this example on the website](https://the-guild.dev/graphql/stitching/handbook/foundation/custom-merge-resolvers) 4 | -------------------------------------------------------------------------------- /examples/custom-merge-resolvers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "custom-merge-resolvers", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "private": "true", 6 | "scripts": { 7 | "start": "ts-node-dev src/index.ts" 8 | }, 9 | "dependencies": { 10 | "@graphql-tools/batch-delegate": "^9.0.0", 11 | "@graphql-tools/delegate": "^10.0.0", 12 | "@graphql-tools/stitch": "^9.0.0", 13 | "@types/node": "^22.0.0", 14 | "dataloader": "^2.1.0", 15 | "graphql": "^16.6.0", 16 | "graphql-yoga": "^5.0.0", 17 | "ts-node": "^10.9.1", 18 | "ts-node-dev": "^2.0.0", 19 | "typescript": "^5.0.0" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /examples/custom-merge-resolvers/src/gateway.ts: -------------------------------------------------------------------------------- 1 | import { createYoga } from 'graphql-yoga'; 2 | import { stitchSchemas } from '@graphql-tools/stitch'; 3 | import { infoSchema } from './services/info'; 4 | import { createInventoryResolver, inventorySchema } from './services/inventory'; 5 | import { createPricingResolver, pricingSchema } from './services/pricing'; 6 | 7 | function makeGatewaySchema() { 8 | // For simplicity, all services run locally in this example. 9 | // Any of these services could easily be turned into a remote server (see Example 1). 10 | return stitchSchemas({ 11 | subschemas: [ 12 | { 13 | schema: infoSchema, 14 | merge: { 15 | Product: { 16 | selectionSet: '{ id }', 17 | fieldName: 'productsInfo', 18 | key: ({ id }) => id, 19 | argsFromKeys: ids => ({ whereIn: ids }), 20 | valuesFromResults: (results, keys) => { 21 | const valuesByKey = Object.create(null); 22 | for (const val of results) valuesByKey[val.id] = val; 23 | return keys.map(key => valuesByKey[key] || null); 24 | }, 25 | }, 26 | }, 27 | }, 28 | { 29 | schema: inventorySchema, 30 | merge: { 31 | Product: { 32 | selectionSet: '{ id }', 33 | key: ({ id }) => id, 34 | resolve: createInventoryResolver({ 35 | fieldName: 'productsInventory', 36 | argsFromKeys: ids => ({ ids }), 37 | }), 38 | }, 39 | }, 40 | }, 41 | { 42 | schema: pricingSchema, 43 | merge: { 44 | Product: { 45 | selectionSet: '{ id }', 46 | key: ({ id }) => id, 47 | resolve: createPricingResolver(), 48 | }, 49 | }, 50 | }, 51 | ], 52 | }); 53 | } 54 | 55 | export const gatewayApp = createYoga({ 56 | schema: makeGatewaySchema(), 57 | maskedErrors: false, 58 | graphiql: { 59 | title: 'Custom merge resolvers', 60 | defaultQuery: /* GraphQL */ ` 61 | query { 62 | productsInfo(whereIn: ["1", "X", "2", "3"]) { 63 | id 64 | title 65 | totalInventory 66 | price 67 | } 68 | } 69 | `, 70 | }, 71 | }); 72 | -------------------------------------------------------------------------------- /examples/custom-merge-resolvers/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { gatewayApp } from './gateway'; 3 | 4 | createServer(gatewayApp).listen(4000, () => 5 | console.log(`gateway running at http://localhost:4000/graphql`), 6 | ); 7 | -------------------------------------------------------------------------------- /examples/custom-merge-resolvers/src/services/info.ts: -------------------------------------------------------------------------------- 1 | import { createSchema } from 'graphql-yoga'; 2 | 3 | const products = [ 4 | { id: '1', title: 'Wallet' }, 5 | { id: '2', title: 'Watch' }, 6 | { id: '3', title: 'Hat' }, 7 | ]; 8 | 9 | export const infoSchema = createSchema({ 10 | typeDefs: /* GraphQL */ ` 11 | type Product { 12 | id: ID! 13 | title: String 14 | } 15 | 16 | type Query { 17 | productsInfo(whereIn: [ID!]!): [Product]! 18 | } 19 | `, 20 | resolvers: { 21 | Query: { 22 | productsInfo: (root, { whereIn }) => products.filter(p => whereIn.includes(p.id)), 23 | }, 24 | }, 25 | }); 26 | -------------------------------------------------------------------------------- /examples/custom-merge-resolvers/tests/custom-merge-resolvers.test.ts: -------------------------------------------------------------------------------- 1 | import { gatewayApp } from '../src/gateway'; 2 | 3 | describe('Custom merge resolvers', () => { 4 | it('should work', async () => { 5 | const response = await gatewayApp.fetch('/graphql', { 6 | method: 'POST', 7 | headers: { 8 | 'Content-Type': 'application/json', 9 | }, 10 | body: JSON.stringify({ 11 | query: /* GraphQL */ ` 12 | query { 13 | productsInfo(whereIn: ["1", "X", "2", "3"]) { 14 | id 15 | title 16 | totalInventory 17 | price 18 | } 19 | } 20 | `, 21 | }), 22 | }); 23 | const result = await response.json(); 24 | expect(result).toMatchInlineSnapshot(` 25 | { 26 | "data": { 27 | "productsInfo": [ 28 | { 29 | "id": "1", 30 | "price": 14, 31 | "title": "Wallet", 32 | "totalInventory": 1, 33 | }, 34 | { 35 | "id": "2", 36 | "price": null, 37 | "title": "Watch", 38 | "totalInventory": null, 39 | }, 40 | { 41 | "id": "3", 42 | "price": 22, 43 | "title": "Hat", 44 | "totalInventory": 5, 45 | }, 46 | ], 47 | }, 48 | } 49 | `); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /examples/custom-merge-resolvers/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "lib": ["esnext", "DOM"], 10 | "skipLibCheck": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/federation-supergraph/README.md: -------------------------------------------------------------------------------- 1 | # Federation Supergraph 2 | 3 | [See the details about this example on the website](https://the-guild.dev/graphql/stitching/handbook/other-integrations/federation-supergraph) 4 | -------------------------------------------------------------------------------- /examples/federation-supergraph/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "federation-supergraph", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "main": "index.js", 6 | "scripts": { 7 | "build-supergraph": "rover supergraph compose --config ./src/supergraph.yaml > ./src/supergraph.graphql", 8 | "start": "concurrently \"yarn:start-*\"", 9 | "start-accounts": "ts-node-dev src/services/accounts.ts", 10 | "start-gateway": "ts-node-dev src/index.ts", 11 | "start-inventory": "ts-node-dev src/services/inventory.ts", 12 | "start-products": "ts-node-dev src/services/products.ts", 13 | "start-reviews": "ts-node-dev src/services/reviews.ts" 14 | }, 15 | "dependencies": { 16 | "@apollo/subgraph": "^2.2.3", 17 | "@graphql-tools/federation": "^3.0.0", 18 | "@types/node": "^22.0.0", 19 | "concurrently": "^9.0.0", 20 | "graphql": "^16.6.0", 21 | "graphql-yoga": "^5.0.0", 22 | "ts-node": "^10.9.1", 23 | "ts-node-dev": "^2.0.0", 24 | "typescript": "^5.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/federation-supergraph/src/example-query.ts: -------------------------------------------------------------------------------- 1 | export const fragments = /* GraphQL */ ` 2 | fragment User on User { 3 | id 4 | username 5 | name 6 | } 7 | 8 | fragment Review on Review { 9 | id 10 | body 11 | } 12 | 13 | fragment Product on Product { 14 | inStock 15 | name 16 | price 17 | shippingEstimate 18 | upc 19 | weight 20 | } 21 | `; 22 | 23 | export const exampleQuery = /* GraphQL */ ` 24 | ${fragments} 25 | 26 | query TestQuery { 27 | users { 28 | ...User 29 | reviews { 30 | ...Review 31 | product { 32 | ...Product 33 | reviews { 34 | ...Review 35 | author { 36 | ...User 37 | reviews { 38 | ...Review 39 | product { 40 | ...Product 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | topProducts { 49 | ...Product 50 | reviews { 51 | ...Review 52 | author { 53 | ...User 54 | reviews { 55 | ...Review 56 | product { 57 | ...Product 58 | } 59 | } 60 | } 61 | } 62 | } 63 | } 64 | `; 65 | -------------------------------------------------------------------------------- /examples/federation-supergraph/src/example-subscription.ts: -------------------------------------------------------------------------------- 1 | import { fragments } from './example-query'; 2 | 3 | export const exampleSubscription = /* GraphQL */ ` 4 | subscription { 5 | newProduct { 6 | ...Product 7 | reviews { 8 | ...Review 9 | author { 10 | ...User 11 | reviews { 12 | ...Review 13 | product { 14 | ...Product 15 | } 16 | } 17 | } 18 | } 19 | } 20 | } 21 | ${fragments} 22 | `; 23 | -------------------------------------------------------------------------------- /examples/federation-supergraph/src/gateway.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { join } from 'path'; 3 | import { createYoga } from 'graphql-yoga'; 4 | import { getStitchedSchemaFromSupergraphSdl } from '@graphql-tools/federation'; 5 | import { exampleQuery } from './example-query'; 6 | 7 | export const yoga = createYoga({ 8 | schema: getStitchedSchemaFromSupergraphSdl({ 9 | supergraphSdl: readFileSync(join(__dirname, 'supergraph.graphql'), 'utf8'), 10 | }), 11 | graphiql: { 12 | defaultQuery: exampleQuery, 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /examples/federation-supergraph/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { yoga } from './gateway'; 3 | 4 | const server = createServer(yoga); 5 | 6 | server.listen(4000, () => { 7 | console.log('gateway running at http://localhost:4000/graphql'); 8 | }); 9 | -------------------------------------------------------------------------------- /examples/federation-supergraph/src/services/accounts.graphql: -------------------------------------------------------------------------------- 1 | type Query @extends { 2 | me: User 3 | users: [User] 4 | } 5 | 6 | type User @key(fields: "id") { 7 | id: ID! 8 | name: String 9 | birthDate: String 10 | username: String 11 | } 12 | -------------------------------------------------------------------------------- /examples/federation-supergraph/src/services/accounts.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { createServer } from 'http'; 3 | import { join } from 'path'; 4 | import { parse } from 'graphql'; 5 | import { createYoga } from 'graphql-yoga'; 6 | import { buildSubgraphSchema } from '@apollo/subgraph'; 7 | 8 | const users = [ 9 | { 10 | id: '1', 11 | name: 'Ada Lovelace', 12 | birthDate: '1815-12-10', 13 | username: '@ada', 14 | }, 15 | { 16 | id: '2', 17 | name: 'Alan Turing', 18 | birthDate: '1912-06-23', 19 | username: '@complete', 20 | }, 21 | ]; 22 | 23 | export const server = createServer( 24 | createYoga({ 25 | schema: buildSubgraphSchema([ 26 | { 27 | typeDefs: parse(readFileSync(join(__dirname, 'accounts.graphql'), 'utf8')), 28 | resolvers: { 29 | Query: { 30 | me() { 31 | return users[0]; 32 | }, 33 | users() { 34 | return users; 35 | }, 36 | }, 37 | User: { 38 | __resolveReference(object: { id: string }) { 39 | return users.find(user => user.id === object.id); 40 | }, 41 | }, 42 | }, 43 | }, 44 | ]), 45 | }), 46 | ); 47 | 48 | server.listen(4001, () => { 49 | console.log('accounts service running at http://localhost:4001/graphql'); 50 | }); 51 | -------------------------------------------------------------------------------- /examples/federation-supergraph/src/services/inventory.graphql: -------------------------------------------------------------------------------- 1 | type Product @key(fields: "upc") @extends { 2 | upc: String! @external 3 | weight: Int @external 4 | price: Int @external 5 | inStock: Boolean 6 | shippingEstimate: Int @requires(fields: "price weight") 7 | } 8 | -------------------------------------------------------------------------------- /examples/federation-supergraph/src/services/inventory.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { createServer } from 'http'; 3 | import { inspect } from 'node:util'; 4 | import { join } from 'path'; 5 | import { parse } from 'graphql'; 6 | import { createYoga } from 'graphql-yoga'; 7 | import { buildSubgraphSchema } from '@apollo/subgraph'; 8 | 9 | const inventory = [ 10 | { upc: '1', inStock: true }, 11 | { upc: '2', inStock: false }, 12 | { upc: '3', inStock: true }, 13 | ]; 14 | 15 | export const server = createServer( 16 | createYoga({ 17 | schema: buildSubgraphSchema([ 18 | { 19 | typeDefs: parse(readFileSync(join(__dirname, 'inventory.graphql'), 'utf8')), 20 | resolvers: { 21 | Product: { 22 | __resolveReference(object) { 23 | return { 24 | ...object, 25 | ...inventory.find(product => product.upc === object.upc), 26 | }; 27 | }, 28 | shippingEstimate(object) { 29 | if (object.price == null || object.weight == null) { 30 | throw new Error( 31 | `${inspect(object)} doesn't have required fields; "price" and "weight".`, 32 | ); 33 | } 34 | // free for expensive items 35 | if (object.price > 1000) return 0; 36 | // estimate is based on weight 37 | return object.weight * 0.5; 38 | }, 39 | }, 40 | }, 41 | }, 42 | ]), 43 | }), 44 | ); 45 | 46 | server.listen(4002, () => { 47 | console.log('inventory service running at http://localhost:4002/graphql'); 48 | }); 49 | -------------------------------------------------------------------------------- /examples/federation-supergraph/src/services/products.graphql: -------------------------------------------------------------------------------- 1 | type Query @extends { 2 | topProducts(first: Int): [Product] 3 | } 4 | 5 | type Product @key(fields: "upc") { 6 | upc: String! 7 | name: String 8 | price: Int 9 | weight: Int 10 | } 11 | 12 | type Subscription { 13 | newProduct: Product 14 | } 15 | -------------------------------------------------------------------------------- /examples/federation-supergraph/src/services/products.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { createServer } from 'http'; 3 | import { join } from 'path'; 4 | import { parse } from 'graphql'; 5 | import { createYoga } from 'graphql-yoga'; 6 | import { buildSubgraphSchema } from '@apollo/subgraph'; 7 | 8 | const products = [ 9 | { 10 | upc: '1', 11 | name: 'Table', 12 | price: 899, 13 | weight: 100, 14 | }, 15 | { 16 | upc: '2', 17 | name: 'Couch', 18 | price: 1299, 19 | weight: 1000, 20 | }, 21 | { 22 | upc: '3', 23 | name: 'Chair', 24 | price: 54, 25 | weight: 50, 26 | }, 27 | ]; 28 | 29 | export const server = createServer( 30 | createYoga({ 31 | schema: buildSubgraphSchema([ 32 | { 33 | typeDefs: parse(readFileSync(join(__dirname, 'products.graphql'), 'utf8')), 34 | resolvers: { 35 | Product: { 36 | __resolveReference(object) { 37 | return products.find(product => product.upc === object.upc); 38 | }, 39 | }, 40 | Query: { 41 | topProducts(_, args) { 42 | return args.first ? products.slice(0, args.first) : products; 43 | }, 44 | }, 45 | Subscription: { 46 | newProduct: { 47 | async *subscribe() { 48 | for (const product of products) { 49 | await new Promise(resolve => setTimeout(resolve, 1000)); 50 | yield { newProduct: product }; 51 | } 52 | }, 53 | }, 54 | }, 55 | }, 56 | }, 57 | ]), 58 | }), 59 | ); 60 | 61 | server.listen(4003, () => { 62 | console.log('products service running at http://localhost:4003/graphql'); 63 | }); 64 | -------------------------------------------------------------------------------- /examples/federation-supergraph/src/services/reviews.graphql: -------------------------------------------------------------------------------- 1 | type Review @key(fields: "id") { 2 | id: ID! 3 | body: String 4 | author: User @provides(fields: "username") 5 | product: Product 6 | } 7 | 8 | type User @key(fields: "id") @extends { 9 | id: ID! @external 10 | username: String @external 11 | numberOfReviews: Int 12 | reviews: [Review] 13 | } 14 | 15 | type Product @key(fields: "upc") @extends { 16 | upc: String! @external 17 | reviews: [Review] 18 | } 19 | -------------------------------------------------------------------------------- /examples/federation-supergraph/src/services/reviews.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'fs'; 2 | import { createServer } from 'http'; 3 | import { join } from 'path'; 4 | import { parse } from 'graphql'; 5 | import { createYoga } from 'graphql-yoga'; 6 | import { buildSubgraphSchema } from '@apollo/subgraph'; 7 | 8 | const usernames = [ 9 | { id: '1', username: '@ada' }, 10 | { id: '2', username: '@complete' }, 11 | ]; 12 | const reviews = [ 13 | { 14 | id: '1', 15 | authorID: '1', 16 | product: { upc: '1' }, 17 | body: 'Love it!', 18 | }, 19 | { 20 | id: '2', 21 | authorID: '1', 22 | product: { upc: '2' }, 23 | body: 'Too expensive.', 24 | }, 25 | { 26 | id: '3', 27 | authorID: '2', 28 | product: { upc: '3' }, 29 | body: 'Could be better.', 30 | }, 31 | { 32 | id: '4', 33 | authorID: '2', 34 | product: { upc: '1' }, 35 | body: 'Prefer something else.', 36 | }, 37 | ]; 38 | 39 | export const server = createServer( 40 | createYoga({ 41 | schema: buildSubgraphSchema([ 42 | { 43 | typeDefs: parse(readFileSync(join(__dirname, 'reviews.graphql'), 'utf8')), 44 | resolvers: { 45 | Review: { 46 | __resolveReference(object) { 47 | return reviews.find(review => review.id === object.id); 48 | }, 49 | author(review) { 50 | return { __typename: 'User', id: review.authorID }; 51 | }, 52 | }, 53 | User: { 54 | reviews(user) { 55 | return reviews.filter(review => review.authorID === user.id); 56 | }, 57 | numberOfReviews(user) { 58 | return reviews.filter(review => review.authorID === user.id).length; 59 | }, 60 | username(user) { 61 | const found = usernames.find(username => username.id === user.id); 62 | return found ? found.username : null; 63 | }, 64 | }, 65 | Product: { 66 | reviews(product) { 67 | return reviews.filter(review => review.product.upc === product.upc); 68 | }, 69 | }, 70 | }, 71 | }, 72 | ]), 73 | }), 74 | ); 75 | 76 | server.listen(4004, () => { 77 | console.log('reviews service running at http://localhost:4004/graphql'); 78 | }); 79 | -------------------------------------------------------------------------------- /examples/federation-supergraph/src/supergraph.yaml: -------------------------------------------------------------------------------- 1 | subgraphs: 2 | accounts: 3 | routing_url: http://localhost:4001/graphql 4 | schema: 5 | file: ./services/accounts.graphql 6 | inventory: 7 | routing_url: http://localhost:4002/graphql 8 | schema: 9 | file: ./services/inventory.graphql 10 | products: 11 | routing_url: http://localhost:4003/graphql 12 | schema: 13 | file: ./services/products.graphql 14 | reviews: 15 | routing_url: http://localhost:4004/graphql 16 | schema: 17 | file: ./services/reviews.graphql 18 | -------------------------------------------------------------------------------- /examples/federation-supergraph/tests/federation-supergraph.spec.ts: -------------------------------------------------------------------------------- 1 | import { exampleQuery } from '../src/example-query'; 2 | import { exampleSubscription } from '../src/example-subscription'; 3 | import { yoga } from '../src/gateway'; 4 | import { server as accountsServer } from '../src/services/accounts'; 5 | import { server as inventoryServer } from '../src/services/inventory'; 6 | import { server as productsServer } from '../src/services/products'; 7 | import { server as reviewsServer } from '../src/services/reviews'; 8 | 9 | describe('federation-supergraph', () => { 10 | afterAll(() => { 11 | accountsServer.close(); 12 | inventoryServer.close(); 13 | productsServer.close(); 14 | reviewsServer.close(); 15 | }); 16 | 17 | it('should pass', async () => { 18 | const res = await yoga.fetch('http://localhost:4000/graphql', { 19 | method: 'POST', 20 | headers: { 21 | 'Content-Type': 'application/json', 22 | }, 23 | body: JSON.stringify({ 24 | query: exampleQuery, 25 | }), 26 | }); 27 | const result = await res.json(); 28 | expect(result).toMatchSnapshot(); 29 | }); 30 | 31 | it('subscriptions', async () => { 32 | const res = await yoga.fetch('http://localhost:4000/graphql', { 33 | method: 'POST', 34 | headers: { 35 | 'Content-Type': 'application/json', 36 | Accept: 'text/event-stream', 37 | }, 38 | body: JSON.stringify({ 39 | query: exampleSubscription, 40 | }), 41 | }); 42 | const reader = res.body.getReader(); 43 | while (true) { 44 | const { done, value } = await reader.read(); 45 | if (done) { 46 | break; 47 | } 48 | const str = Buffer.from(value).toString('utf-8'); 49 | const [, actualJson] = str.split('data: '); 50 | if (actualJson) { 51 | const res = JSON.parse(actualJson); 52 | expect(res.errors).toBeUndefined(); 53 | expect(res.data).toMatchSnapshot(); 54 | } 55 | } 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /examples/federation-supergraph/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "lib": ["esnext", "DOM"], 10 | "skipLibCheck": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/federation-to-stitching-sdl/README.md: -------------------------------------------------------------------------------- 1 | # Federation to Stitching SDL 2 | 3 | [See the details about this example on the website](https://the-guild.dev/graphql/stitching/handbook/other-integrations/federation-to-stitching-sdl) 4 | -------------------------------------------------------------------------------- /examples/federation-to-stitching-sdl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "federation-to-stitching-sdl", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "concurrently \"yarn:start-*\"", 8 | "start-gateway": "ts-node-dev src/index.ts", 9 | "start-products": "ts-node-dev src/services/products/index.ts", 10 | "start-reviews": "ts-node-dev src/services/reviews/index.ts", 11 | "start-users": "ts-node-dev src/services/users/index.ts" 12 | }, 13 | "dependencies": { 14 | "@apollo/subgraph": "^2.2.3", 15 | "@graphql-tools/executor-http": "^2.0.0", 16 | "@graphql-tools/stitch": "^9.0.0", 17 | "@graphql-tools/stitching-directives": "^3.0.0", 18 | "@graphql-tools/utils": "^10.0.0", 19 | "@types/node": "^22.0.0", 20 | "@types/wait-on": "^5.3.1", 21 | "concurrently": "^9.0.0", 22 | "graphql": "^16.6.0", 23 | "graphql-yoga": "^5.0.0", 24 | "ts-node": "^10.9.1", 25 | "ts-node-dev": "^2.0.0", 26 | "typescript": "^5.0.0", 27 | "wait-on": "^8.0.0" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/federation-to-stitching-sdl/src/gateway.ts: -------------------------------------------------------------------------------- 1 | import { buildSchema, parse } from 'graphql'; 2 | import { createYoga, isAsyncIterable } from 'graphql-yoga'; 3 | import waitOn from 'wait-on'; 4 | import { buildHTTPExecutor } from '@graphql-tools/executor-http'; 5 | import { stitchSchemas } from '@graphql-tools/stitch'; 6 | import { federationToStitchingSDL, stitchingDirectives } from '@graphql-tools/stitching-directives'; 7 | import { Executor } from '@graphql-tools/utils'; 8 | 9 | const SDL_QUERY = parse(/* GraphQL */ ` 10 | query GetSDL { 11 | _service { 12 | sdl 13 | } 14 | } 15 | `); 16 | 17 | async function fetchFederationSubschema(executor: Executor) { 18 | const result = await executor({ document: SDL_QUERY }); 19 | if (isAsyncIterable(result)) { 20 | throw new Error('Executor returned an AsyncIterable, which is not supported'); 21 | } 22 | const sdl = federationToStitchingSDL(result.data._service.sdl); 23 | return { 24 | schema: buildSchema(sdl, { 25 | assumeValidSDL: true, 26 | assumeValid: true, 27 | }), 28 | executor, 29 | }; 30 | } 31 | 32 | async function makeGatewaySchema() { 33 | await waitOn({ resources: [4001, 4002, 4003].map(p => `tcp:${p}`) }); 34 | const { stitchingDirectivesTransformer } = stitchingDirectives(); 35 | return stitchSchemas({ 36 | subschemaConfigTransforms: [stitchingDirectivesTransformer], 37 | subschemas: await Promise.all([ 38 | fetchFederationSubschema(buildHTTPExecutor({ endpoint: 'http://localhost:4001/graphql' })), 39 | fetchFederationSubschema(buildHTTPExecutor({ endpoint: 'http://localhost:4002/graphql' })), 40 | fetchFederationSubschema(buildHTTPExecutor({ endpoint: 'http://localhost:4003/graphql' })), 41 | ]), 42 | }); 43 | } 44 | 45 | export const gatewayApp = createYoga({ 46 | schema: makeGatewaySchema(), 47 | maskedErrors: false, 48 | graphiql: { 49 | title: 'Federation services', 50 | defaultQuery: /* GraphQL */ ` 51 | query { 52 | user(id: "1") { 53 | username 54 | recentPurchases { 55 | upc 56 | name 57 | } 58 | reviews { 59 | body 60 | author { 61 | id 62 | username 63 | } 64 | product { 65 | upc 66 | name 67 | acceptsNewReviews 68 | } 69 | } 70 | } 71 | } 72 | `, 73 | }, 74 | }); 75 | -------------------------------------------------------------------------------- /examples/federation-to-stitching-sdl/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { gatewayApp } from './gateway'; 3 | 4 | const server = createServer(gatewayApp); 5 | server.listen(4000, () => console.log('gateway running at http://localhost:4000/graphql')); 6 | -------------------------------------------------------------------------------- /examples/federation-to-stitching-sdl/src/services/products/index.ts: -------------------------------------------------------------------------------- 1 | import { productsServer } from './server'; 2 | 3 | productsServer.listen(4001, () => { 4 | console.log(`Products service running at http://localhost:4001`); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/federation-to-stitching-sdl/src/services/products/server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { GraphQLError, parse } from 'graphql'; 3 | import { createYoga } from 'graphql-yoga'; 4 | import { buildSubgraphSchema } from '@apollo/subgraph'; 5 | 6 | const products = [ 7 | { upc: '1', name: 'iPhone', price: 699.99, unitsInStock: 7 }, 8 | { upc: '2', name: 'Super Baking Cookbook', price: 15.99, unitsInStock: 0 }, 9 | { upc: '3', name: 'Best Selling Novel', price: 7.99, unitsInStock: 25 }, 10 | { upc: '4', name: 'iOS Survival Guide', price: 24.99, unitsInStock: 150 }, 11 | ]; 12 | 13 | const productPurchases = [ 14 | { productUpc: '1', userId: '1' }, 15 | { productUpc: '4', userId: '1' }, 16 | { productUpc: '1', userId: '2' }, 17 | { productUpc: '3', userId: '2' }, 18 | { productUpc: '2', userId: '3' }, 19 | ]; 20 | 21 | export const productsServer = createServer( 22 | createYoga({ 23 | schema: buildSubgraphSchema({ 24 | typeDefs: parse(/* GraphQL */ ` 25 | type Product @key(fields: "upc") { 26 | upc: ID! 27 | name: String! 28 | price: Float! 29 | unitsInStock: Int! 30 | } 31 | 32 | extend type User @key(fields: "id") { 33 | id: ID! @external 34 | recentPurchases: [Product] 35 | } 36 | 37 | type Query { 38 | product(upc: ID!): Product 39 | } 40 | `), 41 | resolvers: { 42 | Product: { 43 | __resolveReference: ({ upc }) => products.find(product => product.upc === upc), 44 | }, 45 | User: { 46 | recentPurchases(user) { 47 | const upcs = productPurchases 48 | .filter(({ userId }) => userId === user.id) 49 | .map(({ productUpc }) => productUpc); 50 | return upcs.map( 51 | upc => 52 | products.find(p => p.upc === upc) || 53 | new GraphQLError('Record not found', { 54 | extensions: { 55 | code: 'NOT_FOUND', 56 | }, 57 | }), 58 | ); 59 | }, 60 | }, 61 | Query: { 62 | product: (_root, { upc }) => 63 | products.find(p => p.upc === upc) || 64 | new GraphQLError('Record not found', { 65 | extensions: { 66 | code: 'NOT_FOUND', 67 | }, 68 | }), 69 | }, 70 | }, 71 | }), 72 | }), 73 | ); 74 | -------------------------------------------------------------------------------- /examples/federation-to-stitching-sdl/src/services/reviews/index.ts: -------------------------------------------------------------------------------- 1 | import { reviewsServer } from './server'; 2 | 3 | reviewsServer.listen(4002, () => { 4 | console.log(`Reviews service running at http://localhost:4002`); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/federation-to-stitching-sdl/src/services/reviews/server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { parse } from 'graphql'; 3 | import { createYoga } from 'graphql-yoga'; 4 | import { buildSubgraphSchema } from '@apollo/subgraph'; 5 | 6 | const reviews = [ 7 | { id: '1', userId: '1', productUpc: '1', body: 'Love it!' }, 8 | { id: '2', userId: '1', productUpc: '2', body: 'Too expensive.' }, 9 | { id: '3', userId: '2', productUpc: '3', body: 'Could be better.' }, 10 | { id: '4', userId: '3', productUpc: '1', body: 'Prefer something else.' }, 11 | ]; 12 | 13 | export const reviewsServer = createServer( 14 | createYoga({ 15 | schema: buildSubgraphSchema({ 16 | typeDefs: parse(/* GraphQL */ ` 17 | type Review @key(fields: "id") { 18 | id: ID! 19 | body: String! 20 | author: User 21 | product: Product 22 | } 23 | 24 | extend type User @key(fields: "id") { 25 | id: ID! @external 26 | reviews: [Review] 27 | } 28 | 29 | extend type Product @key(fields: "upc") { 30 | acceptsNewReviews: Boolean @requires(fields: "unitsInStock") 31 | reviews: [Review] 32 | unitsInStock: Int @external 33 | upc: ID! @external 34 | } 35 | `), 36 | resolvers: { 37 | Review: { 38 | __resolveReference: ({ id }) => reviews.find(review => review.id === id), 39 | author: review => ({ id: review.userId }), 40 | product: review => ({ upc: review.productUpc }), 41 | }, 42 | Product: { 43 | reviews: product => reviews.filter(review => review.productUpc === product.upc), 44 | acceptsNewReviews: product => product.unitsInStock > 0, 45 | }, 46 | User: { 47 | reviews: user => reviews.filter(review => review.userId === user.id), 48 | }, 49 | }, 50 | }), 51 | }), 52 | ); 53 | -------------------------------------------------------------------------------- /examples/federation-to-stitching-sdl/src/services/users/index.ts: -------------------------------------------------------------------------------- 1 | import { usersServer } from './server'; 2 | 3 | usersServer.listen(4003, () => { 4 | console.log(`Users service running at http://localhost:4003`); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/federation-to-stitching-sdl/src/services/users/server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { GraphQLError, parse } from 'graphql'; 3 | import { createYoga } from 'graphql-yoga'; 4 | import { buildSubgraphSchema } from '@apollo/subgraph'; 5 | 6 | const users = [ 7 | { id: '1', username: 'hanshotfirst', email: 'han@solo.me' }, 8 | { id: '2', username: 'bigvader23', email: 'vader@darkside.io' }, 9 | { id: '3', username: 'yodamecrazy', email: 'yoda@theforce.net' }, 10 | ]; 11 | 12 | export const usersServer = createServer( 13 | createYoga({ 14 | schema: buildSubgraphSchema({ 15 | typeDefs: parse(/* GraphQL */ ` 16 | type User @key(fields: "id") { 17 | id: ID! 18 | email: String! 19 | username: String! 20 | } 21 | 22 | type Query { 23 | user(id: ID!): User 24 | } 25 | `), 26 | resolvers: { 27 | User: { 28 | __resolveReference: ({ id }) => users.find(user => user.id === id), 29 | }, 30 | Query: { 31 | user: (_root, { id }) => 32 | users.find(user => user.id === id) || 33 | new GraphQLError('Record not found', { 34 | extensions: { 35 | code: 'NOT_FOUND', 36 | }, 37 | }), 38 | }, 39 | }, 40 | }), 41 | }), 42 | ); 43 | -------------------------------------------------------------------------------- /examples/federation-to-stitching-sdl/tests/__snapshots__/federation-services.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Federation services should work: users 1`] = ` 4 | { 5 | "data": { 6 | "user": { 7 | "recentPurchases": [ 8 | { 9 | "name": "iPhone", 10 | "upc": "1", 11 | }, 12 | { 13 | "name": "iOS Survival Guide", 14 | "upc": "4", 15 | }, 16 | ], 17 | "reviews": [ 18 | { 19 | "author": { 20 | "id": "1", 21 | "username": "hanshotfirst", 22 | }, 23 | "body": "Love it!", 24 | "product": { 25 | "acceptsNewReviews": true, 26 | "name": "iPhone", 27 | "upc": "1", 28 | }, 29 | }, 30 | { 31 | "author": { 32 | "id": "1", 33 | "username": "hanshotfirst", 34 | }, 35 | "body": "Too expensive.", 36 | "product": { 37 | "acceptsNewReviews": false, 38 | "name": "Super Baking Cookbook", 39 | "upc": "2", 40 | }, 41 | }, 42 | ], 43 | "username": "hanshotfirst", 44 | }, 45 | }, 46 | } 47 | `; 48 | -------------------------------------------------------------------------------- /examples/federation-to-stitching-sdl/tests/federation-services.test.ts: -------------------------------------------------------------------------------- 1 | import { gatewayApp } from '../src/gateway'; 2 | import { productsServer } from '../src/services/products/server'; 3 | import { reviewsServer } from '../src/services/reviews/server'; 4 | import { usersServer } from '../src/services/users/server'; 5 | 6 | describe('Federation services', () => { 7 | beforeAll(async () => { 8 | await Promise.all([ 9 | new Promise(resolve => productsServer.listen(4001, resolve)), 10 | new Promise(resolve => reviewsServer.listen(4002, resolve)), 11 | new Promise(resolve => usersServer.listen(4003, resolve)), 12 | ]); 13 | }); 14 | afterAll(async () => { 15 | await Promise.all([ 16 | new Promise(resolve => productsServer.close(resolve)), 17 | new Promise(resolve => reviewsServer.close(resolve)), 18 | new Promise(resolve => usersServer.close(resolve)), 19 | ]); 20 | }); 21 | it('should work', async () => { 22 | const response = await gatewayApp.fetch('/graphql', { 23 | method: 'POST', 24 | headers: { 25 | 'Content-Type': 'application/json', 26 | }, 27 | body: JSON.stringify({ 28 | query: /* GraphQL */ ` 29 | query { 30 | user(id: "1") { 31 | username 32 | recentPurchases { 33 | upc 34 | name 35 | } 36 | reviews { 37 | body 38 | author { 39 | id 40 | username 41 | } 42 | product { 43 | upc 44 | name 45 | acceptsNewReviews 46 | } 47 | } 48 | } 49 | } 50 | `, 51 | }), 52 | }); 53 | const result = await response.json(); 54 | expect(result).toMatchSnapshot('users'); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /examples/federation-to-stitching-sdl/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "lib": ["esnext", "DOM"], 10 | "skipLibCheck": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/graphql-upload/README.md: -------------------------------------------------------------------------------- 1 | # GraphQL Upload 2 | 3 | [See the details about this example on the website](https://the-guild.dev/graphql/stitching/handbook/other-integrations/graphql-upload) 4 | -------------------------------------------------------------------------------- /examples/graphql-upload/file.txt: -------------------------------------------------------------------------------- 1 | test file but not image 2 | -------------------------------------------------------------------------------- /examples/graphql-upload/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graphql-upload", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "concurrently \"yarn:start-*\"", 8 | "start-gateway": "ts-node-dev src/index.ts", 9 | "start-resize-images": "ts-node-dev src/services/resize-images/index.ts", 10 | "start-upload-files": "ts-node-dev src/services/upload-files/index.ts" 11 | }, 12 | "dependencies": { 13 | "@graphql-tools/delegate": "^10.0.0", 14 | "@graphql-tools/executor-http": "^2.0.0", 15 | "@graphql-tools/stitch": "^9.0.0", 16 | "@graphql-tools/wrap": "^10.0.0", 17 | "@types/mime-types": "^2.1.1", 18 | "@types/node": "^22.0.0", 19 | "@types/sharp": "^0.32.0", 20 | "@types/wait-on": "^5.3.1", 21 | "concurrently": "^9.0.0", 22 | "graphql": "^16.6.0", 23 | "graphql-yoga": "^5.0.0", 24 | "mime-types": "^3.0.0", 25 | "sharp": "^0.34.1", 26 | "ts-node": "^10.9.1", 27 | "ts-node-dev": "^2.0.0", 28 | "typescript": "^5.0.0", 29 | "wait-on": "^8.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/graphql-upload/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { gatewayApp } from './gateway'; 3 | 4 | const server = createServer(gatewayApp); 5 | server.listen(4000, () => console.log('gateway running at http://localhost:4000/graphql')); 6 | -------------------------------------------------------------------------------- /examples/graphql-upload/src/services/resize-images/index.ts: -------------------------------------------------------------------------------- 1 | import { resizeImagesServer } from './server'; 2 | 3 | resizeImagesServer.listen(4002, () => 4 | console.log('resize images service running at http://localhost:4002/graphql'), 5 | ); 6 | -------------------------------------------------------------------------------- /examples/graphql-upload/src/services/resize-images/server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { createSchema, createYoga } from 'graphql-yoga'; 3 | import sharp from 'sharp'; 4 | 5 | export const resizeImagesServer = createServer( 6 | createYoga({ 7 | schema: createSchema({ 8 | typeDefs: /* GraphQL */ ` 9 | scalar File 10 | type Query { 11 | resizeImage(file: File!, width: Int!, height: Int!): String 12 | } 13 | `, 14 | resolvers: { 15 | Query: { 16 | async resizeImage( 17 | _root, 18 | { file, width, height }: { file: File; width: number; height: number }, 19 | ) { 20 | const inputBuffer = Buffer.from(await file.arrayBuffer()); 21 | const buffer = await sharp(inputBuffer).resize(width, height).toBuffer(); 22 | const base64 = buffer.toString('base64'); 23 | return base64; 24 | }, 25 | }, 26 | }, 27 | }), 28 | }), 29 | ); 30 | -------------------------------------------------------------------------------- /examples/graphql-upload/src/services/upload-files/files/file.txt: -------------------------------------------------------------------------------- 1 | test file but not image -------------------------------------------------------------------------------- /examples/graphql-upload/src/services/upload-files/files/yoga.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ardatan/schema-stitching/4456bcbbacfb131e28152f143ae4381038d15c9a/examples/graphql-upload/src/services/upload-files/files/yoga.png -------------------------------------------------------------------------------- /examples/graphql-upload/src/services/upload-files/index.ts: -------------------------------------------------------------------------------- 1 | import { uploadFilesServer } from './server'; 2 | 3 | uploadFilesServer.listen(4001, () => 4 | console.log('upload files service running at http://localhost:4001/graphql'), 5 | ); 6 | -------------------------------------------------------------------------------- /examples/graphql-upload/src/services/upload-files/server.ts: -------------------------------------------------------------------------------- 1 | import { promises as fsPromises } from 'fs'; 2 | import { createServer } from 'http'; 3 | import { join } from 'path'; 4 | import { GraphQLSchema } from 'graphql'; 5 | import { createSchema, createYoga } from 'graphql-yoga'; 6 | import { lookup as mimeLookup } from 'mime-types'; 7 | 8 | const FILES_DIR = join(__dirname, 'files'); 9 | 10 | const yoga = createYoga({ 11 | schema: createSchema({ 12 | typeDefs: /* GraphQL */ ` 13 | scalar File 14 | 15 | type FileEntry { 16 | name: String 17 | type: String 18 | text: String 19 | base64: String 20 | } 21 | 22 | type Query { 23 | readFile(name: String!): FileEntry! 24 | } 25 | 26 | type Mutation { 27 | uploadFile(file: File!): FileEntry! 28 | } 29 | `, 30 | resolvers: { 31 | Query: { 32 | async readFile(_root, { name }) { 33 | const buffer = await fsPromises.readFile(join(FILES_DIR, name)); 34 | const type = mimeLookup(name) || 'application/octet-stream'; 35 | return new yoga.fetchAPI.File([buffer], name, { type }); 36 | }, 37 | }, 38 | FileEntry: { 39 | // We don't need to implement resolvers for `name`, `type` and `text` since they are available on the root `File` object 40 | async base64(file: File) { 41 | const buffer = Buffer.from(await file.arrayBuffer()); 42 | return buffer.toString('base64'); 43 | }, 44 | }, 45 | Mutation: { 46 | async uploadFile(_root, { file }: { file: File }) { 47 | const buffer = Buffer.from(await file.arrayBuffer()); 48 | await fsPromises.writeFile(join(FILES_DIR, file.name), buffer); 49 | return new yoga.fetchAPI.File([buffer], file.name, { type: file.type }); 50 | }, 51 | }, 52 | }, 53 | }) as GraphQLSchema, 54 | }); 55 | 56 | export const uploadFilesServer = createServer(yoga); 57 | -------------------------------------------------------------------------------- /examples/graphql-upload/tests/__snapshots__/graphql-upload.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`GraphQL Upload should forward file uploads correctly: uploadFile 1`] = ` 4 | { 5 | "data": { 6 | "uploadFile": { 7 | "name": "file.txt", 8 | "text": "test file but not image", 9 | "type": "text/plain", 10 | }, 11 | }, 12 | } 13 | `; 14 | -------------------------------------------------------------------------------- /examples/graphql-upload/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "lib": ["esnext", "DOM"], 10 | "skipLibCheck": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/hot-schema-reloading/README.md: -------------------------------------------------------------------------------- 1 | # Hot schema reloading 2 | 3 | [See the details about this example on the website](https://the-guild.dev/graphql/stitching/handbook/architecture/hot-schema-reloading) 4 | -------------------------------------------------------------------------------- /examples/hot-schema-reloading/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hot-schema-reloading", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "concurrently \"yarn:start-*\"", 8 | "start-gateway": "ts-node-dev src/index.ts", 9 | "start-inventory": "ts-node-dev src/services/inventory/index.ts", 10 | "start-products": "ts-node-dev src/services/products/index.ts" 11 | }, 12 | "dependencies": { 13 | "@graphql-tools/delegate": "^10.0.0", 14 | "@graphql-tools/executor-http": "^2.0.0", 15 | "@graphql-tools/stitch": "^9.0.0", 16 | "@graphql-tools/stitching-directives": "^3.0.0", 17 | "@graphql-tools/utils": "^10.0.0", 18 | "@types/node": "^22.0.0", 19 | "@types/wait-on": "^5.3.1", 20 | "concurrently": "^9.0.0", 21 | "graphql": "^16.6.0", 22 | "graphql-yoga": "^5.0.0", 23 | "ts-node": "^10.9.1", 24 | "ts-node-dev": "^2.0.0", 25 | "typescript": "^5.0.0", 26 | "wait-on": "^8.0.0" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /examples/hot-schema-reloading/src/SchemaLoader.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema, parse } from 'graphql'; 2 | import { buildHTTPExecutor } from '@graphql-tools/executor-http'; 3 | import { isAsyncIterable } from '@graphql-tools/utils'; 4 | 5 | interface LoadedEndpoint { 6 | url: string; 7 | sdl: string; 8 | } 9 | 10 | export class SchemaLoader { 11 | public schema: GraphQLSchema | null = null; 12 | public loadedEndpoints: LoadedEndpoint[] = []; 13 | private intervalId: NodeJS.Timeout | null = null; 14 | 15 | constructor( 16 | private buildSchema: (endpoints: LoadedEndpoint[]) => GraphQLSchema, 17 | public endpoints: string[], 18 | ) {} 19 | 20 | async reload() { 21 | const loadedEndpoints: LoadedEndpoint[] = []; 22 | await Promise.all( 23 | this.endpoints.map(async url => { 24 | try { 25 | const fetcher = buildHTTPExecutor({ 26 | endpoint: url, 27 | timeout: 300, 28 | }); 29 | const result = await fetcher({ 30 | document: parse(/* GraphQL */ ` 31 | { 32 | _sdl 33 | } 34 | `), 35 | }); 36 | if (isAsyncIterable(result)) { 37 | throw new Error('Expected executor to return a single result'); 38 | } 39 | loadedEndpoints.push({ 40 | url, 41 | sdl: result.data._sdl, 42 | }); 43 | } catch (err) { 44 | // drop the schema, or return the last cached version, etc... 45 | } 46 | }), 47 | ); 48 | 49 | this.loadedEndpoints = loadedEndpoints; 50 | this.schema = this.buildSchema(this.loadedEndpoints); 51 | console.log( 52 | `gateway reload ${new Date().toLocaleString()}, endpoints: ${this.loadedEndpoints.length}`, 53 | ); 54 | return this.schema; 55 | } 56 | 57 | autoRefresh(interval = 3000) { 58 | this.stopAutoRefresh(); 59 | this.intervalId = setTimeout(async () => { 60 | await this.reload(); 61 | this.intervalId = null; 62 | this.autoRefresh(interval); 63 | }, interval); 64 | } 65 | 66 | stopAutoRefresh() { 67 | if (this.intervalId != null) { 68 | clearInterval(this.intervalId); 69 | this.intervalId = null; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /examples/hot-schema-reloading/src/gateway.ts: -------------------------------------------------------------------------------- 1 | import { createServer, Server } from 'http'; 2 | import { buildSchema, GraphQLSchema } from 'graphql'; 3 | import { createYoga } from 'graphql-yoga'; 4 | import waitOn from 'wait-on'; 5 | import { SubschemaConfig } from '@graphql-tools/delegate'; 6 | import { buildHTTPExecutor } from '@graphql-tools/executor-http'; 7 | import { stitchSchemas } from '@graphql-tools/stitch'; 8 | import { stitchingDirectives } from '@graphql-tools/stitching-directives'; 9 | import { SchemaLoader } from './SchemaLoader'; 10 | import { makeEndpointsSchema } from './services/endpoints'; 11 | 12 | const { stitchingDirectivesTransformer } = stitchingDirectives(); 13 | 14 | const loader = new SchemaLoader( 15 | function buildSchemaFromEndpoints(loadedEndpoints) { 16 | const subschemas: SubschemaConfig[] = loadedEndpoints.map(({ sdl, url }) => ({ 17 | schema: buildSchema(sdl), 18 | executor: buildHTTPExecutor({ 19 | endpoint: url, 20 | timeout: 5000, 21 | }), 22 | batch: true, 23 | })); 24 | 25 | subschemas.push(makeEndpointsSchema(loader)); 26 | 27 | return stitchSchemas({ 28 | subschemaConfigTransforms: [stitchingDirectivesTransformer], 29 | subschemas, 30 | }); 31 | }, 32 | ['http://localhost:4001/graphql', 'http://localhost:4002/graphql'], 33 | ); 34 | 35 | const server = createServer( 36 | createYoga({ 37 | schema: () => loader.schema, 38 | maskedErrors: false, 39 | graphiql: { 40 | title: 'Hot schema reloading', 41 | }, 42 | }), 43 | ); 44 | export async function startServer() { 45 | await waitOn({ resources: [4001, 4002].map(p => `tcp:${p}`) }); 46 | await loader.reload(); 47 | await new Promise(resolve => server.listen(4000, resolve)); 48 | console.log('Gateway started on http://localhost:4000'); 49 | } 50 | 51 | export async function stopServer() { 52 | loader.stopAutoRefresh(); 53 | await new Promise(resolve => server.close(resolve)); 54 | } 55 | -------------------------------------------------------------------------------- /examples/hot-schema-reloading/src/index.ts: -------------------------------------------------------------------------------- 1 | import { startServer } from './gateway'; 2 | 3 | startServer().catch(e => { 4 | console.error(e); 5 | process.exit(1); 6 | }); 7 | -------------------------------------------------------------------------------- /examples/hot-schema-reloading/src/services/endpoints.ts: -------------------------------------------------------------------------------- 1 | import { createSchema } from 'graphql-yoga'; 2 | import { SchemaLoader } from '../SchemaLoader'; 3 | 4 | const typeDefs = /* GraphQL */ ` 5 | type Endpoint { 6 | url: String! 7 | sdl: String 8 | } 9 | 10 | type Query { 11 | endpoints: [Endpoint!]! 12 | } 13 | 14 | type AddEndpointResult { 15 | endpoint: Endpoint 16 | success: Boolean! 17 | } 18 | 19 | type RemoveEndpointResult { 20 | success: Boolean! 21 | } 22 | 23 | type ReloadAllEndpointsResult { 24 | success: Boolean! 25 | } 26 | 27 | type Mutation { 28 | addEndpoint(url: String!): AddEndpointResult! 29 | removeEndpoint(url: String!): RemoveEndpointResult! 30 | reloadAllEndpoints: ReloadAllEndpointsResult! 31 | } 32 | `; 33 | 34 | export function makeEndpointsSchema(loader: SchemaLoader) { 35 | return { 36 | schema: createSchema({ 37 | typeDefs, 38 | resolvers: { 39 | Query: { 40 | endpoints: () => loader.loadedEndpoints, 41 | }, 42 | Mutation: { 43 | async addEndpoint(_root, { url }) { 44 | let success = false; 45 | if (!loader.endpoints.includes(url)) { 46 | loader.endpoints.push(url); 47 | await loader.reload(); 48 | success = true; 49 | } 50 | return { 51 | endpoint: loader.loadedEndpoints.find(s => s.url === url), 52 | success, 53 | }; 54 | }, 55 | async removeEndpoint(_root, { url }) { 56 | let success = false; 57 | const index = loader.endpoints.indexOf(url); 58 | if (index > -1) { 59 | loader.endpoints.splice(index, 1); 60 | await loader.reload(); 61 | success = true; 62 | } 63 | return { success }; 64 | }, 65 | async reloadAllEndpoints() { 66 | await loader.reload(); 67 | return { success: true }; 68 | }, 69 | }, 70 | }, 71 | }), 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /examples/hot-schema-reloading/src/services/inventory/index.ts: -------------------------------------------------------------------------------- 1 | import { inventoryServer } from './server'; 2 | 3 | inventoryServer.listen(4002, () => 4 | console.log('Inventory service running on http://localhost:4002'), 5 | ); 6 | -------------------------------------------------------------------------------- /examples/hot-schema-reloading/src/services/inventory/server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { GraphQLError } from 'graphql'; 3 | import { createSchema, createYoga } from 'graphql-yoga'; 4 | import { stitchingDirectives } from '@graphql-tools/stitching-directives'; 5 | 6 | const { stitchingDirectivesValidator, stitchingDirectivesTypeDefs } = stitchingDirectives(); 7 | 8 | const typeDefs = /* GraphQL */ ` 9 | ${stitchingDirectivesTypeDefs} 10 | type Product { 11 | upc: ID! 12 | inStock: Boolean 13 | } 14 | 15 | type Query { 16 | mostStockedProduct: Product 17 | _products(upcs: [ID!]!): [Product]! @merge(keyField: "upc") 18 | _sdl: String! 19 | } 20 | `; 21 | 22 | const inventory = [ 23 | { upc: '1', unitsInStock: 3 }, 24 | { upc: '2', unitsInStock: 0 }, 25 | { upc: '3', unitsInStock: 5 }, 26 | ]; 27 | 28 | export const inventoryServer = createServer( 29 | createYoga({ 30 | schema: stitchingDirectivesValidator( 31 | createSchema({ 32 | typeDefs, 33 | resolvers: { 34 | Product: { 35 | inStock: product => product.unitsInStock > 0, 36 | }, 37 | Query: { 38 | mostStockedProduct: () => 39 | inventory.reduce( 40 | (acc, i) => (acc.unitsInStock >= i.unitsInStock ? acc : i), 41 | inventory[0], 42 | ), 43 | _products: (_root, { upcs }) => 44 | upcs.map( 45 | (upc: string) => 46 | inventory.find(i => i.upc === upc) || 47 | new GraphQLError('Record not found', { 48 | extensions: { 49 | code: 'NOT_FOUND', 50 | }, 51 | }), 52 | ), 53 | _sdl: () => typeDefs, 54 | }, 55 | }, 56 | }), 57 | ), 58 | }), 59 | ); 60 | -------------------------------------------------------------------------------- /examples/hot-schema-reloading/src/services/products/index.ts: -------------------------------------------------------------------------------- 1 | import { productsServer } from './server'; 2 | 3 | productsServer.listen(4001, () => console.log('Products server running on http://localhost:4001')); 4 | -------------------------------------------------------------------------------- /examples/hot-schema-reloading/src/services/products/server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { GraphQLError } from 'graphql'; 3 | import { createSchema, createYoga } from 'graphql-yoga'; 4 | import { stitchingDirectives } from '@graphql-tools/stitching-directives'; 5 | 6 | const { stitchingDirectivesTypeDefs, stitchingDirectivesValidator } = stitchingDirectives(); 7 | 8 | const typeDefs = /* GraphQL */ ` 9 | ${stitchingDirectivesTypeDefs} 10 | type Product { 11 | upc: ID! 12 | name: String! 13 | price: Int! 14 | weight: Int! 15 | } 16 | 17 | type Query { 18 | topProducts(first: Int = 2): [Product]! 19 | products(upcs: [ID!]!): [Product]! @merge(keyField: "upc") 20 | _sdl: String! 21 | } 22 | `; 23 | const products = [ 24 | { upc: '1', name: 'Table', price: 899, weight: 100 }, 25 | { upc: '2', name: 'Couch', price: 1299, weight: 1000 }, 26 | { upc: '3', name: 'Chair', price: 54, weight: 50 }, 27 | ]; 28 | 29 | export const productsServer = createServer( 30 | createYoga({ 31 | schema: stitchingDirectivesValidator( 32 | createSchema({ 33 | typeDefs, 34 | resolvers: { 35 | Query: { 36 | topProducts: (_root, args) => products.slice(0, args.first), 37 | products: (_root, { upcs }) => 38 | upcs.map( 39 | (upc: string) => 40 | products.find(product => product.upc === upc) || 41 | new GraphQLError('Record not found', { 42 | extensions: { 43 | code: 'NOT_FOUND', 44 | }, 45 | }), 46 | ), 47 | _sdl: () => typeDefs, 48 | }, 49 | }, 50 | }), 51 | ), 52 | }), 53 | ); 54 | -------------------------------------------------------------------------------- /examples/hot-schema-reloading/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "lib": ["esnext", "DOM"], 10 | "skipLibCheck": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/mutations-and-subscriptions/README.md: -------------------------------------------------------------------------------- 1 | # Mutations & subscriptions 2 | 3 | [See the details about this example on the website](https://the-guild.dev/graphql/stitching/handbook/foundation/mutations-and-subscriptions) 4 | -------------------------------------------------------------------------------- /examples/mutations-and-subscriptions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mutations-and-subscriptions", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "concurrently \"yarn:start-*\"", 8 | "start-gateway": "ts-node-dev src/index.ts", 9 | "start-posts": "ts-node-dev src/services/posts/index.ts", 10 | "start-users": "ts-node-dev src/services/users/index.ts" 11 | }, 12 | "dependencies": { 13 | "@graphql-tools/executor-http": "^2.0.0", 14 | "@graphql-tools/stitch": "^9.0.0", 15 | "@graphql-tools/wrap": "^10.0.0", 16 | "@types/node": "^22.0.0", 17 | "@types/wait-on": "^5.3.1", 18 | "concurrently": "^9.0.0", 19 | "graphql": "^16.6.0", 20 | "graphql-yoga": "^5.0.0", 21 | "ts-node": "^10.9.1", 22 | "ts-node-dev": "^2.0.0", 23 | "typescript": "^5.0.0", 24 | "wait-on": "^8.0.0" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/mutations-and-subscriptions/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { makeGatewayApp } from './gateway'; 3 | 4 | createServer( 5 | makeGatewayApp({ 6 | waitForPorts: true, 7 | }), 8 | ).listen(4000, () => console.log(`gateway running at http://localhost:4000/graphql`)); 9 | -------------------------------------------------------------------------------- /examples/mutations-and-subscriptions/src/services/posts/index.ts: -------------------------------------------------------------------------------- 1 | import { server } from './server'; 2 | 3 | server.listen(4001, () => console.info('posts running at http://localhost:4001/graphql')); 4 | -------------------------------------------------------------------------------- /examples/mutations-and-subscriptions/src/services/posts/server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { createPubSub, createSchema, createYoga } from 'graphql-yoga'; 3 | 4 | const pubsub = createPubSub(); 5 | const posts = []; 6 | const NEW_POST = 'new_post'; 7 | 8 | export const app = createYoga({ 9 | schema: createSchema({ 10 | typeDefs: /* GraphQL */ ` 11 | type Post { 12 | id: ID! 13 | message: String! 14 | user: User 15 | } 16 | 17 | type User { 18 | id: ID! 19 | } 20 | 21 | type Query { 22 | posts: [Post]! 23 | } 24 | 25 | type Mutation { 26 | createPost(message: String!): Post! 27 | } 28 | 29 | type Subscription { 30 | newPost: Post! 31 | } 32 | `, 33 | resolvers: { 34 | Post: { 35 | user: post => ({ id: post.userId }), 36 | }, 37 | Query: { 38 | posts: () => posts, 39 | }, 40 | Mutation: { 41 | createPost: (root, { message }) => { 42 | const newPost = { 43 | id: posts.length + 1, 44 | userId: String(Math.round(Math.random() * 2) + 1), 45 | message, 46 | }; 47 | 48 | posts.push(newPost); 49 | pubsub.publish(NEW_POST, { newPost }); 50 | return newPost; 51 | }, 52 | }, 53 | Subscription: { 54 | newPost: { 55 | subscribe: () => pubsub.subscribe(NEW_POST), 56 | }, 57 | }, 58 | }, 59 | }), 60 | }); 61 | 62 | export const server = createServer(app); 63 | -------------------------------------------------------------------------------- /examples/mutations-and-subscriptions/src/services/users/index.ts: -------------------------------------------------------------------------------- 1 | import { server } from './server'; 2 | 3 | server.listen(4002, () => console.info('users running at http://localhost:4002/graphql')); 4 | -------------------------------------------------------------------------------- /examples/mutations-and-subscriptions/src/services/users/server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { GraphQLError } from 'graphql'; 3 | import { createSchema, createYoga } from 'graphql-yoga'; 4 | 5 | const users = [ 6 | { id: '1', username: 'hanshotfirst', email: 'han@solo.me' }, 7 | { id: '2', username: 'bigvader23', email: 'vader@darkside.io' }, 8 | { id: '3', username: 'yodamecrazy', email: 'yoda@theforce.net' }, 9 | ]; 10 | 11 | export const app = createYoga({ 12 | schema: createSchema({ 13 | typeDefs: /* GraphQL */ ` 14 | type User { 15 | id: ID! 16 | username: String! 17 | email: String! 18 | } 19 | 20 | type Query { 21 | user(id: ID!): User 22 | } 23 | `, 24 | resolvers: { 25 | Query: { 26 | user: (root, { id }) => 27 | users.find(user => user.id === id) || 28 | new GraphQLError('Record not found', { 29 | extensions: { 30 | code: 'NOT_FOUND', 31 | }, 32 | }), 33 | }, 34 | }, 35 | }), 36 | }); 37 | 38 | export const server = createServer(app); 39 | -------------------------------------------------------------------------------- /examples/mutations-and-subscriptions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "lib": ["esnext", "DOM"], 10 | "skipLibCheck": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/persistent-connection/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "persistent-connection", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "start": "concurrently \"yarn:start-*\"", 6 | "start-gateway": "ts-node-dev src/index.ts", 7 | "start-inventory": "ts-node-dev src/services/inventory/index.ts", 8 | "start-products": "ts-node-dev src/services/products/index.ts" 9 | }, 10 | "dependencies": { 11 | "@graphql-tools/executor-graphql-ws": "^2.0.0", 12 | "@graphql-tools/stitch": "^9.0.0", 13 | "@graphql-tools/stitching-directives": "^3.0.0", 14 | "@graphql-tools/utils": "^10.0.0", 15 | "@types/ws": "^8.5.4", 16 | "concurrently": "^9.0.0", 17 | "graphql": "^16.6.0", 18 | "graphql-ws": "^6.0.0", 19 | "graphql-yoga": "^5.0.0", 20 | "ts-node": "^10.9.1", 21 | "ts-node-dev": "^2.0.0", 22 | "typescript": "^5.0.0", 23 | "ws": "^8.18.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/persistent-connection/src/gateway.ts: -------------------------------------------------------------------------------- 1 | import { buildSchema, parse } from 'graphql'; 2 | import { Client } from 'graphql-ws'; 3 | import { createYoga, isAsyncIterable } from 'graphql-yoga'; 4 | import { buildGraphQLWSExecutor } from '@graphql-tools/executor-graphql-ws'; 5 | import { stitchSchemas } from '@graphql-tools/stitch'; 6 | import { stitchingDirectives } from '@graphql-tools/stitching-directives'; 7 | import type { Executor } from '@graphql-tools/utils'; 8 | 9 | export const clients: Client[] = []; 10 | 11 | function onClient(client: Client) { 12 | clients.push(client); 13 | } 14 | 15 | async function makeGatewaySchema() { 16 | const { stitchingDirectivesTransformer } = stitchingDirectives(); 17 | const productsExec = buildGraphQLWSExecutor({ 18 | url: 'ws://localhost:4001/graphql', 19 | lazy: true, 20 | lazyCloseTimeout: 10_000, 21 | onClient, 22 | }); 23 | const inventoryExec = buildGraphQLWSExecutor({ 24 | url: 'ws://localhost:4002/graphql', 25 | lazy: true, 26 | lazyCloseTimeout: 10_000, 27 | onClient, 28 | }); 29 | 30 | return stitchSchemas({ 31 | subschemaConfigTransforms: [stitchingDirectivesTransformer], 32 | subschemas: [ 33 | { 34 | schema: await fetchRemoteSchema(productsExec), 35 | executor: productsExec, 36 | }, 37 | { 38 | schema: await fetchRemoteSchema(inventoryExec), 39 | executor: inventoryExec, 40 | }, 41 | ], 42 | }); 43 | } 44 | 45 | async function fetchRemoteSchema(executor: Executor) { 46 | const result = await executor({ 47 | document: parse(/* GraphQL */ ` 48 | { 49 | _sdl 50 | } 51 | `), 52 | }); 53 | if (isAsyncIterable(result)) { 54 | throw new Error('Expected executor to return a single result'); 55 | } 56 | return buildSchema(result.data._sdl); 57 | } 58 | 59 | export const gatewayApp = createYoga({ 60 | schema: makeGatewaySchema(), 61 | maskedErrors: false, 62 | graphiql: { 63 | title: 'Persistent connection', 64 | defaultQuery: /* GraphQL */ ` 65 | query { 66 | product1: product(id: "1") { 67 | id 68 | name 69 | stock 70 | } 71 | product2: product(id: "2") { 72 | id 73 | name 74 | stock 75 | } 76 | } 77 | `, 78 | }, 79 | }); 80 | -------------------------------------------------------------------------------- /examples/persistent-connection/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { clients, gatewayApp } from './gateway'; 3 | 4 | const server = createServer(gatewayApp); 5 | server.listen(4000, () => console.log(`Gateway running at http://localhost:4000/graphql`)); 6 | 7 | const events = ['SIGINT', 'SIGTERM']; 8 | events.forEach(event => { 9 | process.once(event, () => { 10 | server.close(); 11 | clients.forEach(client => client.dispose()); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /examples/persistent-connection/src/services/inventory/index.ts: -------------------------------------------------------------------------------- 1 | import { inventoryServer } from './server'; 2 | 3 | inventoryServer.listen(4002, () => { 4 | console.log(`Inventory service running at http://localhost:4002`); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/persistent-connection/src/services/inventory/server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | // yarn add ws 3 | import { useServer } from 'graphql-ws/use/ws'; 4 | import { createSchema } from 'graphql-yoga'; 5 | import { WebSocketServer } from 'ws'; 6 | import { stitchingDirectives } from '@graphql-tools/stitching-directives'; 7 | 8 | export const inventoryServer = createServer(); 9 | 10 | const wsServer = new WebSocketServer({ 11 | server: inventoryServer, 12 | path: '/graphql', 13 | }); 14 | 15 | const products = [ 16 | { id: '1', stock: 100 }, 17 | { id: '2', stock: 200 }, 18 | ]; 19 | 20 | const { stitchingDirectivesTypeDefs, stitchingDirectivesValidator } = stitchingDirectives(); 21 | 22 | const typeDefs = /* GraphQL */ ` 23 | ${stitchingDirectivesTypeDefs} 24 | type Product { 25 | id: ID! 26 | stock: Int! 27 | } 28 | 29 | type Query { 30 | product(id: ID): Product @merge(keyField: "id", keyArg: "id") 31 | _sdl: String! 32 | } 33 | `; 34 | 35 | useServer( 36 | { 37 | schema: stitchingDirectivesValidator( 38 | createSchema({ 39 | typeDefs, 40 | resolvers: { 41 | Query: { 42 | product: (_root, { id }) => products.find(product => product.id === id), 43 | _sdl: () => typeDefs, 44 | }, 45 | }, 46 | }), 47 | ), 48 | }, 49 | wsServer, 50 | ); 51 | -------------------------------------------------------------------------------- /examples/persistent-connection/src/services/products/index.ts: -------------------------------------------------------------------------------- 1 | import { productsServer } from './server'; 2 | 3 | productsServer.listen(4001, () => { 4 | console.log(`Products service running at http://localhost:4001`); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/persistent-connection/src/services/products/server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | // yarn add ws 3 | import { useServer } from 'graphql-ws/use/ws'; 4 | import { createSchema } from 'graphql-yoga'; 5 | import { WebSocketServer } from 'ws'; 6 | import { stitchingDirectives } from '@graphql-tools/stitching-directives'; 7 | 8 | export const productsServer = createServer(); 9 | 10 | const wsServer = new WebSocketServer({ 11 | server: productsServer, 12 | path: '/graphql', 13 | }); 14 | 15 | const products = [ 16 | { id: '1', name: 'Product 1' }, 17 | { id: '2', name: 'Product 2' }, 18 | ]; 19 | 20 | const { stitchingDirectivesTypeDefs, stitchingDirectivesValidator } = stitchingDirectives(); 21 | 22 | const typeDefs = /* GraphQL */ ` 23 | ${stitchingDirectivesTypeDefs} 24 | type Product @canonical { 25 | id: ID! 26 | name: String! 27 | } 28 | 29 | type Query { 30 | product(id: ID): Product @merge(keyField: "id", keyArg: "id") 31 | _sdl: String! 32 | } 33 | `; 34 | 35 | useServer( 36 | { 37 | schema: stitchingDirectivesValidator( 38 | createSchema({ 39 | typeDefs, 40 | resolvers: { 41 | Query: { 42 | product: (_root, { id }) => products.find(product => product.id === id), 43 | _sdl: () => typeDefs, 44 | }, 45 | }, 46 | }), 47 | ), 48 | }, 49 | wsServer, 50 | ); 51 | -------------------------------------------------------------------------------- /examples/persistent-connection/tests/persistent-connection.test.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'net'; 2 | import { clients, gatewayApp } from '../src/gateway'; 3 | import { inventoryServer } from '../src/services/inventory/server'; 4 | import { productsServer } from '../src/services/products/server'; 5 | 6 | describe('Persistent connection', () => { 7 | const connectionSet = new Set(); 8 | beforeAll(async () => { 9 | productsServer.on('connection', connection => { 10 | // Gateway should only open 1 connection to each service 11 | expect(connectionSet.size).toBeLessThanOrEqual(2); 12 | connectionSet.add(connection); 13 | }); 14 | inventoryServer.on('connection', connection => { 15 | // Gateway should only open 1 connection to each service 16 | expect(connectionSet.size).toBeLessThanOrEqual(2); 17 | connectionSet.add(connection); 18 | }); 19 | await new Promise(resolve => productsServer.listen(4001, resolve)); 20 | await new Promise(resolve => inventoryServer.listen(4002, resolve)); 21 | }); 22 | afterAll(async () => { 23 | connectionSet.forEach(connection => connection.destroy()); 24 | clients.forEach(client => client.dispose()); 25 | await new Promise(resolve => productsServer.close(resolve)); 26 | await new Promise(resolve => inventoryServer.close(resolve)); 27 | }); 28 | it('should work', async () => { 29 | const response = await gatewayApp.fetch('/graphql', { 30 | method: 'POST', 31 | headers: { 32 | 'Content-Type': 'application/json', 33 | }, 34 | body: JSON.stringify({ 35 | query: /* GraphQL */ ` 36 | query { 37 | product1: product(id: "1") { 38 | id 39 | name 40 | stock 41 | } 42 | product2: product(id: "2") { 43 | id 44 | name 45 | stock 46 | } 47 | } 48 | `, 49 | }), 50 | }); 51 | const result = await response.json(); 52 | expect(result).toMatchInlineSnapshot(` 53 | { 54 | "data": { 55 | "product1": { 56 | "id": "1", 57 | "name": "Product 1", 58 | "stock": 100, 59 | }, 60 | "product2": { 61 | "id": "2", 62 | "name": "Product 2", 63 | "stock": 200, 64 | }, 65 | }, 66 | } 67 | `); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /examples/persistent-connection/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "lib": ["esnext", "DOM"], 10 | "skipLibCheck": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/public-and-private-apis/README.md: -------------------------------------------------------------------------------- 1 | # Public & private APIs 2 | 3 | [See the details about this example on the website](https://the-guild.dev/graphql/stitching/handbook/architecture/public-and-private-apis) 4 | -------------------------------------------------------------------------------- /examples/public-and-private-apis/gateway.ts: -------------------------------------------------------------------------------- 1 | import { createYoga } from 'graphql-yoga'; 2 | import { stitchSchemas } from '@graphql-tools/stitch'; 3 | import { stitchingDirectives } from '@graphql-tools/stitching-directives'; 4 | import { filterSchema, pruneSchema } from '@graphql-tools/utils'; 5 | import { accountsSchema } from './services/accounts'; 6 | import { productsSchema } from './services/products'; 7 | import { reviewsSchema } from './services/reviews'; 8 | 9 | const { stitchingDirectivesTransformer } = stitchingDirectives(); 10 | 11 | function makeGatewaySchema() { 12 | return stitchSchemas({ 13 | subschemaConfigTransforms: [stitchingDirectivesTransformer], 14 | subschemas: [{ schema: reviewsSchema }, { schema: accountsSchema }, { schema: productsSchema }], 15 | }); 16 | } 17 | 18 | // Build public and private versions of the gateway schema: 19 | // - the private schema has all fields, including internal services. 20 | // - the public schema has all underscored name fields removed. 21 | const privateSchema = makeGatewaySchema(); 22 | const publicSchema = pruneSchema( 23 | filterSchema({ 24 | schema: privateSchema, 25 | rootFieldFilter: (type, fieldName) => !fieldName.startsWith('_'), 26 | fieldFilter: (type, fieldName) => !fieldName.startsWith('_'), 27 | argumentFilter: (typeName, fieldName, argName) => !argName.startsWith('_'), 28 | }), 29 | ); 30 | 31 | // Serve the public and private schema versions at different locations. 32 | // This allows the public to access one API with reduced features, 33 | // while internal services can authenticate with the private API for all features. 34 | export const gatewayApp = createYoga({ 35 | schema({ request }) { 36 | const url = new URL(request.url, 'http://localhost'); 37 | if (url.pathname === '/private/graphql') return privateSchema; 38 | return publicSchema; 39 | }, 40 | maskedErrors: false, 41 | graphqlEndpoint: '/:scope/graphql', 42 | }); 43 | -------------------------------------------------------------------------------- /examples/public-and-private-apis/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { gatewayApp } from './gateway'; 3 | 4 | createServer(gatewayApp).listen(4000, () => 5 | console.log(`gateway running at http://localhost:4000/graphql`), 6 | ); 7 | -------------------------------------------------------------------------------- /examples/public-and-private-apis/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "public-and-private-apis", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "private": "true", 6 | "scripts": { 7 | "start": "ts-node-dev src/index.ts" 8 | }, 9 | "dependencies": { 10 | "@graphql-tools/stitch": "^9.0.0", 11 | "@graphql-tools/stitching-directives": "^3.0.0", 12 | "@graphql-tools/utils": "^10.0.0", 13 | "graphql": "^16.6.0", 14 | "graphql-yoga": "^5.0.0", 15 | "ts-node": "^10.9.1", 16 | "ts-node-dev": "^2.0.0", 17 | "typescript": "^5.0.0" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/public-and-private-apis/services/accounts.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql'; 2 | import { createSchema } from 'graphql-yoga'; 3 | import { stitchingDirectives } from '@graphql-tools/stitching-directives'; 4 | 5 | const { stitchingDirectivesTypeDefs, stitchingDirectivesValidator } = stitchingDirectives(); 6 | 7 | const typeDefs = /* GraphQL */ ` 8 | ${stitchingDirectivesTypeDefs} 9 | "Represents a human user with an account." 10 | type User @canonical { 11 | "The primary key of this user." 12 | id: ID! 13 | "A formal display name for this user." 14 | name: String! 15 | "An alpha-numeric handle for this user." 16 | username: String! 17 | } 18 | 19 | type Query { 20 | "Specifies the current user, or null when anonymous." 21 | me: User 22 | "Fetches a User by ID reference." 23 | user(id: ID!): User @merge(keyField: "id") 24 | _sdl: String! 25 | } 26 | `; 27 | 28 | const users = [ 29 | { id: '1', name: 'Ada Lovelace', username: '@ada' }, 30 | { id: '2', name: 'Alan Turing', username: '@complete' }, 31 | ]; 32 | 33 | export const accountsSchema = stitchingDirectivesValidator( 34 | createSchema({ 35 | typeDefs, 36 | resolvers: { 37 | Query: { 38 | me: () => users[0], 39 | user: (_root, { id }) => 40 | users.find(user => user.id === id) || 41 | new GraphQLError('Record not found', { 42 | extensions: { 43 | code: 'NOT_FOUND', 44 | }, 45 | }), 46 | _sdl: () => typeDefs, 47 | }, 48 | }, 49 | }), 50 | ); 51 | -------------------------------------------------------------------------------- /examples/public-and-private-apis/services/products.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql'; 2 | import { createSchema } from 'graphql-yoga'; 3 | import { stitchingDirectives } from '@graphql-tools/stitching-directives'; 4 | 5 | const { stitchingDirectivesTypeDefs, stitchingDirectivesValidator } = stitchingDirectives(); 6 | 7 | const typeDefs = /* GraphQL */ ` 8 | ${stitchingDirectivesTypeDefs} 9 | "Represents a retail product." 10 | type Product @canonical { 11 | "The Univeral Product Code (UPC) of this product." 12 | upc: ID! 13 | "The name of this product." 14 | name: String! 15 | "The price of this product, in whole GBP (£)." 16 | price: Int! 17 | "The weight of this product, in grams." 18 | weight: Int! 19 | } 20 | 21 | type Query { 22 | "Fetches an array of Products by their UPC references." 23 | products(upcs: [ID!]!): [Product]! @merge(keyField: "upc") 24 | _sdl: String! 25 | } 26 | `; 27 | 28 | const products = [ 29 | { upc: '1', name: 'Table', price: 899, weight: 100 }, 30 | { upc: '2', name: 'Couch', price: 1299, weight: 1000 }, 31 | { upc: '3', name: 'Chair', price: 54, weight: 50 }, 32 | ]; 33 | 34 | export const productsSchema = stitchingDirectivesValidator( 35 | createSchema({ 36 | typeDefs, 37 | resolvers: { 38 | Query: { 39 | products: (_root, { upcs }) => 40 | upcs.map( 41 | (upc: string) => 42 | products.find(product => product.upc === upc) || 43 | new GraphQLError('Record not found', { 44 | extensions: { 45 | code: 'NOT_FOUND', 46 | }, 47 | }), 48 | ), 49 | _sdl: () => typeDefs, 50 | }, 51 | }, 52 | }), 53 | ); 54 | -------------------------------------------------------------------------------- /examples/public-and-private-apis/tests/__snapshots__/public-and-private-apis.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Public and private APIs private API should allow to query internal fields: private-internal 1`] = ` 4 | { 5 | "data": { 6 | "_users": [ 7 | { 8 | "__typename": "User", 9 | }, 10 | ], 11 | }, 12 | } 13 | `; 14 | 15 | exports[`Public and private APIs public API should not allow to query internal fields: public-internal 1`] = ` 16 | { 17 | "errors": [ 18 | { 19 | "extensions": { 20 | "code": "GRAPHQL_VALIDATION_FAILED", 21 | }, 22 | "locations": [ 23 | { 24 | "column": 13, 25 | "line": 3, 26 | }, 27 | ], 28 | "message": "Cannot query field "_sdl" on type "Query".", 29 | }, 30 | ], 31 | } 32 | `; 33 | -------------------------------------------------------------------------------- /examples/public-and-private-apis/tests/public-and-private-apis.test.ts: -------------------------------------------------------------------------------- 1 | import { gatewayApp } from '../gateway'; 2 | 3 | describe('Public and private APIs', () => { 4 | it('public API should not allow to query internal fields', async () => { 5 | const response = await gatewayApp.fetch('http://localhost/public/graphql', { 6 | method: 'POST', 7 | headers: { 8 | 'Content-Type': 'application/json', 9 | }, 10 | body: JSON.stringify({ 11 | query: /* GraphQL */ ` 12 | query { 13 | _sdl 14 | } 15 | `, 16 | }), 17 | }); 18 | const result = await response.json(); 19 | expect(result).toMatchSnapshot('public-internal'); 20 | }); 21 | it('private API should allow to query internal fields', async () => { 22 | const response = await gatewayApp.fetch('http://localhost/private/graphql', { 23 | method: 'POST', 24 | headers: { 25 | 'Content-Type': 'application/json', 26 | }, 27 | body: JSON.stringify({ 28 | query: /* GraphQL */ ` 29 | query { 30 | _users(ids: ["1"]) { 31 | __typename 32 | } 33 | } 34 | `, 35 | }), 36 | }); 37 | const result = await response.json(); 38 | expect(result).toMatchSnapshot('private-internal'); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /examples/public-and-private-apis/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "lib": ["esnext", "DOM"], 10 | "skipLibCheck": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/stitching-directives-sdl/README.md: -------------------------------------------------------------------------------- 1 | # Stitching directives SDL 2 | 3 | [See the details about this example on the website](https://the-guild.dev/graphql/stitching/handbook/foundation/stitching-directives-sdl) 4 | -------------------------------------------------------------------------------- /examples/stitching-directives-sdl/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stitching-directives-sdl", 3 | "version": "1.0.0", 4 | "license": "MIT", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "concurrently \"yarn:start-*\"", 8 | "start-accounts": "ts-node-dev src/services/accounts/index.ts", 9 | "start-gateway": "ts-node-dev src/index.ts", 10 | "start-inventory": "ts-node-dev src/services/inventory/index.ts", 11 | "start-products": "ts-node-dev src/services/products/index.ts", 12 | "start-reviews": "ts-node-dev src/services/reviews/index.ts" 13 | }, 14 | "dependencies": { 15 | "@graphql-tools/executor-http": "^2.0.0", 16 | "@graphql-tools/stitch": "^9.0.0", 17 | "@graphql-tools/stitching-directives": "^3.0.0", 18 | "@graphql-tools/utils": "^10.0.0", 19 | "concurrently": "^9.0.0", 20 | "graphql": "^16.6.0", 21 | "graphql-yoga": "^5.0.0", 22 | "ts-node": "^10.9.1", 23 | "ts-node-dev": "^2.0.0", 24 | "typescript": "^5.0.0", 25 | "wait-on": "^8.0.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/stitching-directives-sdl/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { gatewayApp } from './gateway'; 3 | 4 | createServer(gatewayApp).listen(4000, () => 5 | console.log(`gateway running at http://localhost:4000/graphql`), 6 | ); 7 | -------------------------------------------------------------------------------- /examples/stitching-directives-sdl/src/services/accounts/index.ts: -------------------------------------------------------------------------------- 1 | import { accountsServer } from './server'; 2 | 3 | accountsServer.listen(4001, () => { 4 | console.log(`Accounts service running at http://localhost:4001`); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/stitching-directives-sdl/src/services/accounts/server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { GraphQLError } from 'graphql'; 3 | import { createSchema, createYoga } from 'graphql-yoga'; 4 | import { stitchingDirectives } from '@graphql-tools/stitching-directives'; 5 | 6 | const { stitchingDirectivesTypeDefs, stitchingDirectivesValidator } = stitchingDirectives(); 7 | 8 | const typeDefs = /* GraphQL */ ` 9 | ${stitchingDirectivesTypeDefs} 10 | type User { 11 | id: ID! 12 | name: String! 13 | username: String! 14 | } 15 | 16 | type Query { 17 | me: User 18 | user(id: ID!): User @merge(keyField: "id") 19 | _sdl: String! 20 | } 21 | `; 22 | 23 | const users = [ 24 | { id: '1', name: 'Ada Lovelace', username: '@ada' }, 25 | { id: '2', name: 'Alan Turing', username: '@complete' }, 26 | ]; 27 | 28 | export const accountsServer = createServer( 29 | createYoga({ 30 | schema: stitchingDirectivesValidator( 31 | createSchema({ 32 | typeDefs, 33 | resolvers: { 34 | Query: { 35 | me: () => users[0], 36 | user: (_root, { id }) => 37 | users.find(user => user.id === id) || 38 | new GraphQLError('Record not found', { 39 | extensions: { 40 | code: 'NOT_FOUND', 41 | }, 42 | }), 43 | _sdl: () => typeDefs, 44 | }, 45 | }, 46 | }), 47 | ), 48 | }), 49 | ); 50 | -------------------------------------------------------------------------------- /examples/stitching-directives-sdl/src/services/inventory/index.ts: -------------------------------------------------------------------------------- 1 | import { inventoryServer } from './server'; 2 | 3 | inventoryServer.listen(4002, () => { 4 | console.log(`Inventory service running at http://localhost:4002`); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/stitching-directives-sdl/src/services/inventory/server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { GraphQLError } from 'graphql'; 3 | import { createSchema, createYoga } from 'graphql-yoga'; 4 | import { stitchingDirectives } from '@graphql-tools/stitching-directives'; 5 | 6 | const { stitchingDirectivesTypeDefs, stitchingDirectivesValidator } = stitchingDirectives(); 7 | 8 | const inventories = [ 9 | { upc: '1', unitsInStock: 3 }, 10 | { upc: '2', unitsInStock: 0 }, 11 | { upc: '3', unitsInStock: 5 }, 12 | ]; 13 | 14 | const typeDefs = /* GraphQL */ ` 15 | ${stitchingDirectivesTypeDefs} 16 | "Stuff sitting in warehouse inventory" 17 | type Product @key(selectionSet: "{ upc }") { 18 | upc: ID! 19 | "Specifies if this product is currently stocked." 20 | inStock: Boolean 21 | "Specifies the estimated shipping cost of this product, in cents." 22 | shippingEstimate: Int @computed(selectionSet: "{ price weight }") 23 | } 24 | 25 | scalar _Key 26 | 27 | type Query { 28 | mostStockedProduct: Product 29 | _products(keys: [_Key!]!): [Product]! @merge 30 | _sdl: String! 31 | } 32 | `; 33 | 34 | export const inventoryServer = createServer( 35 | createYoga({ 36 | schema: stitchingDirectivesValidator( 37 | createSchema({ 38 | typeDefs, 39 | resolvers: { 40 | Product: { 41 | inStock: product => product.unitsInStock > 0, 42 | shippingEstimate(product) { 43 | // free for expensive items, otherwise estimate based on weight 44 | return product.price > 1000 ? 0 : Math.round(product.weight * 0.5); 45 | }, 46 | }, 47 | Query: { 48 | mostStockedProduct: () => 49 | inventories.reduce( 50 | (acc, i) => (acc.unitsInStock >= i.unitsInStock ? acc : i), 51 | inventories[0], 52 | ), 53 | _products: (_root, { keys }) => 54 | keys.map(key => { 55 | const inventory = inventories.find(i => i.upc === key.upc); 56 | return inventory 57 | ? { ...key, ...inventory } 58 | : new GraphQLError('Record not found', { 59 | extensions: { 60 | code: 'NOT_FOUND', 61 | }, 62 | }); 63 | }), 64 | _sdl: () => typeDefs, 65 | }, 66 | }, 67 | }), 68 | ), 69 | }), 70 | ); 71 | -------------------------------------------------------------------------------- /examples/stitching-directives-sdl/src/services/products/index.ts: -------------------------------------------------------------------------------- 1 | import { productsServer } from './server'; 2 | 3 | productsServer.listen(4003, () => { 4 | console.log(`Products service running at http://localhost:4003`); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/stitching-directives-sdl/src/services/products/server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { GraphQLError } from 'graphql'; 3 | import { createSchema, createYoga } from 'graphql-yoga'; 4 | import { stitchingDirectives } from '@graphql-tools/stitching-directives'; 5 | 6 | const { stitchingDirectivesTypeDefs, stitchingDirectivesValidator } = stitchingDirectives(); 7 | 8 | const typeDefs = /* GraphQL */ ` 9 | ${stitchingDirectivesTypeDefs} 10 | "Represents a Product available for resale." 11 | type Product @canonical { 12 | "The primary key of this product." 13 | upc: ID! 14 | "The name of this product." 15 | name: String! 16 | "The price of this product in cents." 17 | price: Int! 18 | "The weight of this product in grams." 19 | weight: Int! 20 | } 21 | 22 | type Query { 23 | topProducts(first: Int = 2): [Product]! 24 | products(upcs: [ID!]!, order: String): [Product]! 25 | @merge( 26 | keyField: "upc" 27 | keyArg: "upcs" 28 | additionalArgs: """ 29 | order: "price" 30 | """ 31 | ) 32 | _sdl: String! 33 | } 34 | `; 35 | 36 | const products = [ 37 | { upc: '1', name: 'Table', price: 899, weight: 100 }, 38 | { upc: '2', name: 'Couch', price: 1299, weight: 1000 }, 39 | { upc: '3', name: 'Chair', price: 54, weight: 50 }, 40 | ]; 41 | 42 | export const productsServer = createServer( 43 | createYoga({ 44 | schema: stitchingDirectivesValidator( 45 | createSchema({ 46 | typeDefs, 47 | resolvers: { 48 | Query: { 49 | topProducts: (_root, args) => products.slice(0, args.first), 50 | products: (_root, { upcs }) => 51 | upcs.map( 52 | upc => 53 | products.find(product => product.upc === upc) || 54 | new GraphQLError('Record not found', { 55 | extensions: { 56 | code: 'NOT_FOUND', 57 | }, 58 | }), 59 | ), 60 | _sdl: () => typeDefs, 61 | }, 62 | }, 63 | }), 64 | ), 65 | }), 66 | ); 67 | -------------------------------------------------------------------------------- /examples/stitching-directives-sdl/src/services/reviews/index.ts: -------------------------------------------------------------------------------- 1 | import { reviewsServer } from './server'; 2 | 3 | reviewsServer.listen(4004, () => { 4 | console.log(`Reviews service running at http://localhost:4004`); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/stitching-directives-sdl/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "lib": ["esnext", "DOM"], 10 | "skipLibCheck": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/subservice-languages/javascript/index.js: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { gatewayApp } from './gateway.js'; 3 | 4 | createServer(gatewayApp).listen(4000, () => { 5 | console.log('Gateway ready at http://localhost:4000/graphql'); 6 | }); 7 | -------------------------------------------------------------------------------- /examples/subservice-languages/javascript/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subservice-languages-javascript", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "license": "MIT", 6 | "main": "index.js", 7 | "scripts": { 8 | "start": "concurrently \"yarn:start-gateway\" \"yarn:start-services\"", 9 | "start-service-accounts": "nodemon -e js,graphql services/accounts/index.js", 10 | "start-service-gateway": "nodemon -e js,graphql index.js", 11 | "start-service-inventory": "nodemon -e js,graphql services/inventory/index.js", 12 | "start-service-products": "nodemon --watch services/products/**/*.ts --exec ts-node-esm services/products/index.ts", 13 | "start-service-reviews": "nodemon -e js,graphql services/reviews/index.js", 14 | "start-services": "concurrently \"yarn:start-service-*\"" 15 | }, 16 | "dependencies": { 17 | "@graphql-tools/executor-http": "^2.0.0", 18 | "@graphql-tools/stitch": "^9.0.0", 19 | "@graphql-tools/stitching-directives": "^3.0.0", 20 | "@graphql-tools/utils": "^10.0.0", 21 | "@types/node": "^22.0.0", 22 | "class-validator": "^0.14.0", 23 | "concurrently": "^9.0.0", 24 | "graphql": "^16.6.0", 25 | "graphql-scalars": "^1.22.2", 26 | "graphql-yoga": "^5.0.0", 27 | "nexus": "^1.0.0", 28 | "nodemon": "^3.0.0", 29 | "reflect-metadata": "^0.2.0", 30 | "ts-node": "^10.9.1", 31 | "type-graphql": "^2.0.0-beta.1", 32 | "typescript": "^5.0.0", 33 | "wait-on": "^8.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /examples/subservice-languages/javascript/services/accounts/index.js: -------------------------------------------------------------------------------- 1 | import { accountsServer } from './server.js'; 2 | 3 | accountsServer.listen(4001, () => { 4 | console.log('Accounts ready at http://localhost:4001/graphql'); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/subservice-languages/javascript/services/accounts/server.js: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { 3 | GraphQLError, 4 | GraphQLID, 5 | GraphQLNonNull, 6 | GraphQLObjectType, 7 | GraphQLScalarType, 8 | GraphQLSchema, 9 | GraphQLString, 10 | specifiedDirectives, 11 | } from 'graphql'; 12 | import { createYoga } from 'graphql-yoga'; 13 | import { stitchingDirectives } from '@graphql-tools/stitching-directives'; 14 | import { printSchemaWithDirectives } from '@graphql-tools/utils'; 15 | 16 | const { allStitchingDirectives, stitchingDirectivesValidator } = stitchingDirectives(); 17 | 18 | const users = [ 19 | { id: '1', name: 'Ada Lovelace', username: '@ada' }, 20 | { id: '2', name: 'Alan Turing', username: '@complete' }, 21 | ]; 22 | 23 | const accountsSchemaTypes = Object.create(null); 24 | 25 | accountsSchemaTypes._Key = new GraphQLScalarType({ 26 | name: '_Key', 27 | }); 28 | accountsSchemaTypes.Query = new GraphQLObjectType({ 29 | name: 'Query', 30 | fields: () => ({ 31 | me: { 32 | type: accountsSchemaTypes.User, 33 | resolve: () => users[0], 34 | }, 35 | user: { 36 | type: accountsSchemaTypes.User, 37 | args: { 38 | id: { 39 | type: new GraphQLNonNull(GraphQLID), 40 | }, 41 | }, 42 | resolve: (_root, { id }) => 43 | users.find(user => user.id === id) || 44 | new GraphQLError('Record not found', { 45 | extensions: { 46 | code: 'NOT_FOUND', 47 | }, 48 | }), 49 | extensions: { directives: { merge: { keyField: 'id' } } }, 50 | }, 51 | _sdl: { 52 | type: new GraphQLNonNull(GraphQLString), 53 | resolve(_root, _args, _context, info) { 54 | return printSchemaWithDirectives(info.schema); 55 | }, 56 | }, 57 | }), 58 | }); 59 | 60 | accountsSchemaTypes.User = new GraphQLObjectType({ 61 | name: 'User', 62 | fields: () => ({ 63 | id: { type: GraphQLID }, 64 | name: { type: GraphQLString }, 65 | username: { type: GraphQLString }, 66 | }), 67 | extensions: { 68 | directives: { 69 | key: { 70 | selectionSet: '{ id }', 71 | }, 72 | }, 73 | }, 74 | }); 75 | 76 | const accountsSchema = new GraphQLSchema({ 77 | query: accountsSchemaTypes.Query, 78 | directives: [...specifiedDirectives, ...allStitchingDirectives], 79 | }); 80 | 81 | export const accountsServer = createServer( 82 | createYoga({ 83 | schema: stitchingDirectivesValidator(accountsSchema), 84 | }), 85 | ); 86 | -------------------------------------------------------------------------------- /examples/subservice-languages/javascript/services/inventory/index.js: -------------------------------------------------------------------------------- 1 | import { inventoryServer } from './server.js'; 2 | 3 | inventoryServer.listen(4002, () => { 4 | console.log('Inventory ready at http://localhost:4002/graphql'); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/subservice-languages/javascript/services/products/index.ts: -------------------------------------------------------------------------------- 1 | import { productsServer } from './server.js'; 2 | 3 | productsServer.listen(4003, () => { 4 | console.log('products running at http://localhost:4003/graphql'); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/subservice-languages/javascript/services/reviews/index.js: -------------------------------------------------------------------------------- 1 | import { reviewsServer } from './server.js'; 2 | 3 | reviewsServer.listen(4004, () => { 4 | console.log('Reviews ready at http://localhost:4004/graphql'); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/subservice-languages/javascript/tests/__snapshots__/gateway.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`JavaScript Code-First Schemas should work: test-query 1`] = ` 4 | { 5 | "data": { 6 | "products": [ 7 | { 8 | "inStock": true, 9 | "name": "Table", 10 | "price": 899, 11 | "reviews": [ 12 | { 13 | "author": { 14 | "name": "Ada Lovelace", 15 | "totalReviews": 2, 16 | "username": "@ada", 17 | }, 18 | "body": "Love it!", 19 | "id": "1", 20 | "product": { 21 | "name": "Table", 22 | "price": 899, 23 | }, 24 | }, 25 | { 26 | "author": { 27 | "name": "Alan Turing", 28 | "totalReviews": 2, 29 | "username": "@complete", 30 | }, 31 | "body": "Prefer something else.", 32 | "id": "4", 33 | "product": { 34 | "name": "Table", 35 | "price": 899, 36 | }, 37 | }, 38 | ], 39 | "shippingEstimate": 50, 40 | "weight": 100, 41 | }, 42 | { 43 | "inStock": false, 44 | "name": "Couch", 45 | "price": 1299, 46 | "reviews": [ 47 | { 48 | "author": { 49 | "name": "Ada Lovelace", 50 | "totalReviews": 2, 51 | "username": "@ada", 52 | }, 53 | "body": "Too expensive.", 54 | "id": "2", 55 | "product": { 56 | "name": "Couch", 57 | "price": 1299, 58 | }, 59 | }, 60 | ], 61 | "shippingEstimate": 0, 62 | "weight": 1000, 63 | }, 64 | ], 65 | }, 66 | } 67 | `; 68 | -------------------------------------------------------------------------------- /examples/subservice-languages/javascript/tests/gateway.spec.ts: -------------------------------------------------------------------------------- 1 | import { gatewayApp } from '../gateway'; 2 | import { accountsServer } from '../services/accounts/server'; 3 | import { inventoryServer } from '../services/inventory/server'; 4 | import { productsServer } from '../services/products/server'; 5 | import { reviewsServer } from '../services/reviews/server'; 6 | 7 | describe('JavaScript Code-First Schemas', () => { 8 | beforeAll(async () => { 9 | await Promise.all([ 10 | new Promise(resolve => accountsServer.listen(4001, resolve)), 11 | new Promise(resolve => inventoryServer.listen(4002, resolve)), 12 | new Promise(resolve => productsServer.listen(4003, resolve)), 13 | new Promise(resolve => reviewsServer.listen(4004, resolve)), 14 | ]); 15 | }); 16 | afterAll(async () => { 17 | await Promise.all([ 18 | new Promise(resolve => accountsServer.close(resolve)), 19 | new Promise(resolve => inventoryServer.close(resolve)), 20 | new Promise(resolve => productsServer.close(resolve)), 21 | new Promise(resolve => reviewsServer.close(resolve)), 22 | ]); 23 | }); 24 | it('should work', async () => { 25 | const response = await gatewayApp.fetch('/graphql', { 26 | method: 'POST', 27 | headers: { 28 | 'Content-Type': 'application/json', 29 | }, 30 | body: JSON.stringify({ 31 | query: /* GraphQL */ ` 32 | query { 33 | products(upcs: [1, 2]) { 34 | name 35 | price 36 | weight 37 | inStock 38 | shippingEstimate 39 | reviews { 40 | id 41 | body 42 | author { 43 | name 44 | username 45 | totalReviews 46 | } 47 | product { 48 | name 49 | price 50 | } 51 | } 52 | } 53 | } 54 | `, 55 | }), 56 | }); 57 | const result = await response.json(); 58 | expect(result).toMatchSnapshot('test-query'); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /examples/subservice-languages/javascript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "lib": ["es2018", "esnext.asynciterable"], 7 | "experimentalDecorators": true, 8 | "emitDecoratorMetadata": true, 9 | "allowJs": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/subservice-languages/ruby/.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | -------------------------------------------------------------------------------- /examples/subservice-languages/ruby/Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gem 'graphql', '~> 1.13' 6 | gem 'graphql-schema_directives', '~> 0.0' 7 | gem 'rack', '~> 2.2' 8 | gem 'webrick', '~> 1.9.1' -------------------------------------------------------------------------------- /examples/subservice-languages/ruby/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | base64 (0.2.0) 5 | graphql (1.13.24) 6 | base64 7 | graphql-schema_directives (0.0.2) 8 | graphql (~> 1.9) 9 | rack (2.2.14) 10 | webrick (1.9.1) 11 | 12 | PLATFORMS 13 | x86_64-darwin-19 14 | x86_64-linux 15 | 16 | DEPENDENCIES 17 | graphql (~> 1.13) 18 | graphql-schema_directives (~> 0.0) 19 | rack (~> 2.2) 20 | webrick (~> 1.9.1) 21 | 22 | BUNDLED WITH 23 | 2.2.3 24 | -------------------------------------------------------------------------------- /examples/subservice-languages/ruby/gateway.ts: -------------------------------------------------------------------------------- 1 | import { buildSchema, parse } from 'graphql'; 2 | import { createYoga, isAsyncIterable } from 'graphql-yoga'; 3 | import waitOn from 'wait-on'; 4 | import { buildHTTPExecutor } from '@graphql-tools/executor-http'; 5 | import { stitchSchemas } from '@graphql-tools/stitch'; 6 | import { stitchingDirectives } from '@graphql-tools/stitching-directives'; 7 | import { Executor } from '@graphql-tools/utils'; 8 | 9 | const SDL_QUERY = parse(/* GraphQL */ ` 10 | query { 11 | _sdl 12 | } 13 | `); 14 | 15 | async function fetchRemoteSchema(executor: Executor) { 16 | const result = await executor({ document: SDL_QUERY }); 17 | if (isAsyncIterable(result)) { 18 | throw new Error('Executor must return a Promise or Observable'); 19 | } 20 | return buildSchema(result.data._sdl); 21 | } 22 | 23 | async function makeGatewaySchema() { 24 | await waitOn({ resources: ['tcp:4001', 'tcp:4002', 'tcp:4003'] }); 25 | const { stitchingDirectivesTransformer } = stitchingDirectives(); 26 | const accountsExec = buildHTTPExecutor({ endpoint: 'http://localhost:4001/graphql' }); 27 | const productsExec = buildHTTPExecutor({ endpoint: 'http://localhost:4002/graphql' }); 28 | const reviewsExec = buildHTTPExecutor({ endpoint: 'http://localhost:4003/graphql' }); 29 | 30 | return stitchSchemas({ 31 | subschemaConfigTransforms: [stitchingDirectivesTransformer], 32 | subschemas: [ 33 | { 34 | schema: await fetchRemoteSchema(accountsExec), 35 | executor: accountsExec, 36 | }, 37 | { 38 | schema: await fetchRemoteSchema(productsExec), 39 | executor: productsExec, 40 | }, 41 | { 42 | schema: await fetchRemoteSchema(reviewsExec), 43 | executor: reviewsExec, 44 | }, 45 | ], 46 | }); 47 | } 48 | 49 | export const gatewayApp = createYoga({ 50 | schema: makeGatewaySchema(), 51 | maskedErrors: false, 52 | graphiql: { 53 | title: 'Ruby subservices', 54 | defaultQuery: /* GraphQL */ ` 55 | query { 56 | users(ids: ["1", "2"]) { 57 | id 58 | name 59 | username 60 | reviews { 61 | body 62 | product { 63 | name 64 | } 65 | } 66 | } 67 | } 68 | `, 69 | }, 70 | }); 71 | -------------------------------------------------------------------------------- /examples/subservice-languages/ruby/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { gatewayApp } from './gateway'; 3 | 4 | createServer(gatewayApp).listen(4000, () => { 5 | console.log('Gateway listening http://localhost:4000/graphql'); 6 | }); 7 | -------------------------------------------------------------------------------- /examples/subservice-languages/ruby/lib/base_schema.rb: -------------------------------------------------------------------------------- 1 | require 'graphql' 2 | require 'graphql-schema_directives' 3 | 4 | # Stitching directive definitions 5 | 6 | class MergeDirective < GraphQL::Schema::Directive 7 | graphql_name 'merge' 8 | add_argument GraphQL::Schema::Argument.new('keyField', String, required: false, owner: GraphQL::Schema) 9 | add_argument GraphQL::Schema::Argument.new('keyArg', String, required: false, owner: GraphQL::Schema) 10 | add_argument GraphQL::Schema::Argument.new('additionalArgs', String, required: false, owner: GraphQL::Schema) 11 | add_argument GraphQL::Schema::Argument.new('key', [String], required: false, owner: GraphQL::Schema) 12 | add_argument GraphQL::Schema::Argument.new('argsExpr', String, required: false, owner: GraphQL::Schema) 13 | locations 'FIELD_DEFINITION' 14 | end 15 | 16 | class KeyDirective < GraphQL::Schema::Directive 17 | graphql_name 'key' 18 | add_argument GraphQL::Schema::Argument.new('selectionSet', String, required: true, owner: GraphQL::Schema) 19 | locations 'OBJECT' 20 | end 21 | 22 | class ComputedDirective < GraphQL::Schema::Directive 23 | graphql_name 'computed' 24 | add_argument GraphQL::Schema::Argument.new('selectionSet', String, required: true, owner: GraphQL::Schema) 25 | locations 'FIELD_DEFINITION' 26 | end 27 | 28 | # GraphQL base types with schema directives 29 | 30 | class BaseField < GraphQL::Schema::Field 31 | include GraphQL::SchemaDirectives::Field 32 | end 33 | 34 | class BaseObject < GraphQL::Schema::Object 35 | include GraphQL::SchemaDirectives::Object 36 | field_class BaseField 37 | end 38 | 39 | class BaseSchema < GraphQL::Schema 40 | include GraphQL::SchemaDirectives::Schema 41 | directive(MergeDirective) 42 | directive(KeyDirective) 43 | directive(ComputedDirective) 44 | end -------------------------------------------------------------------------------- /examples/subservice-languages/ruby/lib/graphql_server.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rack' 4 | require 'json' 5 | 6 | class GraphQLServer 7 | def self.run(schema, options = {}) 8 | Rack::Handler::WEBrick.run(GraphQLServer.new(schema), **options) 9 | end 10 | 11 | attr_reader :schema 12 | 13 | def initialize(schema) 14 | @schema = schema 15 | end 16 | 17 | def call(env) 18 | req = Rack::Request.new(env) 19 | req_vars = JSON.parse(req.body.read) 20 | result = schema.execute( 21 | req_vars['query'], 22 | operation_name: req_vars['operationName'], 23 | variables: req_vars['variables'] || {}, 24 | ) 25 | ['200', { 'Content-Type' => 'application/json' }, [JSON.dump(result.to_h)]] 26 | end 27 | end -------------------------------------------------------------------------------- /examples/subservice-languages/ruby/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "subservice-languages-ruby", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "concurrently \"yarn:start-gateway\" \"yarn:start-services\"", 8 | "start-gateway": "ts-node-dev index.ts", 9 | "start-service-accounts": "nodemon -e rb --exec \"bundle exec ruby\" ./services/accounts.rb", 10 | "start-service-products": "nodemon -e rb --exec \"bundle exec ruby\" ./services/products.rb", 11 | "start-service-reviews": "nodemon -e rb --exec \"bundle exec ruby\" ./services/reviews.rb", 12 | "start-services": "concurrently \"yarn:start-service-*\"" 13 | }, 14 | "dependencies": { 15 | "@graphql-tools/executor-http": "^2.0.0", 16 | "@graphql-tools/stitch": "^9.0.0", 17 | "@graphql-tools/stitching-directives": "^3.0.0", 18 | "@graphql-tools/utils": "^10.0.0", 19 | "@types/node": "^22.0.0", 20 | "@types/wait-on": "^5.3.1", 21 | "concurrently": "^9.0.0", 22 | "graphql": "^16.6.0", 23 | "graphql-yoga": "^5.0.0", 24 | "ts-node": "^10.9.1", 25 | "ts-node-dev": "^2.0.0", 26 | "typescript": "^5.0.0", 27 | "wait-on": "^8.0.0" 28 | }, 29 | "devDependencies": { 30 | "kill-port-process": "3.2.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/subservice-languages/ruby/services/accounts.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require_relative '../lib/base_schema' 3 | require_relative '../lib/graphql_server' 4 | 5 | USERS = [ 6 | { 7 | id: '1', 8 | name: 'Ada Lovelace', 9 | username: '@ada', 10 | }, 11 | { 12 | id: '2', 13 | name: 'Alan Turing', 14 | username: '@complete', 15 | }, 16 | ].freeze 17 | 18 | class User < BaseObject 19 | add_directive :key, { selectionSet: '{ id }' } 20 | field :id, ID, null: false 21 | field :name, String, null: true 22 | field :username, String, null: true 23 | end 24 | 25 | class Query < BaseObject 26 | field :users, [User, null: true], null: false, directives: { merge: { keyField: 'id' } } do 27 | argument :ids, [ID], required: true 28 | end 29 | field :_sdl, String, null: false 30 | 31 | def users(ids:) 32 | USERS.select { |u| ids.include?(u[:id]) } 33 | end 34 | 35 | def _sdl 36 | AccountSchema.print_schema_with_directives 37 | end 38 | end 39 | 40 | class AccountSchema < BaseSchema 41 | query(Query) 42 | end 43 | 44 | GraphQLServer.run(AccountSchema, Port: 4001) -------------------------------------------------------------------------------- /examples/subservice-languages/ruby/services/products.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require_relative '../lib/base_schema' 3 | require_relative '../lib/graphql_server' 4 | 5 | schema = nil 6 | type_defs = %( 7 | directive @key(selectionSet: String) on OBJECT 8 | directive @merge(keyField: String) on FIELD_DEFINITION 9 | type Product @key(selectionSet: "{ upc }") { 10 | upc: String! 11 | name: String 12 | price: Int 13 | weight: Int 14 | } 15 | type Query { 16 | products(upcs: [ID!]!): [Product]! @merge(keyField: "upc") 17 | _sdl: String! 18 | } 19 | ) 20 | 21 | PRODUCTS = [ 22 | { 23 | upc: '1', 24 | name: 'Table', 25 | price: 899, 26 | weight: 100, 27 | }, 28 | { 29 | upc: '2', 30 | name: 'Couch', 31 | price: 1299, 32 | weight: 1000, 33 | }, 34 | { 35 | upc: '3', 36 | name: 'Chair', 37 | price: 54, 38 | weight: 50, 39 | }, 40 | ].freeze 41 | 42 | RESOLVERS = { 43 | Query: { 44 | products: ->(obj, args, ctx) { args[:upcs].map { |upc| PRODUCTS.find { |p| p[:upc] == upc } } }, 45 | _sdl: ->(obj, args, ctx) { schema.print_schema_with_directives } 46 | } 47 | }.freeze 48 | 49 | module DefaultResolver 50 | def self.call(type, field, obj, args, ctx) 51 | type_name = type.graphql_name 52 | field_name = field.name 53 | resolver = RESOLVERS.dig(type_name.to_sym, field_name.to_sym) 54 | 55 | if resolver 56 | resolver.call(obj, args, ctx) 57 | elsif obj.is_a?(Hash) 58 | obj[field_name] || obj[field_name.to_sym] 59 | end 60 | end 61 | end 62 | 63 | schema = GraphQL::SchemaDirectives.from_definition(type_defs, default_resolve: DefaultResolver) 64 | schema.directive(MergeDirective) 65 | schema.directive(KeyDirective) 66 | schema.directive(ComputedDirective) 67 | 68 | GraphQLServer.run(schema, Port: 4002) -------------------------------------------------------------------------------- /examples/subservice-languages/ruby/services/reviews.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require_relative '../lib/base_schema' 3 | require_relative '../lib/graphql_server' 4 | 5 | REVIEWS = [ 6 | { 7 | id: '1', 8 | authorId: '1', 9 | productUpc: '1', 10 | body: 'Love it!', 11 | }, 12 | { 13 | id: '2', 14 | authorId: '1', 15 | productUpc: '2', 16 | body: 'Too expensive.', 17 | }, 18 | { 19 | id: '3', 20 | authorId: '2', 21 | productUpc: '3', 22 | body: 'Could be better.', 23 | }, 24 | { 25 | id: '4', 26 | authorId: '2', 27 | productUpc: '1', 28 | body: 'Prefer something else.', 29 | }, 30 | ].freeze 31 | 32 | class Review < BaseObject 33 | add_directive :key, { selectionSet: '{ id }' } 34 | 35 | field :id, ID, null: false 36 | field :body, String, null: true 37 | field :author, 'User', null: true 38 | field :product, 'Product', null: true 39 | 40 | def author 41 | { id: object[:authorId] } 42 | end 43 | 44 | def product 45 | { upc: object[:productUpc] } 46 | end 47 | end 48 | 49 | class User < BaseObject 50 | add_directive :key, { selectionSet: '{ id }' } 51 | 52 | field :id, ID, null: false 53 | field :reviews, [Review], null: true 54 | 55 | def reviews 56 | REVIEWS.select { |review| review[:authorId] == object[:id] } 57 | end 58 | end 59 | 60 | class Product < BaseObject 61 | add_directive :key, { selectionSet: '{ upc }' } 62 | 63 | field :upc, String, null: false 64 | field :reviews, [Review], null: true 65 | 66 | def reviews 67 | REVIEWS.select { |review| review[:productUpc] == object[:upc] } 68 | end 69 | end 70 | 71 | class Query < BaseObject 72 | field :_users, [User, null: true], null: false, directives: { merge: { keyField: 'id' } } do 73 | argument :ids, [ID], required: true 74 | end 75 | field :_products, [Product, null: true], null: false, directives: { merge: { keyField: 'upc' } } do 76 | argument :upcs, [ID], required: true 77 | end 78 | field :_sdl, String, null: false 79 | 80 | def _users(ids:) 81 | ids.map { |id| { id: id } } 82 | end 83 | 84 | def _products(upcs:) 85 | upcs.map { |upc| { upc: upc } } 86 | end 87 | 88 | def _sdl 89 | ReviewSchema.print_schema_with_directives 90 | end 91 | end 92 | 93 | class ReviewSchema < BaseSchema 94 | query(Query) 95 | end 96 | 97 | GraphQLServer.run(ReviewSchema, Port: 4003) 98 | -------------------------------------------------------------------------------- /examples/subservice-languages/ruby/tests/__snapshots__/ruby.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Ruby subservices should work: result 1`] = ` 4 | { 5 | "data": { 6 | "users": [ 7 | { 8 | "id": "1", 9 | "name": "Ada Lovelace", 10 | "reviews": [ 11 | { 12 | "body": "Love it!", 13 | "product": { 14 | "name": "Table", 15 | }, 16 | }, 17 | { 18 | "body": "Too expensive.", 19 | "product": { 20 | "name": "Couch", 21 | }, 22 | }, 23 | ], 24 | "username": "@ada", 25 | }, 26 | { 27 | "id": "2", 28 | "name": "Alan Turing", 29 | "reviews": [ 30 | { 31 | "body": "Could be better.", 32 | "product": { 33 | "name": "Chair", 34 | }, 35 | }, 36 | { 37 | "body": "Prefer something else.", 38 | "product": { 39 | "name": "Table", 40 | }, 41 | }, 42 | ], 43 | "username": "@complete", 44 | }, 45 | ], 46 | }, 47 | } 48 | `; 49 | -------------------------------------------------------------------------------- /examples/subservice-languages/ruby/tests/ruby.test.ts: -------------------------------------------------------------------------------- 1 | import { ChildProcessWithoutNullStreams, execSync, spawn } from 'child_process'; 2 | import { join } from 'path'; 3 | import { killPortProcess } from 'kill-port-process'; 4 | import { gatewayApp } from '../gateway'; 5 | 6 | describe('Ruby subservices', () => { 7 | let servicesProcess: ChildProcessWithoutNullStreams; 8 | const baseDir = join(__dirname, '..'); 9 | beforeAll(async () => { 10 | execSync('bundle install', { 11 | cwd: baseDir, 12 | }); 13 | servicesProcess = spawn('npm', ['run', 'start-services'], { 14 | cwd: baseDir, 15 | }); 16 | }); 17 | afterAll(async () => { 18 | servicesProcess.stdout.destroy(); 19 | servicesProcess.stderr.destroy(); 20 | servicesProcess.kill(); 21 | await killPortProcess([4001, 4002, 4003]).catch(e => { 22 | console.error(e); 23 | }); 24 | }); 25 | it('should work', async () => { 26 | const result = await gatewayApp.fetch('/graphql', { 27 | method: 'POST', 28 | headers: { 29 | 'Content-Type': 'application/json', 30 | }, 31 | body: JSON.stringify({ 32 | query: /* GraphQL */ ` 33 | query { 34 | users(ids: ["1", "2"]) { 35 | id 36 | name 37 | username 38 | reviews { 39 | body 40 | product { 41 | name 42 | } 43 | } 44 | } 45 | } 46 | `, 47 | }), 48 | }); 49 | const json = await result.json(); 50 | expect(json).toMatchSnapshot('result'); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /examples/subservice-languages/ruby/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "lib": ["esnext", "DOM"], 10 | "skipLibCheck": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/type-merging-arrays/README.md: -------------------------------------------------------------------------------- 1 | # Array-batched type merging 2 | 3 | [See the details about this example on the website](https://the-guild.dev/graphql/stitching/handbook/foundation/type-merging-arrays) 4 | -------------------------------------------------------------------------------- /examples/type-merging-arrays/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "type-merging-arrays", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "private": "true", 6 | "scripts": { 7 | "start": "concurrently \"yarn:start-*\"", 8 | "start-gateway": "ts-node-dev src/index.ts", 9 | "start-manufacturers": "ts-node-dev services/manufacturers/index.ts", 10 | "start-products": "ts-node-dev services/products/index.ts", 11 | "start-storefronts": "ts-node-dev services/storefronts/index.ts" 12 | }, 13 | "dependencies": { 14 | "@graphql-tools/executor-http": "^2.0.0", 15 | "@graphql-tools/stitch": "^9.0.0", 16 | "@graphql-tools/wrap": "^10.0.0", 17 | "concurrently": "^9.0.0", 18 | "graphql": "^16.6.0", 19 | "graphql-yoga": "^5.0.0", 20 | "ts-node": "^10.9.1", 21 | "ts-node-dev": "^2.0.0", 22 | "typescript": "^5.0.0", 23 | "wait-on": "^8.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/type-merging-arrays/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { gatewayApp } from './gateway'; 3 | 4 | createServer(gatewayApp).listen(4000, () => 5 | console.log(`gateway running at http://localhost:4000/graphql`), 6 | ); 7 | -------------------------------------------------------------------------------- /examples/type-merging-arrays/src/services/manufacturers/index.ts: -------------------------------------------------------------------------------- 1 | import { manufacturerServer } from './server'; 2 | 3 | manufacturerServer.listen(4001, () => { 4 | console.info('Manufacturers service listening on http://localhost:4001'); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/type-merging-arrays/src/services/manufacturers/server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { GraphQLError } from 'graphql'; 3 | import { createSchema, createYoga } from 'graphql-yoga'; 4 | 5 | // data fixtures 6 | const manufacturers = [ 7 | { id: '1', name: 'Apple' }, 8 | { id: '2', name: 'Macmillan' }, 9 | ]; 10 | 11 | export const manufacturerServer = createServer( 12 | createYoga({ 13 | schema: createSchema({ 14 | typeDefs: /* GraphQL */ ` 15 | type Manufacturer { 16 | id: ID! 17 | name: String! 18 | } 19 | 20 | type Query { 21 | manufacturers(ids: [ID!]!): [Manufacturer]! 22 | } 23 | `, 24 | resolvers: { 25 | Query: { 26 | manufacturers(root, { ids }) { 27 | return ids.map( 28 | id => 29 | manufacturers.find(m => m.id === id) || 30 | new GraphQLError('Record not found', { 31 | extensions: { 32 | code: 'NOT_FOUND', 33 | }, 34 | }), 35 | ); 36 | }, 37 | }, 38 | }, 39 | }), 40 | }), 41 | ); 42 | -------------------------------------------------------------------------------- /examples/type-merging-arrays/src/services/products/index.ts: -------------------------------------------------------------------------------- 1 | import { productsServer } from './server'; 2 | 3 | productsServer.listen(4002, () => { 4 | console.info('Products service listening on http://localhost:4002'); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/type-merging-arrays/src/services/storefronts/index.ts: -------------------------------------------------------------------------------- 1 | import { storefrontsServer } from './server'; 2 | 3 | storefrontsServer.listen(4003, () => { 4 | console.info('Storefronts service listening on http://localhost:4003'); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/type-merging-arrays/src/services/storefronts/server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { GraphQLError } from 'graphql'; 3 | import { createSchema, createYoga } from 'graphql-yoga'; 4 | 5 | // data fixtures 6 | const storefronts = [ 7 | { id: '1', name: 'eShoppe', productUpcs: ['1', '2'] }, 8 | { id: '2', name: 'BestBooks Online', productUpcs: ['3', '4', '5'] }, 9 | ]; 10 | 11 | export const storefrontsServer = createServer( 12 | createYoga({ 13 | schema: createSchema({ 14 | typeDefs: /* GraphQL */ ` 15 | type Storefront { 16 | id: ID! 17 | name: String! 18 | products: [Product]! 19 | } 20 | 21 | type Product { 22 | upc: ID! 23 | } 24 | 25 | type Query { 26 | storefront(id: ID!): Storefront 27 | } 28 | `, 29 | resolvers: { 30 | Query: { 31 | storefront: (root, { id }) => 32 | storefronts.find(s => s.id === id) || 33 | new GraphQLError('Record not found', { 34 | extensions: { 35 | code: 'NOT_FOUND', 36 | }, 37 | }), 38 | }, 39 | Storefront: { 40 | products: storefront => storefront.productUpcs.map(upc => ({ upc })), 41 | }, 42 | }, 43 | }), 44 | }), 45 | ); 46 | -------------------------------------------------------------------------------- /examples/type-merging-arrays/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "lib": ["esnext", "DOM"], 10 | "skipLibCheck": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/type-merging-interfaces/README.md: -------------------------------------------------------------------------------- 1 | # Cross-service interfaces 2 | 3 | [See the details about this example on the website](https://the-guild.dev/graphql/stitching/handbook/foundation/type-merging-interfaces) 4 | -------------------------------------------------------------------------------- /examples/type-merging-interfaces/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "type-merging-interfaces", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "private": "true", 6 | "scripts": { 7 | "start": "ts-node-dev src/index.ts" 8 | }, 9 | "dependencies": { 10 | "@graphql-tools/stitch": "^9.0.0", 11 | "graphql": "^16.6.0", 12 | "graphql-yoga": "^5.0.0", 13 | "ts-node": "^10.9.1", 14 | "ts-node-dev": "^2.0.0", 15 | "typescript": "^5.0.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/type-merging-interfaces/src/gateway.ts: -------------------------------------------------------------------------------- 1 | import { createYoga } from 'graphql-yoga'; 2 | import { stitchSchemas } from '@graphql-tools/stitch'; 3 | import { productsSchema } from './services/products'; 4 | import { storefrontsSchema } from './services/storefronts'; 5 | 6 | function makeGatewaySchema() { 7 | // For simplicity, all services run locally in this example. 8 | // Any of these services could easily be turned into a remote server (see Example 1). 9 | return stitchSchemas({ 10 | subschemas: [ 11 | { 12 | schema: productsSchema, 13 | batch: true, 14 | merge: { 15 | Product: { 16 | selectionSet: '{ id }', 17 | fieldName: 'products', 18 | key: ({ id }) => id, 19 | argsFromKeys: ids => ({ ids }), 20 | }, 21 | }, 22 | }, 23 | { 24 | schema: storefrontsSchema, 25 | batch: true, 26 | }, 27 | ], 28 | }); 29 | } 30 | 31 | export const gatewayApp = createYoga({ 32 | schema: makeGatewaySchema(), 33 | maskedErrors: false, 34 | graphiql: { 35 | title: 'Cross-service interfaces', 36 | defaultQuery: /* GraphQL */ ` 37 | query { 38 | storefront(id: "1") { 39 | id 40 | name 41 | productOfferings { 42 | __typename 43 | id 44 | name 45 | price 46 | ... on ProductDeal { 47 | products { 48 | name 49 | price 50 | } 51 | } 52 | } 53 | } 54 | } 55 | `, 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /examples/type-merging-interfaces/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { gatewayApp } from './gateway'; 3 | 4 | createServer(gatewayApp).listen(4000, () => 5 | console.log(`gateway running at http://localhost:4000/graphql`), 6 | ); 7 | -------------------------------------------------------------------------------- /examples/type-merging-interfaces/src/services/products.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql'; 2 | import { createSchema } from 'graphql-yoga'; 3 | 4 | // data fixtures 5 | const products = [ 6 | { id: '1', name: 'iPhone', price: 699.99 }, 7 | { id: '2', name: 'Apple Watch', price: 399.99 }, 8 | { id: '3', name: 'Super Baking Cookbook', price: 15.99 }, 9 | { id: '4', name: 'Best Selling Novel', price: 7.99 }, 10 | { id: '5', name: 'iOS Survival Guide', price: 24.99 }, 11 | ]; 12 | 13 | export const productsSchema = createSchema({ 14 | typeDefs: /* GraphQL */ ` 15 | interface ProductOffering { 16 | id: ID! 17 | name: String! 18 | price: Float! 19 | } 20 | 21 | type Product implements ProductOffering { 22 | id: ID! 23 | name: String! 24 | price: Float! 25 | } 26 | 27 | type Query { 28 | products(ids: [ID!]!): [Product]! 29 | } 30 | `, 31 | resolvers: { 32 | Query: { 33 | products: (root, { ids }) => 34 | ids.map( 35 | (id: string) => 36 | products.find(p => p.id === id) || 37 | new GraphQLError('Record not found', { 38 | extensions: { 39 | code: 'NOT_FOUND', 40 | }, 41 | }), 42 | ), 43 | }, 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /examples/type-merging-interfaces/src/services/storefronts.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError, print } from 'graphql'; 2 | import { createSchema } from 'graphql-yoga'; 3 | 4 | // data fixtures 5 | const storefronts = [ 6 | { 7 | id: '1', 8 | name: 'eShoppe', 9 | productOfferKeys: ['Product:1', 'ProductDeal:1', 'Product:2'], 10 | }, 11 | { 12 | id: '2', 13 | name: 'BestBooks Online', 14 | productOfferKeys: ['Product:3', 'Product:4', 'ProductDeal:2', 'Product:5'], 15 | }, 16 | ]; 17 | 18 | const productDeals = [ 19 | { id: '1', name: 'iPhone + Survival Guide', price: 679.99, productIds: ['1', '5'] }, 20 | { id: '2', name: 'Best Sellers', price: 19.99, productIds: ['3', '4'] }, 21 | ]; 22 | 23 | export const storefrontsSchema = createSchema({ 24 | typeDefs: /* GraphQL */ ` 25 | interface ProductOffering { 26 | id: ID! 27 | } 28 | 29 | type Product implements ProductOffering { 30 | id: ID! 31 | } 32 | 33 | type ProductDeal implements ProductOffering { 34 | id: ID! 35 | name: String! 36 | price: Float! 37 | products: [Product]! 38 | } 39 | 40 | type Storefront { 41 | id: ID! 42 | name: String! 43 | productOfferings: [ProductOffering]! 44 | } 45 | 46 | type Query { 47 | storefront(id: ID!): Storefront 48 | } 49 | `, 50 | resolvers: { 51 | Query: { 52 | storefront: (_root, { id }) => 53 | storefronts.find(s => s.id === id) || 54 | new GraphQLError('Record not found', { 55 | extensions: { 56 | code: 'NOT_FOUND', 57 | }, 58 | }), 59 | }, 60 | Storefront: { 61 | productOfferings(storefront, _args, _ctx, info) { 62 | console.log(print(info.operation)); 63 | return storefront.productOfferKeys.map(key => { 64 | const [__typename, id] = key.split(':'); 65 | const obj = __typename === 'Product' ? { id } : productDeals.find(d => d.id === id); 66 | return obj 67 | ? { __typename, ...obj } 68 | : new GraphQLError('Record not found', { 69 | extensions: { 70 | code: 'NOT_FOUND', 71 | }, 72 | }); 73 | }); 74 | }, 75 | }, 76 | ProductDeal: { 77 | products: deal => deal.productIds.map(id => ({ id })), 78 | }, 79 | }, 80 | }); 81 | -------------------------------------------------------------------------------- /examples/type-merging-interfaces/tests/type-merging-interfaces.test.ts: -------------------------------------------------------------------------------- 1 | import { gatewayApp } from '../src/gateway'; 2 | 3 | describe('Cross-service interfaces', () => { 4 | it('should work', async () => { 5 | const response = await gatewayApp.fetch('/graphql', { 6 | method: 'POST', 7 | headers: { 8 | 'Content-Type': 'application/json', 9 | }, 10 | body: JSON.stringify({ 11 | query: /* GraphQL */ ` 12 | query { 13 | storefront(id: "1") { 14 | id 15 | name 16 | productOfferings { 17 | __typename 18 | id 19 | name 20 | price 21 | ... on ProductDeal { 22 | products { 23 | name 24 | price 25 | } 26 | } 27 | } 28 | } 29 | } 30 | `, 31 | }), 32 | }); 33 | const result = await response.json(); 34 | expect(result).toMatchInlineSnapshot(` 35 | { 36 | "data": { 37 | "storefront": { 38 | "id": "1", 39 | "name": "eShoppe", 40 | "productOfferings": [ 41 | { 42 | "__typename": "Product", 43 | "id": "1", 44 | "name": "iPhone", 45 | "price": 699.99, 46 | }, 47 | { 48 | "__typename": "ProductDeal", 49 | "id": "1", 50 | "name": "iPhone + Survival Guide", 51 | "price": 679.99, 52 | "products": [ 53 | { 54 | "name": "iPhone", 55 | "price": 699.99, 56 | }, 57 | { 58 | "name": "iOS Survival Guide", 59 | "price": 24.99, 60 | }, 61 | ], 62 | }, 63 | { 64 | "__typename": "Product", 65 | "id": "2", 66 | "name": "Apple Watch", 67 | "price": 399.99, 68 | }, 69 | ], 70 | }, 71 | }, 72 | } 73 | `); 74 | }); 75 | }); 76 | -------------------------------------------------------------------------------- /examples/type-merging-interfaces/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "lib": ["esnext", "DOM"], 10 | "skipLibCheck": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/type-merging-multiple-keys/README.md: -------------------------------------------------------------------------------- 1 | # Type merging with multiple keys 2 | 3 | [See the details about this example on the website](https://the-guild.dev/graphql/stitching/handbook/foundation/type-merging-multiple-keys) 4 | -------------------------------------------------------------------------------- /examples/type-merging-multiple-keys/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "type-merging-multiple-keys", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "private": "true", 6 | "scripts": { 7 | "start": "ts-node-dev src/index.ts" 8 | }, 9 | "dependencies": { 10 | "@graphql-tools/stitch": "^9.0.0", 11 | "graphql": "^16.6.0", 12 | "graphql-yoga": "^5.0.0", 13 | "ts-node": "^10.9.1", 14 | "ts-node-dev": "^2.0.0", 15 | "typescript": "^5.0.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/type-merging-multiple-keys/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { gatewayApp } from './gateway'; 3 | 4 | createServer(gatewayApp).listen(4000, () => 5 | console.log(`gateway running at http://localhost:4000/graphql`), 6 | ); 7 | -------------------------------------------------------------------------------- /examples/type-merging-multiple-keys/src/services/catalog.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql'; 2 | import { createSchema } from 'graphql-yoga'; 3 | 4 | const products = [ 5 | { upc: '1', name: 'Table', price: 899, weight: 100 }, 6 | { upc: '2', name: 'Couch', price: 1299, weight: 1000 }, 7 | { upc: '3', name: 'Chair', price: 54, weight: 50 }, 8 | ]; 9 | 10 | export const catalogSchema = createSchema({ 11 | typeDefs: /* GraphQL */ ` 12 | type Product { 13 | upc: ID! 14 | msrp: Int! 15 | name: String 16 | weight: Int! 17 | } 18 | 19 | type Query { 20 | productsByUpc(upcs: [ID!]!): [Product]! 21 | } 22 | `, 23 | resolvers: { 24 | Query: { 25 | productsByUpc: (_root, { upcs }) => 26 | upcs.map( 27 | (upc: string) => 28 | products.find(product => product.upc === upc) || 29 | new GraphQLError('Record not found', { 30 | extensions: { 31 | code: 'NOT_FOUND', 32 | }, 33 | }), 34 | ), 35 | }, 36 | }, 37 | }); 38 | -------------------------------------------------------------------------------- /examples/type-merging-multiple-keys/src/services/reviews.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql'; 2 | import { createSchema } from 'graphql-yoga'; 3 | 4 | const reviews = [ 5 | { id: '1', productId: '101', body: 'Love it!' }, 6 | { id: '2', productId: '102', body: 'Too expensive.' }, 7 | { id: '3', productId: '103', body: 'Could be better.' }, 8 | { id: '4', productId: '101', body: 'Prefer something else.' }, 9 | ]; 10 | 11 | export const reviewsSchema = createSchema({ 12 | typeDefs: /* GraphQL */ ` 13 | type Review { 14 | id: ID! 15 | body: String 16 | product: Product 17 | } 18 | 19 | type Product { 20 | id: ID! 21 | reviews: [Review] 22 | } 23 | 24 | type Query { 25 | review(id: ID!): Review 26 | productsById(ids: [ID!]!): [Product]! 27 | } 28 | `, 29 | resolvers: { 30 | Review: { 31 | product: review => ({ id: review.productId }), 32 | }, 33 | Product: { 34 | reviews: product => reviews.filter(review => review.productId === product.id), 35 | }, 36 | Query: { 37 | review: (_root, { id }) => 38 | reviews.find(review => review.id === id) || 39 | new GraphQLError('Record not found', { 40 | extensions: { 41 | code: 'NOT_FOUND', 42 | }, 43 | }), 44 | productsById: (_root, { ids }) => ids.map((id: string) => ({ id })), 45 | }, 46 | }, 47 | }); 48 | -------------------------------------------------------------------------------- /examples/type-merging-multiple-keys/src/services/vendors.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql'; 2 | import { createSchema } from 'graphql-yoga'; 3 | 4 | const products = [ 5 | { id: '101', upc: '1', retailPrice: 879, unitsInStock: 23 }, 6 | { id: '102', upc: '2', retailPrice: 1279, unitsInStock: 77 }, 7 | { id: '103', upc: '3', retailPrice: 54, unitsInStock: 0 }, 8 | ]; 9 | 10 | export const vendorsSchema = createSchema({ 11 | typeDefs: /* GraphQL */ ` 12 | type Product { 13 | id: ID! 14 | upc: ID! 15 | retailPrice: Int 16 | unitsInStock: Int 17 | } 18 | 19 | input ProductKey { 20 | id: ID 21 | upc: ID 22 | } 23 | 24 | type Query { 25 | productsByKey(keys: [ProductKey!]!): [Product]! 26 | } 27 | `, 28 | resolvers: { 29 | Query: { 30 | productsByKey: (_root, { keys }) => 31 | keys.map( 32 | (k: { id: string; upc: string }) => 33 | products.find(p => p.id === k.id || p.upc === k.upc) || 34 | new GraphQLError('Record not found', { 35 | extensions: { 36 | code: 'NOT_FOUND', 37 | }, 38 | }), 39 | ), 40 | }, 41 | }, 42 | }); 43 | -------------------------------------------------------------------------------- /examples/type-merging-multiple-keys/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "lib": ["esnext", "DOM"], 10 | "skipLibCheck": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/type-merging-nullables/README.md: -------------------------------------------------------------------------------- 1 | # Nullable merges 2 | 3 | [See the details about this example on the website](https://the-guild.dev/graphql/stitching/handbook/foundation/type-merging-nullables) 4 | -------------------------------------------------------------------------------- /examples/type-merging-nullables/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "type-merging-nullables", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "private": "true", 6 | "scripts": { 7 | "start": "ts-node-dev src/index.ts" 8 | }, 9 | "dependencies": { 10 | "@graphql-tools/stitch": "^9.0.0", 11 | "graphql": "^16.6.0", 12 | "graphql-yoga": "^5.0.0", 13 | "ts-node": "^10.9.1", 14 | "ts-node-dev": "^2.0.0", 15 | "typescript": "^5.0.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/type-merging-nullables/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { gatewayApp } from './gateway'; 3 | 4 | createServer(gatewayApp).listen(4000, () => 5 | console.log(`gateway running at http://localhost:4000/graphql`), 6 | ); 7 | -------------------------------------------------------------------------------- /examples/type-merging-nullables/src/services/products.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql'; 2 | import { createSchema } from 'graphql-yoga'; 3 | 4 | // data fixtures 5 | const products = [ 6 | { upc: '1', name: 'Cookbook', price: 15.99 }, 7 | { upc: '2', name: 'Toothbrush', price: 3.99 }, 8 | ]; 9 | 10 | export const productsSchema = createSchema({ 11 | typeDefs: /* GraphQL */ ` 12 | type Product { 13 | upc: ID! 14 | name: String! 15 | price: Float! 16 | } 17 | 18 | type Query { 19 | products(upcs: [ID!]!): [Product]! 20 | } 21 | `, 22 | resolvers: { 23 | Query: { 24 | products: (_root, { upcs }) => 25 | upcs.map( 26 | (upc: string) => 27 | products.find(p => p.upc === upc) || 28 | new GraphQLError('Record not found', { 29 | extensions: { 30 | code: 'NOT_FOUND', 31 | }, 32 | }), 33 | ), 34 | }, 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /examples/type-merging-nullables/src/services/users.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLError } from 'graphql'; 2 | import { createSchema } from 'graphql-yoga'; 3 | 4 | // data fixtures 5 | const users = [ 6 | { id: '1', username: 'hanshotfirst' }, 7 | { id: '2', username: 'bigvader23' }, 8 | ]; 9 | 10 | export const usersSchema = createSchema({ 11 | typeDefs: /* GraphQL */ ` 12 | type User { 13 | id: ID! 14 | username: String! 15 | } 16 | 17 | type Query { 18 | users(ids: [ID!]!): [User]! 19 | } 20 | `, 21 | resolvers: { 22 | Query: { 23 | users: (_root, { ids }) => 24 | ids.map( 25 | (id: string) => 26 | users.find(u => u.id === id) || 27 | new GraphQLError('Record not found', { 28 | extensions: { 29 | code: 'NOT_FOUND', 30 | }, 31 | }), 32 | ), 33 | }, 34 | }, 35 | }); 36 | -------------------------------------------------------------------------------- /examples/type-merging-nullables/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "lib": ["esnext", "DOM"], 10 | "skipLibCheck": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/type-merging-single-records/README.md: -------------------------------------------------------------------------------- 1 | # Single-record type merging 2 | 3 | [See the details about this example on the website](https://the-guild.dev/graphql/stitching/handbook/foundation/type-merging-single-records) 4 | -------------------------------------------------------------------------------- /examples/type-merging-single-records/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "type-merging-single-records", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "private": "true", 6 | "scripts": { 7 | "start": "concurrently \"yarn:start-*\"", 8 | "start-gateway": "ts-node-dev src/index.ts", 9 | "start-manufacturers": "ts-node-dev services/manufacturers/index.ts", 10 | "start-products": "ts-node-dev services/products/index.ts", 11 | "start-storefronts": "ts-node-dev services/storefronts/index.ts" 12 | }, 13 | "dependencies": { 14 | "@graphql-tools/executor-http": "^2.0.0", 15 | "@graphql-tools/stitch": "^9.0.0", 16 | "@graphql-tools/wrap": "^10.0.0", 17 | "concurrently": "^9.0.0", 18 | "graphql": "^16.6.0", 19 | "graphql-yoga": "^5.0.0", 20 | "ts-node": "^10.9.1", 21 | "ts-node-dev": "^2.0.0", 22 | "typescript": "^5.0.0", 23 | "wait-on": "^8.0.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/type-merging-single-records/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'node:http'; 2 | import { gatewayApp } from './gateway'; 3 | 4 | createServer(gatewayApp).listen(4000, () => { 5 | console.info('Gateway listening on http://localhost:4000'); 6 | }); 7 | -------------------------------------------------------------------------------- /examples/type-merging-single-records/src/services/manufacturers/index.ts: -------------------------------------------------------------------------------- 1 | import { manufacturerServer } from './server'; 2 | 3 | manufacturerServer.listen(4001, () => { 4 | console.info('Manufacturers service listening on http://localhost:4001'); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/type-merging-single-records/src/services/manufacturers/server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'node:http'; 2 | import { GraphQLError } from 'graphql'; 3 | import { createSchema, createYoga } from 'graphql-yoga'; 4 | 5 | // data fixtures 6 | const manufacturers = [ 7 | { id: '1', name: 'Apple' }, 8 | { id: '2', name: 'Macmillan' }, 9 | ]; 10 | 11 | export const manufacturerServer = createServer( 12 | createYoga({ 13 | schema: createSchema({ 14 | typeDefs: /* GraphQL */ ` 15 | type Manufacturer { 16 | id: ID! 17 | name: String! 18 | } 19 | 20 | type Query { 21 | manufacturer(id: ID!): Manufacturer 22 | } 23 | `, 24 | resolvers: { 25 | Query: { 26 | manufacturer: (root, { id }) => 27 | manufacturers.find(m => m.id === id) || 28 | new GraphQLError('Record not found', { 29 | extensions: { 30 | code: 'NOT_FOUND', 31 | }, 32 | }), 33 | }, 34 | }, 35 | }), 36 | }), 37 | ); 38 | -------------------------------------------------------------------------------- /examples/type-merging-single-records/src/services/products/index.ts: -------------------------------------------------------------------------------- 1 | import { productsServer } from './server'; 2 | 3 | productsServer.listen(4002, () => { 4 | console.info('Products service listening on http://localhost:4002'); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/type-merging-single-records/src/services/products/server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'node:http'; 2 | import { GraphQLError } from 'graphql'; 3 | import { createSchema, createYoga } from 'graphql-yoga'; 4 | 5 | // data fixtures 6 | const products = [ 7 | { upc: '1', name: 'iPhone', price: 699.99, manufacturerId: '1' }, 8 | { upc: '2', name: 'Apple Watch', price: 399.99, manufacturerId: '1' }, 9 | { upc: '3', name: 'Super Baking Cookbook', price: 15.99, manufacturerId: '2' }, 10 | { upc: '4', name: 'Best Selling Novel', price: 7.99, manufacturerId: '2' }, 11 | { upc: '5', name: 'iOS Survival Guide', price: 24.99, manufacturerId: '1' }, 12 | ]; 13 | 14 | export const productsServer = createServer( 15 | createYoga({ 16 | schema: createSchema({ 17 | typeDefs: /* GraphQL */ ` 18 | type Product { 19 | manufacturer: Manufacturer 20 | name: String! 21 | price: Float! 22 | upc: ID! 23 | } 24 | 25 | type Manufacturer { 26 | id: ID! 27 | products: [Product]! 28 | } 29 | 30 | type Query { 31 | product(upc: ID!): Product 32 | _manufacturer(id: ID!): Manufacturer 33 | } 34 | `, 35 | resolvers: { 36 | Query: { 37 | product: (root, { upc }) => 38 | products.find(p => p.upc === upc) || 39 | new GraphQLError('Record not found', { 40 | extensions: { 41 | code: 'NOT_FOUND', 42 | }, 43 | }), 44 | _manufacturer: (root, { id }) => ({ 45 | id, 46 | products: products.filter(p => p.manufacturerId === id), 47 | }), 48 | }, 49 | Product: { 50 | manufacturer: product => ({ id: product.manufacturerId }), 51 | }, 52 | Manufacturer: { 53 | products: manufacturer => products.filter(p => p.manufacturerId === manufacturer.id), 54 | }, 55 | }, 56 | }), 57 | }), 58 | ); 59 | -------------------------------------------------------------------------------- /examples/type-merging-single-records/src/services/storefronts/index.ts: -------------------------------------------------------------------------------- 1 | import { storefrontsServer } from './server'; 2 | 3 | storefrontsServer.listen(4003, () => { 4 | console.info('Storefronts service listening on http://localhost:4003'); 5 | }); 6 | -------------------------------------------------------------------------------- /examples/type-merging-single-records/src/services/storefronts/server.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'node:http'; 2 | import { GraphQLError } from 'graphql'; 3 | import { createSchema, createYoga } from 'graphql-yoga'; 4 | 5 | // data fixtures 6 | const storefronts = [ 7 | { id: '1', name: 'eShoppe', productUpcs: ['1', '2'] }, 8 | { id: '2', name: 'BestBooks Online', productUpcs: ['3', '4', '5'] }, 9 | ]; 10 | 11 | export const storefrontsServer = createServer( 12 | createYoga({ 13 | schema: createSchema({ 14 | typeDefs: /* GraphQL */ ` 15 | type Storefront { 16 | id: ID! 17 | name: String! 18 | products: [Product]! 19 | } 20 | 21 | type Product { 22 | upc: ID! 23 | } 24 | 25 | type Query { 26 | storefront(id: ID!): Storefront 27 | } 28 | `, 29 | resolvers: { 30 | Query: { 31 | storefront: (root, { id }) => 32 | storefronts.find(s => s.id === id) || 33 | new GraphQLError('Record not found', { 34 | extensions: { 35 | code: 'NOT_FOUND', 36 | }, 37 | }), 38 | }, 39 | Storefront: { 40 | products: storefront => storefront.productUpcs.map(upc => ({ upc })), 41 | }, 42 | }, 43 | }), 44 | }), 45 | ); 46 | -------------------------------------------------------------------------------- /examples/type-merging-single-records/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "lib": ["esnext", "DOM"], 10 | "skipLibCheck": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/versioning-schema-releases/.gitignore: -------------------------------------------------------------------------------- 1 | repo.json 2 | -------------------------------------------------------------------------------- /examples/versioning-schema-releases/README.md: -------------------------------------------------------------------------------- 1 | # Versioning schema releases 2 | 3 | [See the details about this example on the website](https://the-guild.dev/graphql/stitching/handbook/architecture/versioning-schema-releases) 4 | -------------------------------------------------------------------------------- /examples/versioning-schema-releases/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "versioning-schema-releases", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "main": "index.js", 6 | "scripts": { 7 | "start-development": "concurrently \"yarn:start-service-*\"", 8 | "start-production": "cross-env NODE_ENV=production ts-node src/index.ts", 9 | "start-service-gateway": "ts-node-dev src/index.ts", 10 | "start-service-inventory": "ts-node-dev src/services/inventory.ts", 11 | "start-service-products": "ts-node-dev services/products.ts" 12 | }, 13 | "dependencies": { 14 | "@graphql-tools/executor-http": "^2.0.0", 15 | "@graphql-tools/stitch": "^9.0.0", 16 | "@graphql-tools/stitching-directives": "^3.0.0", 17 | "@graphql-tools/utils": "^10.0.0", 18 | "@whatwg-node/fetch": "^0.10.0", 19 | "concurrently": "^9.0.0", 20 | "cross-env": "^7.0.3", 21 | "graphql": "^16.6.0", 22 | "graphql-yoga": "^5.0.0", 23 | "ts-node": "^10.9.1", 24 | "ts-node-dev": "^2.0.0", 25 | "typescript": "^5.0.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /examples/versioning-schema-releases/repo.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "owner": "your-github-name", 3 | "repo": "your-github-repo-name", 4 | "token": "keepItSecretKeepItSafe", 5 | "mainBranch": "main", 6 | "registryPath": "graphql/remote_schemas" 7 | } 8 | -------------------------------------------------------------------------------- /examples/versioning-schema-releases/src/services/inventory.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { GraphQLError } from 'graphql'; 3 | import { createSchema, createYoga } from 'graphql-yoga'; 4 | import { stitchingDirectives } from '@graphql-tools/stitching-directives'; 5 | 6 | const { stitchingDirectivesTypeDefs, stitchingDirectivesValidator } = stitchingDirectives(); 7 | 8 | const typeDefs = /* GraphQL */ ` 9 | ${stitchingDirectivesTypeDefs} 10 | type Product { 11 | upc: ID! 12 | inStock: Boolean 13 | } 14 | 15 | type Query { 16 | mostStockedProduct: Product 17 | _products(upcs: [ID!]!): [Product]! @merge(keyField: "upc") 18 | _sdl: String! 19 | } 20 | `; 21 | 22 | const inventory = [ 23 | { upc: '1', unitsInStock: 3 }, 24 | { upc: '2', unitsInStock: 0 }, 25 | { upc: '3', unitsInStock: 5 }, 26 | ]; 27 | 28 | createServer( 29 | createYoga({ 30 | schema: stitchingDirectivesValidator( 31 | createSchema({ 32 | typeDefs, 33 | resolvers: { 34 | Product: { 35 | inStock: product => product.unitsInStock > 0, 36 | }, 37 | Query: { 38 | mostStockedProduct: () => 39 | inventory.reduce( 40 | (acc, i) => (acc.unitsInStock >= i.unitsInStock ? acc : i), 41 | inventory[0], 42 | ), 43 | _products: (_root, { upcs }) => 44 | upcs.map( 45 | (upc: string) => 46 | inventory.find(i => i.upc === upc) || 47 | new GraphQLError('Record not found', { 48 | extensions: { 49 | code: 'NOT_FOUND', 50 | }, 51 | }), 52 | ), 53 | _sdl: () => typeDefs, 54 | }, 55 | }, 56 | }), 57 | ), 58 | graphiql: { 59 | title: 'Inventory service', 60 | }, 61 | }), 62 | ).listen(4001, () => { 63 | console.log('Inventory service listening on http://localhost:4001/graphql'); 64 | }); 65 | -------------------------------------------------------------------------------- /examples/versioning-schema-releases/src/services/products.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'http'; 2 | import { GraphQLError } from 'graphql'; 3 | import { createSchema, createYoga } from 'graphql-yoga'; 4 | import { stitchingDirectives } from '@graphql-tools/stitching-directives'; 5 | 6 | const { stitchingDirectivesTypeDefs, stitchingDirectivesValidator } = stitchingDirectives(); 7 | 8 | const typeDefs = /* GraphQL */ ` 9 | ${stitchingDirectivesTypeDefs} 10 | type Product { 11 | upc: ID! 12 | name: String! 13 | price: Int! 14 | weight: Int! 15 | } 16 | 17 | type Query { 18 | topProducts(first: Int = 2): [Product]! 19 | products(upcs: [ID!]!): [Product]! @merge(keyField: "upc") 20 | _sdl: String! 21 | } 22 | `; 23 | 24 | const products = [ 25 | { upc: '1', name: 'Table', price: 899, weight: 100 }, 26 | { upc: '2', name: 'Couch', price: 1299, weight: 1000 }, 27 | { upc: '3', name: 'Chair', price: 54, weight: 50 }, 28 | ]; 29 | 30 | createServer( 31 | createYoga({ 32 | schema: stitchingDirectivesValidator( 33 | createSchema({ 34 | typeDefs, 35 | resolvers: { 36 | Query: { 37 | topProducts: (_root, args) => products.slice(0, args.first), 38 | products: (_root, { upcs }) => 39 | upcs.map( 40 | (upc: string) => 41 | products.find(product => product.upc === upc) || 42 | new GraphQLError('Record not found', { 43 | extensions: { 44 | code: 'NOT_FOUND', 45 | }, 46 | }), 47 | ), 48 | _sdl: () => typeDefs, 49 | }, 50 | }, 51 | }), 52 | ), 53 | graphiql: { 54 | title: 'Products service', 55 | }, 56 | }), 57 | ).listen(4001, () => { 58 | console.log('Products service listening on http://localhost:4001/graphql'); 59 | }); 60 | -------------------------------------------------------------------------------- /examples/versioning-schema-releases/src/services/registry.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql'; 2 | import { createSchema } from 'graphql-yoga'; 3 | import { SchemaRegistry } from '../schema_registry'; 4 | 5 | export function makeRegistrySchema(registry: SchemaRegistry): GraphQLSchema { 6 | return createSchema({ 7 | typeDefs: /* GraphQL */ ` 8 | type RemoteService { 9 | name: String! 10 | url: String! 11 | } 12 | 13 | type SchemaReleaseBranch { 14 | name: String! 15 | sha: String! 16 | url: String! 17 | pullRequestUrl: String 18 | } 19 | 20 | type SchemaRelease { 21 | name: String! 22 | sha: String! 23 | } 24 | 25 | type Query { 26 | remoteServices: [RemoteService]! 27 | } 28 | 29 | type Mutation { 30 | createSchemaReleaseBranch(name: String!, message: String): SchemaReleaseBranch! 31 | updateSchemaReleaseBranch(name: String!, message: String): SchemaReleaseBranch! 32 | createOrUpdateSchemaReleaseBranch(name: String!, message: String): SchemaReleaseBranch! 33 | mergeSchemaReleaseBranch(name: String!, message: String): SchemaRelease! 34 | } 35 | `, 36 | resolvers: { 37 | Query: { 38 | remoteServices: () => registry.services, 39 | }, 40 | Mutation: { 41 | async createSchemaReleaseBranch(_root, { name, message }) { 42 | return registry.createReleaseBranch(name, message); 43 | }, 44 | async updateSchemaReleaseBranch(_root, { name, message }) { 45 | return registry.updateReleaseBranch(name, message); 46 | }, 47 | async createOrUpdateSchemaReleaseBranch(_root, { name, message }) { 48 | return registry.createOrUpdateReleaseBranch(name, message); 49 | }, 50 | async mergeSchemaReleaseBranch(_root, { name, message }) { 51 | return registry.mergeReleaseBranch(name, message); 52 | }, 53 | }, 54 | }, 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /examples/versioning-schema-releases/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "moduleResolution": "node", 5 | "module": "commonjs", 6 | "sourceMap": true, 7 | "allowSyntheticDefaultImports": true, 8 | "esModuleInterop": true, 9 | "lib": ["esnext", "DOM"], 10 | "skipLibCheck": true, 11 | "resolveJsonModule": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require('path'); 2 | const CI = !!process.env.CI; 3 | 4 | const ROOT_DIR = __dirname; 5 | 6 | module.exports = { 7 | testEnvironment: 'node', 8 | rootDir: ROOT_DIR, 9 | restoreMocks: true, 10 | reporters: ['default'], 11 | modulePathIgnorePatterns: ['dist'], 12 | collectCoverage: false, 13 | cacheDirectory: resolve(ROOT_DIR, `${CI ? '' : 'node_modules/'}.cache/jest`), 14 | transform: { 15 | '^.+\\.mjs?$': 'babel-jest', 16 | '^.+\\.ts?$': 'babel-jest', 17 | '^.+\\.js$': 'babel-jest', 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | import prettierConfig from '@theguild/prettier-config'; 2 | 3 | export default prettierConfig; 4 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>the-guild-org/shared-config:renovate"], 4 | "automerge": true, 5 | "major": { 6 | "automerge": false 7 | }, 8 | "packageRules": [ 9 | { 10 | "excludePackagePatterns": [ 11 | "@changesets/*", 12 | "typescript", 13 | "typedoc*", 14 | "^@theguild/", 15 | "@graphql-inspector/core", 16 | "next", 17 | "graphql" 18 | ], 19 | "matchPackagePatterns": ["*"], 20 | "matchUpdateTypes": ["minor", "patch"], 21 | "groupName": "all non-major dependencies", 22 | "groupSlug": "all-minor-patch" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "outDir": "dist", 5 | "baseUrl": ".", 6 | 7 | "target": "es2018", 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "lib": ["esnext"], 11 | "allowSyntheticDefaultImports": true, 12 | "esModuleInterop": true, 13 | "importHelpers": true, 14 | "resolveJsonModule": true, 15 | "sourceMap": true, 16 | "declaration": true, 17 | "downlevelIteration": true, 18 | 19 | "suppressImplicitAnyIndexErrors": true, 20 | 21 | "skipLibCheck": true, 22 | 23 | "strict": true, 24 | "noUnusedLocals": true, 25 | "noUnusedParameters": true, 26 | "noFallthroughCasesInSwitch": true, 27 | "noPropertyAccessFromIndexSignature": true 28 | }, 29 | "include": ["examples"], 30 | "exclude": ["**/node_modules", "**/dist"] 31 | } 32 | -------------------------------------------------------------------------------- /website/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/app/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /website/next-sitemap.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next-sitemap').IConfig} */ 2 | export default { 3 | siteUrl: process.env.SITE_URL || 'https://the-guild.dev/graphql/stitching', 4 | generateIndexSitemap: false, 5 | output: 'export', 6 | }; 7 | -------------------------------------------------------------------------------- /website/next.config.ts: -------------------------------------------------------------------------------- 1 | import { withGuildDocs } from '@theguild/components/next.config'; 2 | 3 | export default withGuildDocs({ 4 | output: 'export', 5 | redirects: async () => 6 | Object.entries({ 7 | '/handbook/other-integrations/federation-services': 8 | '/handbook/other-integrations/federation-to-stitching-sdl', 9 | }).map(([from, to]) => ({ 10 | destination: to, 11 | permanent: true, 12 | source: from, 13 | })), 14 | env: { 15 | SITE_URL: 'https://the-guild.dev/graphql/stitching', 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "build": "next build", 8 | "dev": "next --turbopack", 9 | "pagefind": "pagefind --site .next/server/app --output-path out/_pagefind", 10 | "postbuild": "next-sitemap && yarn pagefind", 11 | "start": "echo 'this should not be ran in production' && serve out" 12 | }, 13 | "dependencies": { 14 | "@theguild/components": "^9.2.0", 15 | "next": "^15.3.1", 16 | "next-sitemap": "^4.2.3", 17 | "react": "^19.0.0", 18 | "react-dom": "^19.0.0", 19 | "serve": "^14.2.4" 20 | }, 21 | "devDependencies": { 22 | "@theguild/tailwind-config": "0.6.3", 23 | "@types/node": "22.15.24", 24 | "@types/react": "19.1.6", 25 | "pagefind": "1.3.0", 26 | "postcss-import": "16.1.0", 27 | "postcss-lightningcss": "1.0.1", 28 | "tailwindcss": "3.4.17", 29 | "typescript": "5.8.3", 30 | "wrangler": "4.18.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /website/postcss.config.js: -------------------------------------------------------------------------------- 1 | import postcssConfig from '@theguild/tailwind-config/postcss.config'; 2 | 3 | export default postcssConfig; 4 | -------------------------------------------------------------------------------- /website/public/assets/banner-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ardatan/schema-stitching/4456bcbbacfb131e28152f143ae4381038d15c9a/website/public/assets/banner-1.jpg -------------------------------------------------------------------------------- /website/public/assets/distributed-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ardatan/schema-stitching/4456bcbbacfb131e28152f143ae4381038d15c9a/website/public/assets/distributed-graph.png -------------------------------------------------------------------------------- /website/public/assets/stitching-flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ardatan/schema-stitching/4456bcbbacfb131e28152f143ae4381038d15c9a/website/public/assets/stitching-flow.png -------------------------------------------------------------------------------- /website/src/app/[...mdxPath]/page.tsx: -------------------------------------------------------------------------------- 1 | import { NextPageProps } from '@theguild/components'; 2 | import { generateStaticParamsFor, importPage } from '@theguild/components/pages'; 3 | import { useMDXComponents } from '../../mdx-components'; 4 | import { Giscus } from '../giscus'; 5 | 6 | export const generateStaticParams = generateStaticParamsFor('mdxPath'); 7 | 8 | export async function generateMetadata(props: NextPageProps<'...mdxPath'>) { 9 | const params = await props.params; 10 | const { metadata } = await importPage(params.mdxPath); 11 | return metadata; 12 | } 13 | 14 | const Wrapper = useMDXComponents().wrapper; 15 | 16 | export default async function Page(props: NextPageProps<'...mdxPath'>) { 17 | const params = await props.params; 18 | const result = await importPage(params.mdxPath); 19 | const { default: MDXContent, toc, metadata } = result; 20 | return ( 21 | }> 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /website/src/app/giscus.tsx: -------------------------------------------------------------------------------- 1 | import { Giscus as Giscus_ } from '@theguild/components'; 2 | 3 | export const Giscus = () => { 4 | return ( 5 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /website/src/content/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: { 3 | display: 'hidden', 4 | }, 5 | docs: { 6 | type: 'page', 7 | }, 8 | handbook: { 9 | type: 'page', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /website/src/content/docs/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: 'Introduction', 3 | 'getting-started': 'Getting Started', 4 | approaches: 'Approaches', 5 | transforms: 'Transforms', 6 | }; 7 | -------------------------------------------------------------------------------- /website/src/content/docs/approaches/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: 'Overview', 3 | 'schema-extensions': 'Schema Extensions', 4 | 'type-merging': 'Type Merging', 5 | 'stitching-directives': 'Stitching Directives', 6 | }; 7 | -------------------------------------------------------------------------------- /website/src/content/docs/getting-started/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'basic-example': 'Basic Example', 3 | 'remote-subschemas': 'Remote Subschemas & Executors', 4 | 'duplicate-types': 'Duplicate Types', 5 | 'adding-transforms': 'Adding Transforms', 6 | 'error-handling': 'Error Handling', 7 | }; 8 | -------------------------------------------------------------------------------- /website/src/content/docs/getting-started/adding-transforms.mdx: -------------------------------------------------------------------------------- 1 | # Adding Transforms 2 | 3 | Another strategy to avoid conflicts while combining schemas is to modify one or more of the 4 | subschemas using [transforms](/docs/transforms). Transforming allows a schema to be groomed in such 5 | ways as adding namespaces, renaming types, or removing fields (to name a few) prior to stitching it 6 | into the combined gateway schema. These transforms should be added directly to subschema config: 7 | 8 | ```js 9 | import { FilterRootFields, RenameTypes } from '@graphql-tools/wrap' 10 | 11 | const postsSubschema = { 12 | schema: postsSchema, 13 | transforms: [ 14 | new FilterRootFields((operation, rootField) => rootField !== 'postsByUserId'), 15 | new RenameTypes(name => `Post_${name}`) 16 | ] 17 | } 18 | ``` 19 | 20 | In the example above, we transform the `postsSchema` by removing the `postsByUserId` root field and 21 | adding a `Post_` prefix to all types in the schema. These modifications will only be present in the 22 | combined gateway schema. 23 | 24 | Note that when [automatically merging types](/docs/getting-started/duplicate-types#automatic-merge), 25 | all transforms are applied _prior_ to merging. That means transformed types will merge based on 26 | their transformed names within the combined gateway schema. 27 | -------------------------------------------------------------------------------- /website/src/content/docs/getting-started/basic-example.mdx: -------------------------------------------------------------------------------- 1 | # Basic Example 2 | 3 | In this example, we'll stitch together two very simple schemas representing a system of users and 4 | posts. You can find more examples and use-cases in the [Handbook](/handbook) 5 | 6 | ```js 7 | import { makeExecutableSchema } from '@graphql-tools/schema' 8 | import { stitchSchemas } from '@graphql-tools/stitch' 9 | 10 | let postsSchema = makeExecutableSchema({ 11 | typeDefs: /* GraphQL */ ` 12 | type Post { 13 | id: ID! 14 | text: String 15 | userId: ID! 16 | } 17 | 18 | type Query { 19 | postById(id: ID!): Post 20 | postsByUserId(userId: ID!): [Post]! 21 | } 22 | `, 23 | resolvers: { 24 | // ... 25 | } 26 | }) 27 | 28 | let usersSchema = makeExecutableSchema({ 29 | typeDefs: /* GraphQL */ ` 30 | type User { 31 | id: ID! 32 | email: String 33 | } 34 | 35 | type Query { 36 | userById(id: ID!): User 37 | } 38 | `, 39 | resolvers: { 40 | // ... 41 | } 42 | }) 43 | 44 | // setup subschema configurations 45 | export const postsSubschema = { schema: postsSchema } 46 | export const usersSubschema = { schema: usersSchema } 47 | 48 | // build the combined schema 49 | export const gatewaySchema = stitchSchemas({ 50 | subschemas: [postsSubschema, usersSubschema] 51 | }) 52 | ``` 53 | 54 | This process builds two GraphQL schemas, places them each into subschema configuration wrappers 55 | (discussed below), and then passes the subschemas to `stitchSchemas` to produce one combined schema 56 | with the following root fields: 57 | 58 | ```graphql 59 | type Query { 60 | postById(id: ID!): Post 61 | postsByUserId(userId: ID!): [Post]! 62 | userById(id: ID!): User 63 | } 64 | ``` 65 | 66 | We now have a single gateway schema that allows data from either subschema to be requested in the 67 | same query. 68 | -------------------------------------------------------------------------------- /website/src/content/docs/getting-started/error-handling.mdx: -------------------------------------------------------------------------------- 1 | # Error Handling 2 | 3 | Whether you're [merging types](/docs/approaches/type-merging), using 4 | [schema extensions](/docs/approaches/schema-extensions), or simply combining schemas, any errors 5 | returned by a subschema will flow through the stitching process and report at their mapped output 6 | positions. It's fairly seamless to provide quality errors from a stitched schema by following some 7 | basic guidelines: 8 | 9 | 1. **Report errors!** Having a subschema return `null` without an error for missing or failed 10 | records is a poor development experience, to begin with. This omission will compound should an 11 | unexpected value produce a misleading failure in gateway stitching. Reporting 12 | [proper GraphQL errors](https://spec.graphql.org/June2018/#sec-Errors) will contextualize 13 | failures in subschemas, and by extension, within the stitched schema. 14 | 15 | 1. **Map errors to array positions**. When returning arrays of records (a common pattern while 16 | [batch loading](/docs/approaches/type-merging#batching)), make sure to return errors for specific 17 | array positions rather than erroring out the entire array. For example, an array should be 18 | resolved as: 19 | 20 | ```js 21 | posts() { 22 | return [ 23 | { id: '1', ... }, 24 | new GraphQLError('Record not found', { 25 | extensions: { 26 | code: 'NOT_FOUND', 27 | }, 28 | }), 29 | { id: '3', ... } 30 | ]; 31 | } 32 | ``` 33 | 34 | 1. **Assure valid error paths**. The 35 | [GraphQL errors spec](https://spec.graphql.org/June2018/#sec-Errors) prescribes a `path` 36 | attribute mapping an error to its corresponding document position. Stitching uses these paths to 37 | remap subschema errors into the combined result. While GraphQL libraries should automatically 38 | configure this `path` for you, the accuracy 39 | [may vary by programming language](https://github.com/rmosolgo/graphql-ruby/issues/3193). 40 | -------------------------------------------------------------------------------- /website/src/content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ![distributed service graph](/assets/distributed-graph.png) 4 | 5 | Schema stitching (`@graphql-tools/stitch`) creates a single GraphQL gateway schema from multiple 6 | underlying GraphQL services. Unlike 7 | [schema merging](https://the-guild.dev/graphql/tools/docs/schema-merging), which simply combines 8 | local schema instances, stitching builds a combined proxy layer that delegates requests through to 9 | underlying service APIs. As of GraphQL Tools v7, stitching is fairly comparable to 10 | [Apollo Federation](https://www.apollographql.com/docs/federation/) with automated query planning, 11 | merged types, and declarative schema directives. 12 | 13 | Schema stitching uses [schema delegation](/docs/approaches/schema-extensions#schema-delegation) 14 | instead of the regular local schema execution using resolvers. 15 | 16 | ## Why Stitching? 17 | 18 | One of the main benefits of GraphQL is that we can query for all data in a single request to one 19 | schema. As that schema grows though, it may become preferable to break it up into separate modules 20 | or microservices that can be developed independently. We may also want to integrate the schemas we 21 | own with third-party schemas, allowing mashups with external data. 22 | 23 | In these cases, `stitchSchemas` is used to combine multiple GraphQL APIs into one unified gateway 24 | proxy schema that knows how to delegate parts of a request to the relevant underlying subschemas. 25 | These subschemas may be local GraphQL instances or APIs running on remote servers. 26 | 27 | ## How to stitch? 28 | 29 | There are three different approaches of stitching your schemas; 30 | 31 | - [Schema Extensions](/docs/approaches/schema-extensions) 32 | - [Programmatic Type Merging](/docs/approaches/type-merging) 33 | - [Directive-based Type Merging](/docs/approaches/stitching-directives) 34 | 35 | [Learn more about the differences between them](/docs/approaches) 36 | -------------------------------------------------------------------------------- /website/src/content/docs/transforms/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: 'Overview', 3 | filtering: 'Filtering', 4 | renaming: 'Renaming', 5 | modifying: 'Modifying', 6 | cleanup: 'Cleanup', 7 | operational: 'Operational', 8 | 'custom-transforms': 'Custom Transforms', 9 | }; 10 | -------------------------------------------------------------------------------- /website/src/content/docs/transforms/modifying.mdx: -------------------------------------------------------------------------------- 1 | # Modifying 2 | 3 | Modifying transforms allow element names and their definitions to be modified or omitted. They may 4 | filter, rename, and make other freeform modifications all at once. These transforms accept element 5 | transformer functions that may return one of several outcomes: 6 | 7 | 1. A modified version of the element config. 8 | 1. An array with a modified field name and new element config. 9 | 1. `null` to omit the element from the schema. 10 | 1. `undefined` to leave the element unchanged. 11 | 12 | Available transforms include: 13 | 14 | - `TransformRootFields`: redefines fields on the root Query, Mutation, and Subscription objects. 15 | - `TransformObjectFields`: redefines fields of Object types. 16 | - `TransformInterfaceFields`: redefines fields of Interface types. 17 | - `TransformCompositeFields`: redefines composite fields. 18 | - `TransformInputObjectFields`: redefines fields of InputObject types. 19 | - `TransformEnumValues`: redefines values of Enum types. 20 | 21 | ```js 22 | import { stitchSchemas } from '@graphql-tools/stitch' 23 | import { 24 | TransformCompositeFields, 25 | TransformEnumValues, 26 | TransformInputObjectFields, 27 | TransformInterfaceFields, 28 | TransformObjectFields, 29 | TransformRootFields 30 | } from '@graphql-tools/wrap' 31 | 32 | const subschema = { 33 | schema: originalSchema, 34 | transforms: [ 35 | new TransformRootFields((operationName, fieldName, fieldConfig) => fieldConfig), 36 | new TransformObjectFields((typeName, fieldName, fieldConfig) => [ 37 | `new_${fieldName}`, 38 | fieldConfig 39 | ]), 40 | new TransformInterfaceFields((typeName, fieldName, fieldConfig) => null), 41 | new TransformCompositeFields((typeName, fieldName, fieldConfig) => undefined), 42 | new TransformInputObjectFields((typeName, fieldName, inputFieldConfig) => [ 43 | `new_${fieldName}`, 44 | inputFieldConfig 45 | ]), 46 | new TransformEnumValues((typeName, enumValue, enumValueConfig) => [ 47 | `NEW_${enumValue}`, 48 | enumValueConfig 49 | ]) 50 | ] 51 | } 52 | 53 | const gateway = stitchSchemas({ 54 | subschemas: [subschema] 55 | }) 56 | ``` 57 | 58 | These transforms accept an optional second node transformer function. When specified, the node 59 | transformer is called upon any element of the given kind in a request; transforming the result is 60 | possible by wrapping the element's resolver with the element transformer function (first argument). 61 | -------------------------------------------------------------------------------- /website/src/content/handbook/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | index: 'Table of Contents', 3 | foundation: 'Foundation', 4 | architecture: 'Architecture', 5 | 'other-integrations': 'Other integrations', 6 | appendices: 'Appendices', 7 | }; 8 | -------------------------------------------------------------------------------- /website/src/content/handbook/appendices/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'batching-arrays-and-queries': 'Batching Arrays and Queries', 3 | }; 4 | -------------------------------------------------------------------------------- /website/src/content/handbook/architecture/_meta.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'hot-schema-reloading': 'Hot schema reloading', 3 | 'versioning-schema-releases': 'Versioning schema releases', 4 | 'continuous-integration-testing': 'Continuous Integration (CI) testing', 5 | 'public-and-private-apis': 'Public and private APIs', 6 | 'persistent-connection': 'Persistent connection via WebSockets', 7 | }; 8 | -------------------------------------------------------------------------------- /website/src/content/handbook/architecture/persistent-connection.mdx: -------------------------------------------------------------------------------- 1 | # Persistent connection via WebSockets 2 | 3 | This example demonstrates how to use 4 | [GraphQL over WebSocket Protocol](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) 5 | to establish a persistent connection between services and a gateway. So the connection between the 6 | gateway and the service not only exists during the request-response cycle, but also during the 7 | entire lifetime of the gateway. 8 | 9 | ## Sandbox 10 | 11 | _⬇️ Click ☰ to see the files_ 12 | 13 |