├── .circleci └── config.yml ├── .eslintrc.js ├── .gitattributes ├── .github ├── renovate.json5 └── workflows │ ├── changelog.yaml │ ├── deploy.yaml │ └── tests.yaml ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── CODEOWNERS ├── LICENSE ├── README.md ├── apollo.config.js ├── codegen.yml ├── docs ├── .gitignore ├── 404.html ├── Gemfile ├── Gemfile.lock ├── _config.yml ├── _posts │ └── 2023-10-15-welcome-to-jekyll.markdown ├── images │ ├── add-api-key.png │ ├── composition-error.png │ ├── custom-mocks-options.png │ ├── design-from-graphos-graph.png │ ├── new-design.gif │ ├── new-operation.png │ ├── run-mocks-locally.png │ ├── studio-operations.png │ ├── view-query-plan.png │ └── view-supergraphSdl.png ├── index.markdown └── mocking.md ├── graphos-schema.graphql ├── images ├── add-operations.png ├── composition-errors.png ├── extension-output.png ├── graphos-logged-in.png ├── graphos-login.png ├── graphos-name-new-design.png ├── graphos-named-new-design.png ├── graphos-new-design.png ├── local-edit-remote-schema.png ├── local-editing-remote-schema.png ├── local-new.png ├── mock-subgraph.png ├── operation-in-explorer.png └── rover-dev.png ├── media ├── graphql-logo.png ├── logo-apollo.png ├── logo-apollo.svg ├── m.png ├── m.svg ├── preloaded-files │ ├── Retail-Supergraph-schemas │ │ ├── discovery.graphql │ │ ├── inventory.graphql │ │ ├── orders.graphql │ │ ├── products.graphql │ │ ├── reviews.graphql │ │ ├── shipping.graphql │ │ └── users.graphql │ ├── Retail-Supergraph.yaml │ ├── Spotify-Showcase-schemas │ │ ├── playback.graphql │ │ └── spotify.graphql │ ├── Spotify-Showcase.yaml │ ├── customMocks.txt │ └── router.yaml ├── q.png ├── q.svg ├── subgraph.png ├── subgraph.svg ├── supergraph.svg ├── versions.svg ├── w.svg ├── workbench.png ├── workbench.svg ├── worktools.png └── worktools.svg ├── package-lock.json ├── package.json ├── src ├── __tests__ │ ├── githubCheckTests.ts │ ├── runTest.ts │ ├── suite │ │ ├── defaults.test.ts │ │ ├── extension.test.ts │ │ ├── helpers.ts │ │ ├── index.ts │ │ └── noFolder.test.ts │ ├── test-workbench │ │ └── index.ts │ ├── testRunner.ts │ └── testsNoStatus.ts ├── _generated_ │ └── typed-document-nodes.ts ├── commands │ ├── extension.ts │ ├── local-supergraph-designs.ts │ ├── preloaded.ts │ ├── studio-graphs.ts │ └── studio-operations.ts ├── extension.ts ├── graphql │ ├── graphClient.ts │ ├── queries │ │ ├── AccountServiceVariants.graphql │ │ ├── CheckUserApiKey.graphql │ │ ├── GetGraphSchemas.graphql │ │ └── UserMemberships.graphql │ └── types │ │ ├── GraphOperations.ts │ │ └── globalTypes.ts ├── utils │ ├── logger.ts │ ├── path.ts │ ├── subgraphWatcher.ts │ ├── uiHelpers.ts │ └── uri.ts └── workbench │ ├── diagnosticsManager.ts │ ├── docProviders.ts │ ├── federationCodeActionProvider.ts │ ├── federationCompletionProvider.ts │ ├── federationReferenceProvider.ts │ ├── file-system │ ├── ApolloConfig.ts │ ├── CompositionResults.ts │ └── fileProvider.ts │ ├── rover.ts │ ├── stateManager.ts │ ├── tree-data-providers │ ├── apolloStudioGraphOpsTreeDataProvider.ts │ ├── apolloStudioGraphsTreeDataProvider.ts │ ├── superGraphTreeDataProvider.ts │ └── tree-items │ │ ├── graphos-operations │ │ └── studioOperationTreeItem.ts │ │ ├── graphos-supergraphs │ │ ├── notLoggedInTreeItem.ts │ │ ├── preloadedSubgraph.ts │ │ ├── preloadedWorkbenchFile.ts │ │ ├── preloadedWorkbenchTopLevel.ts │ │ ├── signupTreeItem.ts │ │ ├── studioAccountTreeItem.ts │ │ ├── studioGraphTreeItem.ts │ │ ├── studioGraphVariantServiceTreeItem.ts │ │ └── studioGraphVariantTreeItem.ts │ │ └── local-supergraph-designs │ │ ├── addDesignOperationTreeItem.ts │ │ ├── federationVersionItem.ts │ │ ├── operationSummaryTreeItem.ts │ │ ├── operationTreeItem.ts │ │ ├── subgraphSummaryTreeItem.ts │ │ ├── subgraphTreeItem.ts │ │ └── supergraphTreeItem.ts │ └── webviews │ ├── operationDesign.ts │ └── sandbox.ts └── tsconfig.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | secops: apollo/circleci-secops-orb@2.0.6 5 | 6 | workflows: 7 | security-scans: 8 | jobs: 9 | - secops/gitleaks: 10 | context: 11 | - platform-docker-ro 12 | - github-orb 13 | - secops-oidc 14 | git-base-revision: <<#pipeline.git.base_revision>><><> 15 | git-revision: << pipeline.git.revision >> 16 | - secops/semgrep: 17 | context: 18 | - secops-oidc 19 | - github-orb 20 | git-base-revision: <<#pipeline.git.base_revision>><><> 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /**@type {import('eslint').Linter.Config} */ 2 | // eslint-disable-next-line no-undef 3 | module.exports = { 4 | root: true, 5 | parser: '@typescript-eslint/parser', 6 | plugins: [ 7 | '@typescript-eslint', 8 | ], 9 | extends: [ 10 | 'eslint:recommended', 11 | 'plugin:@typescript-eslint/recommended', 12 | ], 13 | rules: { 14 | 'semi': [2, "always"], 15 | '@typescript-eslint/no-unused-vars': 0, 16 | '@typescript-eslint/no-explicit-any': 0, 17 | '@typescript-eslint/explicit-module-boundary-types': 0, 18 | '@typescript-eslint/no-non-null-assertion': 0, 19 | } 20 | }; -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/changelog.yaml: -------------------------------------------------------------------------------- 1 | name: Check Changelog 2 | on: 3 | pull_request: 4 | types: [assigned, opened, synchronize, reopened, labeled, unlabeled] 5 | branches: 6 | - main 7 | jobs: 8 | Check-Changelog: 9 | name: Check Changelog Action 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - uses: tarides/changelog-check-action@v2 13 | with: 14 | changelog: CHANGELOG.md -------------------------------------------------------------------------------- /.github/workflows/deploy.yaml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the main branch 8 | push: 9 | branches: [main] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | # This workflow contains a single job called "build" 17 | build: 18 | strategy: 19 | matrix: 20 | os: [ubuntu-latest] 21 | runs-on: ${{ matrix.os }} 22 | # Steps represent a sequence of tasks that will be executed as part of the job 23 | steps: 24 | - uses: actions/checkout@v4 25 | - uses: actions/setup-node@v4 26 | with: 27 | node-version: '20' 28 | check-latest: true 29 | #Bump version 30 | - name: 'Automated Version Bump' 31 | id: version-bump 32 | uses: 'phips28/gh-action-bump-version@master' 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} 35 | 36 | #Setup project and package extension 37 | - run: npm install 38 | - run: npm install vsce -g 39 | - run: npm run vscode:package 40 | 41 | #Create new release 42 | - name: Create Release 43 | id: create_release 44 | uses: actions/create-release@v1 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | with: 48 | tag_name: ${{ steps.version-bump.outputs.newTag }} 49 | release_name: Release ${{ steps.version-bump.outputs.newTag }} 50 | draft: false 51 | prerelease: false 52 | 53 | #Upload VSIX to release 54 | - name: Upload Release Asset 55 | id: upload-release-asset 56 | uses: actions/upload-release-asset@v1 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | with: 60 | upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps 61 | asset_path: ./apollo-workbench-${{ steps.version-bump.outputs.newTag }}.vsix 62 | asset_name: apollo-workbench-vscode-${{ steps.version-bump.outputs.newTag }}.vsix 63 | asset_content_type: application/vsix 64 | 65 | #Deploy VSIX to Marketplace 66 | - name: Publish to Visual Studio Marketplace 67 | uses: HaaLeo/publish-vscode-extension@v1 68 | with: 69 | pat: ${{ secrets.VS_MARKETPLACE_TOKEN }} 70 | registryUrl: https://marketplace.visualstudio.com 71 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: CI Tests 2 | 3 | on: 4 | pull_request: 5 | types: [assigned, opened, synchronize, reopened, pull_request_review] 6 | 7 | # Allows you to run this workflow manually from the Actions tab 8 | workflow_dispatch: 9 | 10 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 11 | jobs: 12 | # This workflow contains a single job called "build" 13 | build: 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, windows-latest] 17 | runs-on: ${{ matrix.os }} 18 | # Steps represent a sequence of tasks that will be executed as part of the job 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: '20' 24 | check-latest: true 25 | #Checkout Source Code 26 | - name: 'Checkout source code' 27 | uses: 'actions/checkout@v4' 28 | with: 29 | ref: ${{ github.ref }} 30 | 31 | #Run project tests 32 | # - run: npm install 33 | # - run: xvfb-run -a npm run test:ci 34 | # env: 35 | # GITHUB_SHA: ${{ github.sha }} 36 | # GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} 37 | # if: runner.os == 'Linux' 38 | # - run: npm run test:ci 39 | # env: 40 | # GITHUB_SHA: ${{ github.sha }} 41 | # GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} 42 | # if: runner.os != 'Linux' 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | .DS_Store 107 | 108 | # VS Code Extension files 109 | out 110 | 111 | .vscode-test/ 112 | 113 | media/temp/* 114 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. 3 | // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp 4 | 5 | // List of extensions which should be recommended for users of this workspace. 6 | "recommendations": [ 7 | "esbenp.prettier-vscode" 8 | ], 9 | // List of extensions recommended by VS Code that should not be recommended for users of this workspace. 10 | "unwantedRecommendations": [ 11 | 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 14 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 15 | "preLaunchTask": "npm: esbuild" 16 | }, 17 | { 18 | "name": "Test - CI", 19 | "program": "${workspaceFolder}/src/__tests__/githubCheckTests.ts", 20 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 21 | "request": "launch", 22 | "skipFiles": ["/**"], 23 | "type": "node", 24 | "runtimeVersion": "14.10.1", 25 | "env": { 26 | "GITHUB_SHA": "", 27 | "GITHUB_TOKEN": "" 28 | } 29 | }, 30 | { 31 | "name": "Test - Load Folder", 32 | "type": "extensionHost", 33 | "request": "launch", 34 | "runtimeExecutable": "${execPath}", 35 | "args": [ 36 | "${workspaceFolder}/out/__tests__/test-workbench", 37 | "--extensionDevelopmentPath=${workspaceFolder}", 38 | "--extensionTestsPath=${workspaceFolder}/out/__tests__/suite/index", 39 | "--disable-extensions" 40 | ], 41 | "env": { 42 | "loadWorkbench": "loadWorkbench" 43 | }, 44 | "outFiles": ["${workspaceFolder}/out/__tests__/**/*.js"], 45 | "preLaunchTask": "npm: compile" 46 | }, 47 | { 48 | "name": "Test - No Folder", 49 | "type": "extensionHost", 50 | "request": "launch", 51 | "runtimeExecutable": "${execPath}", 52 | "args": [ 53 | "--extensionDevelopmentPath=${workspaceFolder}", 54 | "--extensionTestsPath=${workspaceFolder}/out/__tests__/suite/index", 55 | "--disable-extensions" 56 | ], 57 | "outFiles": ["${workspaceFolder}/out/__tests__/**/*.js"], 58 | "preLaunchTask": "npm: compile" 59 | } 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "svg.preview.background": "dark-transparent" 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | **/*.ts 2 | **/tsconfig.json 3 | !file.ts 4 | graphos-schema.graphql 5 | **/*.vsix 6 | .env 7 | *.vsix 8 | apollo.config.js 9 | .vscode 10 | .github 11 | .gitignore 12 | docs 13 | images 14 | 15 | .vscode 16 | node_modules/!(@faker-js)/**/* 17 | src/ 18 | tsconfig.json 19 | webpack.config.js 20 | 21 | 22 | apollo.config.js 23 | **/*.zip 24 | codegen.yml 25 | target 26 | src/__tests__ 27 | **/*.map 28 | **/*.cjs -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by the Apollo SecOps team 2 | # Please customize this file as needed prior to merging. 3 | 4 | * @michael-watson 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Apollo Graph, Inc. 2 | 3 | Source code in this repository is covered by the Elastic License 2.0 license. 4 | 5 | -------------------------------------------------------------------------------- 6 | 7 | Elastic License 2.0 8 | 9 | ## Acceptance 10 | 11 | By using the software, you agree to all of the terms and conditions below. 12 | 13 | ## Copyright License 14 | 15 | The licensor grants you a non-exclusive, royalty-free, worldwide, 16 | non-sublicensable, non-transferable license to use, copy, distribute, make 17 | available, and prepare derivative works of the software, in each case subject to 18 | the limitations and conditions below. 19 | 20 | ## Limitations 21 | 22 | You may not provide the software to third parties as a hosted or managed 23 | service, where the service provides users with access to any substantial set of 24 | the features or functionality of the software. 25 | 26 | You may not move, change, disable, or circumvent the license key functionality 27 | in the software, and you may not remove or obscure any functionality in the 28 | software that is protected by the license key. 29 | 30 | You may not alter, remove, or obscure any licensing, copyright, or other notices 31 | of the licensor in the software. Any use of the licensor’s trademarks is subject 32 | to applicable law. 33 | 34 | ## Patents 35 | 36 | The licensor grants you a license, under any patent claims the licensor can 37 | license, or becomes able to license, to make, have made, use, sell, offer for 38 | sale, import and have imported the software, in each case subject to the 39 | limitations and conditions in this license. This license does not cover any 40 | patent claims that you cause to be infringed by modifications or additions to 41 | the software. If you or your company make any written claim that the software 42 | infringes or contributes to infringement of any patent, your patent license for 43 | the software granted under these terms ends immediately. If your company makes 44 | such a claim, your patent license ends immediately for work on behalf of your 45 | company. 46 | 47 | ## Notices 48 | 49 | You must ensure that anyone who gets a copy of any part of the software from you 50 | also gets a copy of these terms. 51 | 52 | If you modify the software, you must include in any modified copies of the 53 | software prominent notices stating that you have modified the software. 54 | 55 | ## No Other Rights 56 | 57 | These terms do not imply any licenses other than those expressly granted in 58 | these terms. 59 | 60 | ## Termination 61 | 62 | If you use the software in violation of these terms, such use is not licensed, 63 | and your licenses will automatically terminate. If the licensor provides you 64 | with a notice of your violation, and you cease all violation of this license no 65 | later than 30 days after you receive that notice, your licenses will be 66 | reinstated retroactively. However, if you violate these terms after such 67 | reinstatement, any additional violation of these terms will cause your licenses 68 | to terminate automatically and permanently. 69 | 70 | ## No Liability 71 | 72 | *As far as the law allows, the software comes as is, without any warranty or 73 | condition, and the licensor will not be liable to you for any damages arising 74 | out of these terms or the use or nature of the software, under any kind of 75 | legal claim.* 76 | 77 | ## Definitions 78 | 79 | The **licensor** is the entity offering these terms, and the **software** is the 80 | software the licensor makes available under these terms, including any portion 81 | of it. 82 | 83 | **you** refers to the individual or entity agreeing to these terms. 84 | 85 | **your company** is any legal entity, sole proprietorship, or other kind of 86 | organization that you work for, plus all organizations that have control over, 87 | are under the control of, or are under common control with that 88 | organization. **control** means ownership of substantially all the assets of an 89 | entity, or the power to direct its management and policies by vote, contract, or 90 | otherwise. Control can be direct or indirect. 91 | 92 | **your licenses** are all the licenses granted to you for the software under 93 | these terms. 94 | 95 | **use** means anything you do with the software requiring one of your licenses. 96 | 97 | **trademark** means trademarks, service marks, and similar rights. 98 | 99 | -------------------------------------------------------------------------------- -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apollo Workbench for VS Code (DEPRECATED) 2 | 3 | Apollo Workbench has been officially deprecated and is no longer being maintained. Please use the new Apollo Language Server for Apollo Federation, which can be used in the Apollo VS Code Extension (or other IDE) - ([docs](https://www.apollographql.com/docs/graphos/schema-design/federated-schemas/ide-support)) 4 | 5 | The Apollo Workbench extension lets you design your schemas using [Apollo Federation](https://www.apollographql.com/docs/federation) and Schema Definition Language (SDL). This extension exposes the functionality of [`rover`](https://www.apollographql.com/docs/rover/) for GraphOS in a friendly VS Code way. 6 | 7 | For more detailed documentation, visit our GitHub [docs page](https://apollographql.github.io/apollo-workbench-vscode/). 8 | 9 | ## Required Installations 10 | 11 | **rover** - This extension requires the GraphOS CLI `rover` to be installed. Most of the functionality in the extension is based on various `rover` commands (like `rover supergraph compose`). There are [simple installation instructions](https://www.apollographql.com/docs/rover/getting-started) for various platforms to install `rover`. 12 | 13 | ## Getting Started 14 | 15 | ### Logging into GraphOS 16 | 17 | 1. Head to your personal settings in Apollo Studio and create a user API key 18 | 2. Click the login row or run the **GraphOS: Login** command form the VS Code command Pallete 19 | 20 | ![](images/graphos-login.png) 21 | 22 | 3. Paste your API key and hit 'Enter'. You should see a list of your graphs show up 23 | 24 | ![](images/graphos-logged-in.png) 25 | 26 | ### Creating a new design from GraphOS 27 | 28 | _You must login to GraphOS to create designs from the schema registry_ 29 | 30 | 1. Find the graph you want to create a design from. You can right click the design and go through a wizard to select with environment (variant) to start from or expand the graph and right click on the environment: 31 | 32 | ![](images/graphos-new-design.png) 33 | 34 | 2. Provide a name for your new design: 35 | 36 | ![](images/graphos-name-new-design.png) 37 | 38 | A new design should be displayed that is a copy from GraphOS. You should see a local `.yaml` file created that can be used with `rover supergraph compose` (assuming you are logged in with `rover`). Here is an example of the `yaml` that was output from the screenshot above: 39 | 40 | ``` 41 | subgraphs: 42 | users: 43 | routing_url: https://hts-users-production.up.railway.app/ 44 | schema: 45 | graphref: hack-the-e-commerce@main 46 | subgraph: users 47 | shipping: 48 | routing_url: https://hts-shipping-production.up.railway.app/ 49 | schema: 50 | graphref: hack-the-e-commerce@main 51 | subgraph: shipping 52 | reviews: 53 | routing_url: https://hts-reviews-production.up.railway.app/ 54 | schema: 55 | graphref: hack-the-e-commerce@main 56 | subgraph: reviews 57 | products: 58 | routing_url: https://hts-products-production.up.railway.app/ 59 | schema: 60 | graphref: hack-the-e-commerce@main 61 | subgraph: products 62 | orders: 63 | routing_url: https://hts-orders-production.up.railway.app/ 64 | schema: 65 | graphref: hack-the-e-commerce@main 66 | subgraph: orders 67 | operations: {} 68 | federation_version: '=2.7.2' 69 | ``` 70 | 71 | ### Creating a design from scratch 72 | 73 | If you don't already have a schema in GraphOS or just want to play around, click the **+** button in the local supergraph designs section to create a blank `.yaml` file locally: 74 | 75 | ![](images/local-new.png) 76 | 77 | ### Changing the federation version for a design 78 | 79 | Since each design is just the `yaml` configuration required for `rover supergraph compose`, you can change the version in the file directly: 80 | 81 | ``` 82 | federation_version: '=2.3.5' 83 | ``` 84 | 85 | ### Editing a subgraph schema 86 | 87 | Depending on what you have in your `yaml` configuration, `rover` could be fetching the schema from a remote source or a local schema file (see `rover` [docs](https://www.apollographql.com/docs/rover/commands/supergraphs#composing-a-supergraph-schema) for details on sources). All remote data sources are initially read-only until you switch them to a local design: 88 | 89 | ![](images/local-edit-remote-schema.png) 90 | 91 | Once the schema has been selected to be changed, a local file will be created with that schema and a `workbench_design` will be added to your `yaml`. Having `workbench_design` present in the schema configuraiton will begin using the local file for `rover supergraph compose`: 92 | 93 | ![](images/local-editing-remote-schema.png) 94 | 95 | ### Composing changes to designs 96 | 97 | Once you save a schema file, workbench will run `rover supergraph compose` across your designs and display any errors into the problems panel: 98 | 99 | ![](images/composition-errors.png) 100 | 101 | ### Running designs locally with `rover dev` 102 | 103 | You can start any design (that doesn't have composition errors) by pressing the "Play" icon when hovering over the subgraphs row. This will manage the `rover dev` session in VS Code terminal windows and open Sandbox in VS Code. You can also navigate to `http://localhost:3000` in your browser; VS Code webview is sometimes a little weird with the embedded UIs. 104 | 105 | ![](images/rover-dev.png) 106 | 107 | You can stop the design at anytime by pressing the "Stop" icon when hovering over the subgraphs row. Only one design can be running at a time. 108 | 109 | ### Mocking designs locally 110 | 111 | By default, designs running locally will route traffic to the `routing_url` defined in the `yaml`. If you want to mock any schema, you can right click that schema and select "Mock Subgraph in Design": 112 | 113 | ![](images/mock-subgraph.png) 114 | 115 | This will add a `file` property to your schema in the `yaml` file. If there isn't already a local `workbench_design` or a `file` already defined, a copy of the schema file will be created locally for that reference. Sometimes you may have both a `file` and `workbench_design` defined; for these cases you should always have them pointing at the same schema file. 116 | 117 | The following configuration has a local design file being used for composition and to be mocked locally. 118 | 119 | ``` 120 | subgraphs: 121 | users: 122 | routing_url: https://hts-users-production.up.railway.app/ 123 | schema: 124 | graphref: hack-the-e-commerce@main 125 | subgraph: users 126 | workbench_design: >- 127 | /Users/michaelwatson/Documents/Apollo/devrel/question-of-the-week/wb-docs/users.graphql 128 | file: >- 129 | /Users/michaelwatson/Documents/Apollo/devrel/question-of-the-week/wb-docs/users.graphql 130 | ``` 131 | 132 | **How the mocking works**: Workbench runs an `@apollo/server` instance locally for every subgraph schema you have mocked. A local schema file is required as part of this process to use `graphql-tools` to apply mocks locally. In this current beta there is no support for custom mocks, but that could change in the future. 133 | 134 | ### Checking subgraph schema changes 135 | 136 | You can right click any subgraph schema and select "Check Subgraph Schema". This will run a wizard to get the necessary information to run `rover subgraph check` ([docs](https://www.apollographql.com/docs/rover/commands/subgraphs#checking-subgraph-schema-changes)) and report the results. Once complete, VS Code will prompty you to open the check report directly in Apollo Studio, the web interface for GraphOS. 137 | 138 | ### Associating an operation design with the Design 139 | 140 | You can add operations to any design by pressing the "**+**" button on the operations row: 141 | 142 | ![](images/add-operations.png) 143 | 144 | You can associate an image with your design now. Local files and urls are both supported. This enables you to pull up a query and what it's design looks like side by side. 145 | 146 | ### Adding an operation from GraphOS into the design 147 | 148 | You can load operations from GraphOS by clicking on any graph in the "GRAPHOS SUPERGRAPHS" section of the extension. Then you can select the "**+**" button for that operation in "GRAPHOS OPERATIONS" and select which design to add it to. 149 | 150 | ### Opening a design in Apollo Explorer 151 | 152 | There is a "Play" button for every defined operaiton. This will ensure the design is running locally and open Apollo Explorer embedded in VS Code with the given operation loaded in. After executing the operation once, you should be able to view the query plan. 153 | 154 | ![](images/operation-in-explorer.png) 155 | 156 | ## VS Code extension output 157 | 158 | You might be seeing some strange behavior. In these cases, you should check the extension output: 159 | 160 | ![](images/extension-output.png) 161 | 162 | ## Community 163 | 164 | Are you stuck? Want to contribute? Come visit us in the [Apollo Discord!](https://discord.gg/graphos) 165 | 166 | ## Maintainers 167 | 168 | - [Michael Watson](https://github.com/michael-watson/) 169 | 170 | ## Licensing 171 | 172 | Source code in this repository is covered by the Elastic License 2.0 as designated by the LICENSE file in the root of the repository. 173 | 174 | **_DISCLAIMER_**: This project is project is maintained by Michael Watson because they use it in schema design discussions regularly. There are many other people that use this tool, but it has been blocked from supporting any new Federation features for some time now. This was due to Federation v1 and v2 eventually requiring different versions of `graphql` making it impossible to have the `npm` packages installed to make Workbench work. Many pieces have been moving in the background to where now Workbench uses `rover` for almost all of it's functionality and the federation specific `npm` packages have been removed (`@apollo/subgraph` is still present to mock schemas locally). If you are interested in helping contribute to this project, feel free to reach out in our [Discord server](https://discord.gg/graphos). 175 | -------------------------------------------------------------------------------- /apollo.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | client: { 3 | name: 'Apollo Studio', 4 | includes: ['./src/graphql/graphClient.ts'], 5 | service: { 6 | url: 'https://graphql.api.apollographql.com/api/graphql', 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /codegen.yml: -------------------------------------------------------------------------------- 1 | schema: ./graphos-schema.graphql 2 | documents: [ 3 | ./src/graphql/queries/AccountServiceVariants.graphql, 4 | ./src/graphql/queries/CheckUserApiKey.graphql, 5 | ./src/graphql/queries/GetGraphSchemas.graphql, 6 | ./src/graphql/queries/UserMemberships.graphql 7 | ] 8 | generates: 9 | ./src/_generated_/typed-document-nodes.ts: 10 | plugins: 11 | - typescript 12 | - typescript-operations 13 | - typed-document-node -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _site 2 | .sass-cache 3 | .jekyll-cache 4 | .jekyll-metadata 5 | vendor 6 | -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: /404.html 3 | layout: default 4 | --- 5 | 6 | 19 | 20 |
21 |

404

22 | 23 |

Page not found :(

24 |

The requested page could not be found.

25 |
26 | -------------------------------------------------------------------------------- /docs/Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | # Hello! This is where you manage which Jekyll version is used to run. 3 | # When you want to use a different version, change it below, save the 4 | # file and run `bundle install`. Run Jekyll with `bundle exec`, like so: 5 | # 6 | # bundle exec jekyll serve 7 | # 8 | # This will help ensure the proper Jekyll version is running. 9 | # Happy Jekylling! 10 | gem "jekyll", "~> 4.3.2" 11 | # This is the default theme for new Jekyll sites. You may change this to anything you like. 12 | gem "minima", "~> 2.5" 13 | # If you want to use GitHub Pages, remove the "gem "jekyll"" above and 14 | # uncomment the line below. To upgrade, run `bundle update github-pages`. 15 | # gem "github-pages", group: :jekyll_plugins 16 | # If you have any plugins, put them here! 17 | group :jekyll_plugins do 18 | gem "jekyll-feed", "~> 0.12" 19 | end 20 | 21 | # Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem 22 | # and associated library. 23 | platforms :mingw, :x64_mingw, :mswin, :jruby do 24 | gem "tzinfo", ">= 1", "< 3" 25 | gem "tzinfo-data" 26 | end 27 | 28 | # Performance-booster for watching directories on Windows 29 | gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin] 30 | 31 | # Lock `http_parser.rb` gem to `v0.6.x` on JRuby builds since newer versions of the gem 32 | # do not have a Java counterpart. 33 | gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby] 34 | -------------------------------------------------------------------------------- /docs/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | addressable (2.8.5) 5 | public_suffix (>= 2.0.2, < 6.0) 6 | colorator (1.1.0) 7 | concurrent-ruby (1.2.2) 8 | em-websocket (0.5.3) 9 | eventmachine (>= 0.12.9) 10 | http_parser.rb (~> 0) 11 | eventmachine (1.2.7) 12 | ffi (1.16.3) 13 | forwardable-extended (2.6.0) 14 | google-protobuf (3.24.4-arm64-darwin) 15 | http_parser.rb (0.8.0) 16 | i18n (1.14.1) 17 | concurrent-ruby (~> 1.0) 18 | jekyll (4.3.2) 19 | addressable (~> 2.4) 20 | colorator (~> 1.0) 21 | em-websocket (~> 0.5) 22 | i18n (~> 1.0) 23 | jekyll-sass-converter (>= 2.0, < 4.0) 24 | jekyll-watch (~> 2.0) 25 | kramdown (~> 2.3, >= 2.3.1) 26 | kramdown-parser-gfm (~> 1.0) 27 | liquid (~> 4.0) 28 | mercenary (>= 0.3.6, < 0.5) 29 | pathutil (~> 0.9) 30 | rouge (>= 3.0, < 5.0) 31 | safe_yaml (~> 1.0) 32 | terminal-table (>= 1.8, < 4.0) 33 | webrick (~> 1.7) 34 | jekyll-feed (0.17.0) 35 | jekyll (>= 3.7, < 5.0) 36 | jekyll-sass-converter (3.0.0) 37 | sass-embedded (~> 1.54) 38 | jekyll-seo-tag (2.8.0) 39 | jekyll (>= 3.8, < 5.0) 40 | jekyll-watch (2.2.1) 41 | listen (~> 3.0) 42 | kramdown (2.4.0) 43 | rexml 44 | kramdown-parser-gfm (1.1.0) 45 | kramdown (~> 2.0) 46 | liquid (4.0.4) 47 | listen (3.8.0) 48 | rb-fsevent (~> 0.10, >= 0.10.3) 49 | rb-inotify (~> 0.9, >= 0.9.10) 50 | mercenary (0.4.0) 51 | minima (2.5.1) 52 | jekyll (>= 3.5, < 5.0) 53 | jekyll-feed (~> 0.9) 54 | jekyll-seo-tag (~> 2.1) 55 | pathutil (0.16.2) 56 | forwardable-extended (~> 2.6) 57 | public_suffix (5.0.3) 58 | rb-fsevent (0.11.2) 59 | rb-inotify (0.10.1) 60 | ffi (~> 1.0) 61 | rexml (3.2.6) 62 | rouge (4.1.3) 63 | safe_yaml (1.0.5) 64 | sass-embedded (1.69.3-arm64-darwin) 65 | google-protobuf (~> 3.23) 66 | terminal-table (3.0.2) 67 | unicode-display_width (>= 1.1.1, < 3) 68 | unicode-display_width (2.5.0) 69 | webrick (1.8.1) 70 | 71 | PLATFORMS 72 | arm64-darwin-22 73 | 74 | DEPENDENCIES 75 | http_parser.rb (~> 0.6.0) 76 | jekyll (~> 4.3.2) 77 | jekyll-feed (~> 0.12) 78 | minima (~> 2.5) 79 | tzinfo (>= 1, < 3) 80 | tzinfo-data 81 | wdm (~> 0.1.1) 82 | 83 | BUNDLED WITH 84 | 2.3.26 85 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | # Welcome to Jekyll! 2 | # 3 | # This config file is meant for settings that affect your whole blog, values 4 | # which you are expected to set up once and rarely edit after that. If you find 5 | # yourself editing this file very often, consider using Jekyll's data files 6 | # feature for the data you need to update frequently. 7 | # 8 | # For technical reasons, this file is *NOT* reloaded automatically when you use 9 | # 'bundle exec jekyll serve'. If you change this file, please restart the server process. 10 | # 11 | # If you need help with YAML syntax, here are some quick references for you: 12 | # https://learn-the-web.algonquindesign.ca/topics/markdown-yaml-cheat-sheet/#yaml 13 | # https://learnxinyminutes.com/docs/yaml/ 14 | # 15 | # Site settings 16 | # These are used to personalize your new site. If you look in the HTML files, 17 | # you will see them accessed via {{ site.title }}, {{ site.email }}, and so on. 18 | # You can create any custom variable you would like, and they will be accessible 19 | # in the templates via {{ site.myvariable }}. 20 | 21 | title: Apollo Workbench 22 | description: >- # this means to ignore newlines until "baseurl:" 23 | Simplifying the design of federated schemas
24 | - Michael Watson 25 | url: 'https://apollographql.com' # the base hostname & protocol for your site, e.g. http://example.com 26 | github_username: apollographql 27 | 28 | # Build settings 29 | theme: minima 30 | plugins: 31 | - jekyll-feed 32 | # Exclude from processing. 33 | # The following items will not be processed, by default. 34 | # Any item listed under the `exclude:` key here will be automatically added to 35 | # the internal "default list". 36 | # 37 | # Excluded items can be processed by explicitly listing the directories or 38 | # their entries' file path in the `include:` list. 39 | # 40 | # exclude: 41 | # - .sass-cache/ 42 | # - .jekyll-cache/ 43 | # - gemfiles/ 44 | # - Gemfile 45 | # - Gemfile.lock 46 | # - node_modules/ 47 | # - vendor/bundle/ 48 | # - vendor/cache/ 49 | # - vendor/gems/ 50 | # - vendor/ruby/ 51 | -------------------------------------------------------------------------------- /docs/_posts/2023-10-15-welcome-to-jekyll.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | layout: post 3 | title: "Welcome to Jekyll!" 4 | date: 2023-10-15 15:35:57 -0700 5 | categories: jekyll update 6 | --- 7 | You’ll find this post in your `_posts` directory. Go ahead and edit it and re-build the site to see your changes. You can rebuild the site in many different ways, but the most common way is to run `jekyll serve`, which launches a web server and auto-regenerates your site when a file is updated. 8 | 9 | Jekyll requires blog post files to be named according to the following format: 10 | 11 | `YEAR-MONTH-DAY-title.MARKUP` 12 | 13 | Where `YEAR` is a four-digit number, `MONTH` and `DAY` are both two-digit numbers, and `MARKUP` is the file extension representing the format used in the file. After that, include the necessary front matter. Take a look at the source for this post to get an idea about how it works. 14 | 15 | Jekyll also offers powerful support for code snippets: 16 | 17 | {% highlight ruby %} 18 | def print_hi(name) 19 | puts "Hi, #{name}" 20 | end 21 | print_hi('Tom') 22 | #=> prints 'Hi, Tom' to STDOUT. 23 | {% endhighlight %} 24 | 25 | Check out the [Jekyll docs][jekyll-docs] for more info on how to get the most out of Jekyll. File all bugs/feature requests at [Jekyll’s GitHub repo][jekyll-gh]. If you have questions, you can ask them on [Jekyll Talk][jekyll-talk]. 26 | 27 | [jekyll-docs]: https://jekyllrb.com/docs/home 28 | [jekyll-gh]: https://github.com/jekyll/jekyll 29 | [jekyll-talk]: https://talk.jekyllrb.com/ 30 | -------------------------------------------------------------------------------- /docs/images/add-api-key.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/docs/images/add-api-key.png -------------------------------------------------------------------------------- /docs/images/composition-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/docs/images/composition-error.png -------------------------------------------------------------------------------- /docs/images/custom-mocks-options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/docs/images/custom-mocks-options.png -------------------------------------------------------------------------------- /docs/images/design-from-graphos-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/docs/images/design-from-graphos-graph.png -------------------------------------------------------------------------------- /docs/images/new-design.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/docs/images/new-design.gif -------------------------------------------------------------------------------- /docs/images/new-operation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/docs/images/new-operation.png -------------------------------------------------------------------------------- /docs/images/run-mocks-locally.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/docs/images/run-mocks-locally.png -------------------------------------------------------------------------------- /docs/images/studio-operations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/docs/images/studio-operations.png -------------------------------------------------------------------------------- /docs/images/view-query-plan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/docs/images/view-query-plan.png -------------------------------------------------------------------------------- /docs/images/view-supergraphSdl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/docs/images/view-supergraphSdl.png -------------------------------------------------------------------------------- /docs/index.markdown: -------------------------------------------------------------------------------- 1 | --- 2 | title: Overview 3 | description: Overview 4 | layout: page 5 | --- 6 | 7 | **Apollo Workbench** is a [VS Code extension](https://marketplace.visualstudio.com/items?itemName=apollographql.apollo-workbench) that helps you design and reason about your organization's graph without writing any server code. Whether you're creating a new graph or making changes to an existing one, Workbench helps you understand how your graph composes throughout the design process. 8 | 9 | ## Setup 10 | 11 | 1. **[Required]** Workbench requires `rover` to run composition. You can Install rover [here](https://www.apollographql.com/docs/rover/getting-started). 12 | 2. **_(Optional)_** Login with GraphOS - run the "GraphOS: Login with User API Key" command with a [user api key](https://studio.apollographql.com/user-settings/api-keys) 13 | 14 | ## Create graphs 15 | 16 | There are two ways to start a design in Workbench: 17 | 18 | 1. Creating a new design, which is a new supergraph `yaml` config file 19 | 2. Importing a `yaml` config from a GraphOS variant 20 | 21 | ### Creating a new design 22 | 23 | Quickly create a new design and start adding subgraphs: 24 | 25 | Creating a Workbench design from scratch 26 | 27 | ### Import graphs from Studio 28 | 29 | After logging into GraphOS, you can create local Workbench designs that are based on any GraphOS graph you have access to: 30 | 31 | Creating a Workbench design from GraphOS 32 | 33 | > All subgraphs in the re-created design will default to read-only and you will have to convert them to a local design file if you want to edit or mock them. You can do this through the prompt that is displayed when you open the schema. 34 | 35 | ### View supergraph and API schemas 36 | 37 | Apollo Workbench runs `rover supergraph compose` every time you save a design file. 38 | 39 | As soon as you have a design that successfully composes, you can view its supergraph schema directly in workbench. If you want to view the API schema, you'll need to [run your design locally](./mocking/). 40 | 41 | Viewing a supergraph schema in Workbench 42 | 43 | ## Debug your graph 44 | 45 | ### See composition errors in-line 46 | 47 | Any composition errors in your design are displayed in both the VS Code editor and the Problems panel. This helps you understand conflicts and resolve them before writing any server code for your subgraphs: 48 | 49 | In-line composition error info 50 | 51 | ### Run locally with mocks 52 | 53 | See the mocking [docs](./mocking/). 54 | 55 | ## Create operations 56 | 57 | ### Build operations from scratch 58 | 59 | Click "Add operation to design" or the "+" button if you have more than one design. You can associate an image for the design that is sourced from a remote url or local file: 60 | 61 | Creating a new operation in Workbench 62 | 63 | ### Import operations from Studio 64 | 65 | After completing the GraphOS Login command, you can import operations that have been executed against any graph you have access to. Just click the graph or graph variant in the "GraphOS Supergraphs" view and the operations will load in the "GraphOS Operations" view. You can then click the "+" icon to add the operation to an existing design: 66 | 67 | Query plan view 68 | 69 | ### View query plans 70 | 71 | You will need to start the design to access Apollo Explorer where you can view the query plans. Just press the play button for the design: 72 | 73 | Query plan view 74 | 75 | > Your design must compose successfully to be able to view query plan details. 76 | -------------------------------------------------------------------------------- /docs/mocking.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Mocking 3 | description: Running supergraph design locally with mocked subgraphs 4 | permalink: /mocking/ 5 | layout: page 6 | --- 7 | 8 | Apollo Workbench enables you to run a mocked version of any fully composing supergraph design on your local machine. 9 | 10 | ## Starting and stopping 11 | 12 | To start your mocked supergraph locally, click the triangular Play button that appears when hovering over the subgraphs row in the navigation panel: 13 | 14 | Play button for running supergraph locally 15 | 16 | This utilizes [`rover dev`](https://www.apollographql.com/docs/rover/commands/dev) to run the supergraph locally with the Apollo Router. Apollo Sandbox will automatically open for you to write queries and view query plans. 17 | 18 | To stop execution, click the square Stop button. 19 | 20 | By default, the mocked supergraph gateway runs on port `4000`. Subgraph ports start with `4001` and increment by one (`4002`, etc.). 21 | 22 | You can override these defaults by modifying your VS Code user settings (go to **Extensions > Apollo-Workbench** and click **Edit in settings.json**): 23 | 24 | ```json 25 | { 26 | "apollo-workbench.gatewayPort": 4000, 27 | "apollo-workbench.startingServerPort": 4001 28 | } 29 | ``` 30 | 31 | > You can only run one supergraph design at a time. Whenever you start a design, any other running design is stopped. 32 | 33 | ## Subgraph Mocking 34 | 35 | All subgraphs can have mocking enabled by either right clicking a subgraph or adding the mocks to the subgraph yaml: 36 | 37 | ```yaml 38 | users: 39 | routing_url: http://localhost:4002 40 | schema: 41 | file: users.graphql 42 | mocks: 43 | enabled: true 44 | customMocks: users-mocks.js 45 | ``` 46 | 47 | ## Defining custom mocks 48 | 49 | You can define custom mocks for any subgraph in [the same format Apollo Server expects for mocks](https://www.apollographql.com/docs/apollo-server/testing/mocking/). Access the custom mocks for any subgraph by right-clicking the subgraph name and selecting **Open Custom Mocks**: 50 | 51 | Viewing custom mocks 52 | 53 | > Custom mocks currently only work with a `.js` file that expects the ` mocks`` to be exported. The default new custom mocks contains the export for you. You can also import `faker` if you want to customize your mocks. 54 | 55 | ### How do mocks work in Workbench? 56 | 57 | When you run your design, Workbench starts one Apollo Server instance for each mocked subgraph in the design. Each instance dynamically creates reference resolvers for any entities that the subgraph defines. These resolvers are used in combination with any defined custom mocks to resolve incoming operations. 58 | 59 | Workbench also runs the appropriate `rover dev` command to start the Apollo Router up locally. 60 | 61 | When all server instances have started, Workbench opens Apollo Sandbox so you can execute operations against the router. 62 | 63 | ## Using non-mocked subgraphs 64 | 65 | You can choose to mock _some_ of your subgraphs while the _rest_ of your subgraphs use actual GraphQL endpoint URLs. These URLs can be local or remote (such as the URL of a staging subgraph). 66 | 67 | To specify a URL for a non-mocked subgraph, just set the `routing_url` in your yaml config: 68 | 69 | ```yaml 70 | subgraphs: 71 | users: 72 | routing_url: http://domain:3000 73 | ``` 74 | 75 | > Workbench does not verify whether a subgraph's remote `url` is reachable. 76 | -------------------------------------------------------------------------------- /images/add-operations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/images/add-operations.png -------------------------------------------------------------------------------- /images/composition-errors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/images/composition-errors.png -------------------------------------------------------------------------------- /images/extension-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/images/extension-output.png -------------------------------------------------------------------------------- /images/graphos-logged-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/images/graphos-logged-in.png -------------------------------------------------------------------------------- /images/graphos-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/images/graphos-login.png -------------------------------------------------------------------------------- /images/graphos-name-new-design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/images/graphos-name-new-design.png -------------------------------------------------------------------------------- /images/graphos-named-new-design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/images/graphos-named-new-design.png -------------------------------------------------------------------------------- /images/graphos-new-design.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/images/graphos-new-design.png -------------------------------------------------------------------------------- /images/local-edit-remote-schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/images/local-edit-remote-schema.png -------------------------------------------------------------------------------- /images/local-editing-remote-schema.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/images/local-editing-remote-schema.png -------------------------------------------------------------------------------- /images/local-new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/images/local-new.png -------------------------------------------------------------------------------- /images/mock-subgraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/images/mock-subgraph.png -------------------------------------------------------------------------------- /images/operation-in-explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/images/operation-in-explorer.png -------------------------------------------------------------------------------- /images/rover-dev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/images/rover-dev.png -------------------------------------------------------------------------------- /media/graphql-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/media/graphql-logo.png -------------------------------------------------------------------------------- /media/logo-apollo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/media/logo-apollo.png -------------------------------------------------------------------------------- /media/logo-apollo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 11 | 12 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 30 | -------------------------------------------------------------------------------- /media/m.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/media/m.png -------------------------------------------------------------------------------- /media/m.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | m 4 | 5 | -------------------------------------------------------------------------------- /media/preloaded-files/Retail-Supergraph-schemas/discovery.graphql: -------------------------------------------------------------------------------- 1 | extend schema 2 | @link(url: "https://specs.apollo.dev/link/v1.0") 3 | @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) 4 | 5 | directive @link( 6 | url: String 7 | as: String 8 | for: link__Purpose 9 | import: [link__Import] 10 | ) repeatable on SCHEMA 11 | 12 | directive @key( 13 | fields: federation__FieldSet! 14 | resolvable: Boolean = true 15 | ) repeatable on OBJECT | INTERFACE 16 | 17 | directive @federation__requires( 18 | fields: federation__FieldSet! 19 | ) on FIELD_DEFINITION 20 | 21 | directive @federation__provides( 22 | fields: federation__FieldSet! 23 | ) on FIELD_DEFINITION 24 | 25 | directive @federation__external(reason: String) on OBJECT | FIELD_DEFINITION 26 | 27 | directive @federation__tag( 28 | name: String! 29 | ) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION 30 | 31 | directive @federation__extends on OBJECT | INTERFACE 32 | 33 | directive @federation__shareable on OBJECT | FIELD_DEFINITION 34 | 35 | directive @federation__inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION 36 | 37 | directive @federation__override(from: String!) on FIELD_DEFINITION 38 | 39 | type User @key(fields: "id") { 40 | id: ID! 41 | 42 | """ 43 | Suggest products for this user, optionally pass in a specific product id to compare too 44 | """ 45 | recommendedProducts(productId: ID = null): [Product] 46 | } 47 | 48 | type Product @key(fields: "id") { 49 | id: ID! 50 | 51 | """ 52 | Related products for this product, the user can be fetched from the `x-user-id` header 53 | """ 54 | recommendedProducts: [Product] 55 | } 56 | 57 | enum link__Purpose { 58 | """ 59 | `SECURITY` features provide metadata necessary to securely resolve fields. 60 | """ 61 | SECURITY 62 | 63 | """ 64 | `EXECUTION` features provide metadata necessary for operation execution. 65 | """ 66 | EXECUTION 67 | } 68 | 69 | scalar link__Import 70 | 71 | scalar federation__FieldSet 72 | 73 | scalar _Any 74 | 75 | type _Service { 76 | sdl: String 77 | } 78 | 79 | union _Entity = Product | User 80 | 81 | type Query { 82 | _entities(representations: [_Any!]!): [_Entity]! 83 | _service: _Service! 84 | } 85 | -------------------------------------------------------------------------------- /media/preloaded-files/Retail-Supergraph-schemas/inventory.graphql: -------------------------------------------------------------------------------- 1 | extend schema 2 | @link(url: "https://specs.apollo.dev/link/v1.0") 3 | @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) 4 | 5 | directive @link( 6 | url: String 7 | as: String 8 | for: link__Purpose 9 | import: [link__Import] 10 | ) repeatable on SCHEMA 11 | 12 | directive @key( 13 | fields: federation__FieldSet! 14 | resolvable: Boolean = true 15 | ) repeatable on OBJECT | INTERFACE 16 | 17 | directive @federation__requires( 18 | fields: federation__FieldSet! 19 | ) on FIELD_DEFINITION 20 | 21 | directive @federation__provides( 22 | fields: federation__FieldSet! 23 | ) on FIELD_DEFINITION 24 | 25 | directive @federation__external(reason: String) on OBJECT | FIELD_DEFINITION 26 | 27 | directive @federation__tag( 28 | name: String! 29 | ) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION 30 | 31 | directive @federation__extends on OBJECT | INTERFACE 32 | 33 | directive @federation__shareable on OBJECT | FIELD_DEFINITION 34 | 35 | directive @federation__inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION 36 | 37 | directive @federation__override(from: String!) on FIELD_DEFINITION 38 | 39 | type Variant @key(fields: "id") { 40 | id: ID! 41 | 42 | """ 43 | Checks the warehouse API for inventory information. 44 | """ 45 | inventory: Inventory 46 | } 47 | 48 | """ 49 | Inventory details about a specific Variant 50 | """ 51 | type Inventory { 52 | """ 53 | Returns true if the inventory count is greater than 0 54 | """ 55 | inStock: Boolean! 56 | 57 | """ 58 | The raw count of not purchased items in the warehouse 59 | """ 60 | inventory: Int 61 | } 62 | 63 | enum link__Purpose { 64 | """ 65 | `SECURITY` features provide metadata necessary to securely resolve fields. 66 | """ 67 | SECURITY 68 | 69 | """ 70 | `EXECUTION` features provide metadata necessary for operation execution. 71 | """ 72 | EXECUTION 73 | } 74 | 75 | scalar link__Import 76 | 77 | scalar federation__FieldSet 78 | 79 | scalar _Any 80 | 81 | type _Service { 82 | sdl: String 83 | } 84 | 85 | union _Entity = Variant 86 | 87 | type Query { 88 | _entities(representations: [_Any!]!): [_Entity]! 89 | _service: _Service! 90 | } 91 | -------------------------------------------------------------------------------- /media/preloaded-files/Retail-Supergraph-schemas/orders.graphql: -------------------------------------------------------------------------------- 1 | schema @link(url: "https://specs.apollo.dev/link/v1.0") { 2 | query: Query 3 | } 4 | 5 | extend schema 6 | @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) 7 | 8 | directive @link( 9 | url: String 10 | as: String 11 | for: link__Purpose 12 | import: [link__Import] 13 | ) repeatable on SCHEMA 14 | 15 | directive @key( 16 | fields: federation__FieldSet! 17 | resolvable: Boolean = true 18 | ) repeatable on OBJECT | INTERFACE 19 | 20 | directive @federation__requires( 21 | fields: federation__FieldSet! 22 | ) on FIELD_DEFINITION 23 | 24 | directive @federation__provides( 25 | fields: federation__FieldSet! 26 | ) on FIELD_DEFINITION 27 | 28 | directive @federation__external(reason: String) on OBJECT | FIELD_DEFINITION 29 | 30 | directive @federation__tag( 31 | name: String! 32 | ) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION 33 | 34 | directive @federation__extends on OBJECT | INTERFACE 35 | 36 | directive @federation__shareable on OBJECT | FIELD_DEFINITION 37 | 38 | directive @federation__inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION 39 | 40 | directive @federation__override(from: String!) on FIELD_DEFINITION 41 | 42 | type Query { 43 | """ 44 | Get a specific order by id. Meant to be used for a detailed view of an order 45 | """ 46 | order(id: ID!): Order 47 | _entities(representations: [_Any!]!): [_Entity]! 48 | _service: _Service! 49 | } 50 | 51 | """ 52 | Returns information about a specific purchase 53 | """ 54 | type Order @key(fields: "id") { 55 | """ 56 | Each order has a unique id which is separate from the user or items they bought 57 | """ 58 | id: ID! 59 | 60 | """ 61 | The user who made the purchase 62 | """ 63 | buyer: User! 64 | 65 | """ 66 | A list of all the items they purchased. This is the Variants, not the Products so we know exactly which 67 | product and which size/color/feature was bought 68 | """ 69 | items: [Variant!]! 70 | } 71 | 72 | type User @key(fields: "id", resolvable: false) { 73 | id: ID! 74 | } 75 | 76 | type Variant @key(fields: "id", resolvable: false) { 77 | id: ID! 78 | } 79 | 80 | enum link__Purpose { 81 | """ 82 | `SECURITY` features provide metadata necessary to securely resolve fields. 83 | """ 84 | SECURITY 85 | 86 | """ 87 | `EXECUTION` features provide metadata necessary for operation execution. 88 | """ 89 | EXECUTION 90 | } 91 | 92 | scalar link__Import 93 | 94 | scalar federation__FieldSet 95 | 96 | scalar _Any 97 | 98 | type _Service { 99 | sdl: String 100 | } 101 | 102 | union _Entity = Order | User | Variant 103 | -------------------------------------------------------------------------------- /media/preloaded-files/Retail-Supergraph-schemas/products.graphql: -------------------------------------------------------------------------------- 1 | schema @link(url: "https://specs.apollo.dev/link/v1.0") { 2 | query: Query 3 | } 4 | 5 | extend schema 6 | @link( 7 | url: "https://specs.apollo.dev/federation/v2.0" 8 | import: ["@key", "@tag"] 9 | ) 10 | 11 | directive @link( 12 | url: String 13 | as: String 14 | for: link__Purpose 15 | import: [link__Import] 16 | ) repeatable on SCHEMA 17 | 18 | directive @key( 19 | fields: federation__FieldSet! 20 | resolvable: Boolean = true 21 | ) repeatable on OBJECT | INTERFACE 22 | 23 | directive @federation__requires( 24 | fields: federation__FieldSet! 25 | ) on FIELD_DEFINITION 26 | 27 | directive @federation__provides( 28 | fields: federation__FieldSet! 29 | ) on FIELD_DEFINITION 30 | 31 | directive @federation__external(reason: String) on OBJECT | FIELD_DEFINITION 32 | 33 | directive @tag( 34 | name: String! 35 | ) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION 36 | 37 | directive @federation__extends on OBJECT | INTERFACE 38 | 39 | directive @federation__shareable on OBJECT | FIELD_DEFINITION 40 | 41 | directive @federation__inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION 42 | 43 | directive @federation__override(from: String!) on FIELD_DEFINITION 44 | 45 | type Query { 46 | """ 47 | Get all available products to shop for. Optionally provide some search filters 48 | """ 49 | searchProducts(searchInput: ProductSearchInput! = {}): [Product] 50 | 51 | """ 52 | Get all available variants of products to shop for. Optionally provide some search filters 53 | """ 54 | searchVariants(searchInput: VariantSearchInput! = {}): [Variant] 55 | 56 | """ 57 | Get a specific product by id. Useful for the product details page or checkout page 58 | """ 59 | product(id: ID!): Product 60 | 61 | """ 62 | Get a specific variant by id. Useful for the product details page or checkout page 63 | """ 64 | variant(id: ID!): Variant 65 | _entities(representations: [_Any!]!): [_Entity]! 66 | _service: _Service! 67 | } 68 | 69 | """ 70 | Search filters for when returning Products 71 | """ 72 | input ProductSearchInput { 73 | titleStartsWith: String 74 | } 75 | 76 | """ 77 | Search filters for when returning Variants 78 | """ 79 | input VariantSearchInput { 80 | sizeStartsWith: String 81 | } 82 | 83 | """ 84 | A specific product sold by our store. This contains all the high level details but is not the purchasable item. 85 | See Variant for more info. 86 | """ 87 | type Product @key(fields: "id") @key(fields: "upc") @tag(name: "partner") { 88 | id: ID! @tag(name: "internal") 89 | upc: ID! 90 | title: String 91 | description: String 92 | mediaUrl: String 93 | 94 | """ 95 | Mock random date of when a product might be released 96 | """ 97 | releaseDate: String 98 | 99 | """ 100 | Variants of the products to view specific size/color/price options 101 | """ 102 | variants(searchInput: VariantSearchInput = {}): [Variant] 103 | } 104 | 105 | """ 106 | A variant of a product which is a unique combination of attributes like size and color 107 | Variants are the entities that are added to carts and purchased 108 | """ 109 | type Variant @key(fields: "id") { 110 | id: ID! 111 | 112 | """ 113 | Link back to the parent Product 114 | """ 115 | product: Product 116 | 117 | """ 118 | Optional color option for this variant 119 | """ 120 | colorway: String 121 | 122 | """ 123 | Price in decimals for this variant 124 | """ 125 | price: Float! 126 | 127 | """ 128 | Optional size option for this variant 129 | """ 130 | size: String 131 | 132 | """ 133 | Optional dimensions. Can be use to calculate other info like shipping or packaging 134 | """ 135 | dimensions: String 136 | 137 | """ 138 | Optional weight. Can be use to calculate other info like shipping or packaging 139 | """ 140 | weight: Float 141 | } 142 | 143 | enum link__Purpose { 144 | """ 145 | `SECURITY` features provide metadata necessary to securely resolve fields. 146 | """ 147 | SECURITY 148 | 149 | """ 150 | `EXECUTION` features provide metadata necessary for operation execution. 151 | """ 152 | EXECUTION 153 | } 154 | 155 | scalar link__Import 156 | 157 | scalar federation__FieldSet 158 | 159 | scalar _Any 160 | 161 | type _Service { 162 | sdl: String 163 | } 164 | 165 | union _Entity = Product | Variant 166 | -------------------------------------------------------------------------------- /media/preloaded-files/Retail-Supergraph-schemas/reviews.graphql: -------------------------------------------------------------------------------- 1 | extend schema 2 | @link(url: "https://specs.apollo.dev/link/v1.0") 3 | @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) 4 | 5 | directive @link( 6 | url: String 7 | as: String 8 | for: link__Purpose 9 | import: [link__Import] 10 | ) repeatable on SCHEMA 11 | 12 | directive @key( 13 | fields: federation__FieldSet! 14 | resolvable: Boolean = true 15 | ) repeatable on OBJECT | INTERFACE 16 | 17 | directive @federation__requires( 18 | fields: federation__FieldSet! 19 | ) on FIELD_DEFINITION 20 | 21 | directive @federation__provides( 22 | fields: federation__FieldSet! 23 | ) on FIELD_DEFINITION 24 | 25 | directive @federation__external(reason: String) on OBJECT | FIELD_DEFINITION 26 | 27 | directive @federation__tag( 28 | name: String! 29 | ) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION 30 | 31 | directive @federation__extends on OBJECT | INTERFACE 32 | 33 | directive @federation__shareable on OBJECT | FIELD_DEFINITION 34 | 35 | directive @federation__inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION 36 | 37 | directive @federation__override(from: String!) on FIELD_DEFINITION 38 | 39 | """ 40 | A review of a given product by a specific user 41 | """ 42 | type Review @key(fields: "id") { 43 | id: ID! 44 | 45 | """ 46 | The plain text version of the review 47 | """ 48 | body: String 49 | 50 | """ 51 | The person who authored the review 52 | """ 53 | author: String @deprecated(reason: "Use the new `user` field") 54 | 55 | """ 56 | The User who submitted the review 57 | """ 58 | user: User 59 | 60 | """ 61 | The product which this review is about 62 | """ 63 | product: Product 64 | } 65 | 66 | type Product @key(fields: "upc") { 67 | upc: ID! 68 | reviews: [Review!] 69 | } 70 | 71 | type User @key(fields: "id", resolvable: false) { 72 | id: ID! 73 | } 74 | 75 | enum link__Purpose { 76 | """ 77 | `SECURITY` features provide metadata necessary to securely resolve fields. 78 | """ 79 | SECURITY 80 | 81 | """ 82 | `EXECUTION` features provide metadata necessary for operation execution. 83 | """ 84 | EXECUTION 85 | } 86 | 87 | scalar link__Import 88 | 89 | scalar federation__FieldSet 90 | 91 | scalar _Any 92 | 93 | type _Service { 94 | sdl: String 95 | } 96 | 97 | union _Entity = Product | Review | User 98 | 99 | type Query { 100 | _entities(representations: [_Any!]!): [_Entity]! 101 | _service: _Service! 102 | } 103 | -------------------------------------------------------------------------------- /media/preloaded-files/Retail-Supergraph-schemas/shipping.graphql: -------------------------------------------------------------------------------- 1 | extend schema 2 | @link(url: "https://specs.apollo.dev/link/v1.0") 3 | @link( 4 | url: "https://specs.apollo.dev/federation/v2.0" 5 | import: ["@key", "@external", "@requires"] 6 | ) 7 | 8 | directive @link( 9 | url: String 10 | as: String 11 | for: link__Purpose 12 | import: [link__Import] 13 | ) repeatable on SCHEMA 14 | 15 | directive @key( 16 | fields: federation__FieldSet! 17 | resolvable: Boolean = true 18 | ) repeatable on OBJECT | INTERFACE 19 | 20 | directive @requires(fields: federation__FieldSet!) on FIELD_DEFINITION 21 | 22 | directive @federation__provides( 23 | fields: federation__FieldSet! 24 | ) on FIELD_DEFINITION 25 | 26 | directive @external(reason: String) on OBJECT | FIELD_DEFINITION 27 | 28 | directive @federation__tag( 29 | name: String! 30 | ) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION 31 | 32 | directive @federation__extends on OBJECT | INTERFACE 33 | 34 | directive @federation__shareable on OBJECT | FIELD_DEFINITION 35 | 36 | directive @federation__inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION 37 | 38 | directive @federation__override(from: String!) on FIELD_DEFINITION 39 | 40 | type Order @key(fields: "id") { 41 | id: ID! 42 | buyer: User! @external 43 | items: [Variant!]! @external 44 | 45 | """ 46 | Calculate the cost to ship all the variants to the users address 47 | """ 48 | shippingCost: Float 49 | @requires(fields: "items { weight } buyer { shippingAddress }") 50 | } 51 | 52 | type User @key(fields: "id", resolvable: false) { 53 | id: ID! 54 | shippingAddress: String @external 55 | } 56 | 57 | type Variant @key(fields: "id", resolvable: false) { 58 | id: ID! 59 | weight: Float @external 60 | } 61 | 62 | enum link__Purpose { 63 | """ 64 | `SECURITY` features provide metadata necessary to securely resolve fields. 65 | """ 66 | SECURITY 67 | 68 | """ 69 | `EXECUTION` features provide metadata necessary for operation execution. 70 | """ 71 | EXECUTION 72 | } 73 | 74 | scalar link__Import 75 | 76 | scalar federation__FieldSet 77 | 78 | scalar _Any 79 | 80 | type _Service { 81 | sdl: String 82 | } 83 | 84 | union _Entity = Order | User | Variant 85 | 86 | type Query { 87 | _entities(representations: [_Any!]!): [_Entity]! 88 | _service: _Service! 89 | } 90 | -------------------------------------------------------------------------------- /media/preloaded-files/Retail-Supergraph-schemas/users.graphql: -------------------------------------------------------------------------------- 1 | schema @link(url: "https://specs.apollo.dev/link/v1.0") { 2 | query: Query 3 | } 4 | 5 | extend schema 6 | @link(url: "https://specs.apollo.dev/federation/v2.0", import: ["@key"]) 7 | 8 | directive @link( 9 | url: String 10 | as: String 11 | for: link__Purpose 12 | import: [link__Import] 13 | ) repeatable on SCHEMA 14 | 15 | directive @key( 16 | fields: federation__FieldSet! 17 | resolvable: Boolean = true 18 | ) repeatable on OBJECT | INTERFACE 19 | 20 | directive @federation__requires( 21 | fields: federation__FieldSet! 22 | ) on FIELD_DEFINITION 23 | 24 | directive @federation__provides( 25 | fields: federation__FieldSet! 26 | ) on FIELD_DEFINITION 27 | 28 | directive @federation__external(reason: String) on OBJECT | FIELD_DEFINITION 29 | 30 | directive @federation__tag( 31 | name: String! 32 | ) repeatable on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION 33 | 34 | directive @federation__extends on OBJECT | INTERFACE 35 | 36 | directive @federation__shareable on OBJECT | FIELD_DEFINITION 37 | 38 | directive @federation__inaccessible on FIELD_DEFINITION | OBJECT | INTERFACE | UNION | ARGUMENT_DEFINITION | SCALAR | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION 39 | 40 | directive @federation__override(from: String!) on FIELD_DEFINITION 41 | 42 | type Query { 43 | """ 44 | Get the current user from our fake "auth" headers 45 | Set the "x-user-id" header to the user id. 46 | """ 47 | user: User 48 | _entities(representations: [_Any!]!): [_Entity]! 49 | _service: _Service! 50 | } 51 | 52 | """ 53 | An user account in our system 54 | """ 55 | type User @key(fields: "id") { 56 | id: ID! 57 | 58 | """ 59 | The users login username 60 | """ 61 | username: String! 62 | 63 | """ 64 | Get the list of last session id of user activity 65 | """ 66 | previousSessions: [ID!] 67 | 68 | """ 69 | Total saved loyalty points and rewards 70 | """ 71 | loyaltyPoints: Int 72 | 73 | """ 74 | Saved payment methods that can be used to submit orders 75 | """ 76 | paymentMethods: [PaymentMethod] 77 | 78 | """ 79 | The users previous purchases 80 | """ 81 | orders: [Order] 82 | 83 | """ 84 | The users current saved shipping address 85 | """ 86 | shippingAddress: String 87 | } 88 | 89 | """ 90 | A saved payment option for an user 91 | """ 92 | type PaymentMethod { 93 | id: ID! 94 | name: String 95 | description: String 96 | type: PaymentType! 97 | } 98 | 99 | """ 100 | A fix set of payment types that we accept 101 | """ 102 | enum PaymentType { 103 | CREDIT_CARD 104 | DEBIT_CARD 105 | BANK_ACCOUNT 106 | } 107 | 108 | type Order @key(fields: "id", resolvable: false) { 109 | id: ID! 110 | } 111 | 112 | enum link__Purpose { 113 | """ 114 | `SECURITY` features provide metadata necessary to securely resolve fields. 115 | """ 116 | SECURITY 117 | 118 | """ 119 | `EXECUTION` features provide metadata necessary for operation execution. 120 | """ 121 | EXECUTION 122 | } 123 | 124 | scalar link__Import 125 | 126 | scalar federation__FieldSet 127 | 128 | scalar _Any 129 | 130 | type _Service { 131 | sdl: String 132 | } 133 | 134 | union _Entity = Order | User 135 | -------------------------------------------------------------------------------- /media/preloaded-files/Retail-Supergraph.yaml: -------------------------------------------------------------------------------- 1 | federation_version: '=2.7.2' 2 | subgraphs: 3 | users: 4 | routing_url: https://apollosolutions--retail-supergraph.fly.dev/users/graphql 5 | schema: 6 | workbench_design: ./Retail-Supergraph-schemas/users.graphql 7 | file: >- 8 | ./Retail-Supergraph-schemas/users.graphql 9 | mocks: 10 | enabled: true 11 | shipping: 12 | routing_url: https://apollosolutions--retail-supergraph.fly.dev/shipping/graphql 13 | schema: 14 | workbench_design: ./Retail-Supergraph-schemas/shipping.graphql 15 | file: >- 16 | ./Retail-Supergraph-schemas/shipping.graphql 17 | mocks: 18 | enabled: true 19 | reviews: 20 | routing_url: https://apollosolutions--retail-supergraph.fly.dev/reviews/graphql 21 | schema: 22 | workbench_design: ./Retail-Supergraph-schemas/reviews.graphql 23 | file: >- 24 | ./Retail-Supergraph-schemas/reviews.graphql 25 | mocks: 26 | enabled: true 27 | products: 28 | routing_url: https://apollosolutions--retail-supergraph.fly.dev/products/graphql 29 | schema: 30 | workbench_design: ./Retail-Supergraph-schemas/products.graphql 31 | file: >- 32 | ./Retail-Supergraph-schemas/products.graphql 33 | mocks: 34 | enabled: true 35 | orders: 36 | routing_url: https://apollosolutions--retail-supergraph.fly.dev/orders/graphql 37 | schema: 38 | workbench_design: ./Retail-Supergraph-schemas/orders.graphql 39 | file: >- 40 | ./Retail-Supergraph-schemas/orders.graphql 41 | mocks: 42 | enabled: true 43 | inventory: 44 | routing_url: https://apollosolutions--retail-supergraph.fly.dev/inventory/graphql 45 | schema: 46 | workbench_design: ./Retail-Supergraph-schemas/inventory.graphql 47 | file: >- 48 | ./Retail-Supergraph-schemas/inventory.graphql 49 | mocks: 50 | enabled: true 51 | discovery: 52 | routing_url: https://apollosolutions--retail-supergraph.fly.dev/discovery/graphql 53 | schema: 54 | workbench_design: ./Retail-Supergraph-schemas/discovery.graphql 55 | file: >- 56 | ./Retail-Supergraph-schemas/discovery.graphql 57 | mocks: 58 | enabled: true 59 | operations: {} 60 | -------------------------------------------------------------------------------- /media/preloaded-files/Spotify-Showcase.yaml: -------------------------------------------------------------------------------- 1 | federation_version: '=2.7.2' 2 | subgraphs: 3 | spotify: 4 | routing_url: https://showcase-spotify.apollographql.com/ 5 | schema: 6 | workbench_design: >- 7 | ./Spotify-Showcase-schemas/spotify.graphql 8 | file: >- 9 | ./Spotify-Showcase-schemas/spotify.graphql 10 | mocks: 11 | enabled: true 12 | playback: 13 | routing_url: https://showcase-playback.apollographql.com/ 14 | schema: 15 | workbench_design: >- 16 | ./Spotify-Showcase-schemas/playback.graphql 17 | file: >- 18 | ./Spotify-Showcase-schemas/playback.graphql 19 | mocks: 20 | enabled: true 21 | operations: {} 22 | -------------------------------------------------------------------------------- /media/preloaded-files/customMocks.txt: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-var-requires */ 2 | const { faker } = require("@faker-js/faker"); 3 | 4 | const mocks = { 5 | Int: () => 6, 6 | Float: () => 22.1, 7 | String: () => "Hello", 8 | Product: () => ({ 9 | id: () => faker.commerce.isbn(), 10 | }), 11 | }; 12 | 13 | //Add this to this subgraphs schema to test out the resolver below 14 | //type Query { 15 | // catFact: String 16 | //} 17 | const resolvers = { 18 | Query: { 19 | catFact: async () => { 20 | console.log(`Getting random cat fact`); 21 | const response = await fetch("https://catfact.ninja/fact"); 22 | const jsonResponse = await response.json(); 23 | return jsonResponse.fact; 24 | }, 25 | }, 26 | }; 27 | 28 | module.exports = { mocks, resolvers }; 29 | -------------------------------------------------------------------------------- /media/preloaded-files/router.yaml: -------------------------------------------------------------------------------- 1 | sandbox: 2 | enabled: true 3 | homepage: 4 | enabled: false 5 | supergraph: 6 | introspection: true 7 | include_subgraph_errors: 8 | all: true 9 | plugins: 10 | # Enable with the header, Apollo-Expose-Query-Plan: true 11 | experimental.expose_query_plan: true 12 | headers: 13 | all: 14 | request: 15 | - propagate: 16 | matching: .* 17 | telemetry: 18 | instrumentation: 19 | spans: 20 | mode: spec_compliant -------------------------------------------------------------------------------- /media/q.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/media/q.png -------------------------------------------------------------------------------- /media/q.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | q 5 | 6 | -------------------------------------------------------------------------------- /media/subgraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/media/subgraph.png -------------------------------------------------------------------------------- /media/subgraph.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /media/supergraph.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /media/versions.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /media/w.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | w 4 | 5 | -------------------------------------------------------------------------------- /media/workbench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/media/workbench.png -------------------------------------------------------------------------------- /media/workbench.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | Group 2 5 | 6 | 7 | 8 | 9 | 11 | 12 | 13 | 15 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /media/worktools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/apollographql/apollo-workbench-vscode/0a8318aa10112d6f1bff4cbaf0bb82c14e996dab/media/worktools.png -------------------------------------------------------------------------------- /media/worktools.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | Created by potrace 1.15, written by Peter Selinger 2001-2017 7 | 8 | 9 | 17 | 27 | 35 | 36 | -------------------------------------------------------------------------------- /src/__tests__/githubCheckTests.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const fetch = require('node-fetch'); 3 | import { testRunner } from './testRunner'; 4 | 5 | async function setStatus(state, description) { 6 | return fetch( 7 | `https://api.github.com/repos/apollographql/apollo-workbench-vscode/statuses/${process.env.GITHUB_SHA}`, 8 | { 9 | method: 'POST', 10 | body: JSON.stringify({ 11 | state, 12 | description, 13 | context: 'VSCode Extension Tests', 14 | }), 15 | headers: { 16 | Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, 17 | 'Content-Type': 'application/json', 18 | }, 19 | }, 20 | ); 21 | } 22 | 23 | async function githubActionTests() { 24 | try { 25 | await setStatus('pending', 'Running check..'); 26 | console.log('Set Status successfully'); 27 | let result = 1; 28 | 29 | console.log('Running tests with no folder loaded'); 30 | result = await testRunner(); 31 | console.log('Running tests with folder loaded'); 32 | result = await testRunner(true); 33 | 34 | await setStatus('success', 'Tests for workbench folder open passed'); 35 | process.exit(result); 36 | } catch (err: any) { 37 | console.log( 38 | err?.message 39 | ? `Failed to run tests: ${err.message}` 40 | : `Failed to run tests: ${err}`, 41 | ); 42 | await setStatus( 43 | 'error', 44 | err?.message ? err.message : `Failed to run tests: ${err}`, 45 | ); 46 | process.exit(1); 47 | } 48 | } 49 | 50 | githubActionTests(); 51 | -------------------------------------------------------------------------------- /src/__tests__/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from '@vscode/test-electron'; 4 | 5 | async function mainTestRunner(loadFolder = false) { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to the extension test script 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 14 | const testWorkbenchFolder = path.resolve(__dirname, './test-workbench'); 15 | 16 | // let shouldLoadWorkbenchFolder = process.argv[2]; 17 | // log('******shouldLoadWorkbenchFolder:' + shouldLoadWorkbenchFolder); 18 | 19 | // log('******extensionDevelopmentPath:' + extensionDevelopmentPath); 20 | // log('******extensionTestsPath:' + extensionTestsPath); 21 | // log('******testWorkbenchFolder:' + testWorkbenchFolder); 22 | 23 | if (loadFolder) 24 | await runTests({ 25 | extensionDevelopmentPath, 26 | extensionTestsPath, 27 | launchArgs: [testWorkbenchFolder, '--disable-extensions'], 28 | }); 29 | else 30 | await runTests({ 31 | extensionDevelopmentPath, 32 | extensionTestsPath, 33 | launchArgs: ['--disable-extensions'], 34 | }); 35 | } catch (err) { 36 | console.error('Failed to run tests'); 37 | process.exit(1); 38 | } 39 | } 40 | 41 | mainTestRunner(); 42 | -------------------------------------------------------------------------------- /src/__tests__/suite/defaults.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | import { it, before } from 'mocha'; 4 | import { StateManager } from '../../workbench/stateManager'; 5 | import { commands } from 'vscode'; 6 | import NotLoggedInTreeItem from '../../workbench/tree-data-providers/tree-items/graphos-supergraphs/notLoggedInTreeItem'; 7 | 8 | suite('Default Workbench Tests', async () => { 9 | before(async () => { 10 | await commands.executeCommand('local-supergraph-designs.focus'); 11 | }); 12 | 13 | it('Defaults:StudioGraphs - Should display login item', async function () { 14 | await commands.executeCommand('local-supergraph-designs.focus'); 15 | //Setup 16 | StateManager.instance.globalState_userApiKey = ''; 17 | 18 | //Get TreeView children 19 | const studioGraphTreeItems = 20 | await StateManager.instance.apolloStudioGraphsProvider.getChildren(); 21 | 22 | //Assert 23 | studioGraphTreeItems.forEach((studioGraphTreeItem) => 24 | assert.notStrictEqual( 25 | studioGraphTreeItem as NotLoggedInTreeItem, 26 | undefined, 27 | ), 28 | ); 29 | }); 30 | it('Defaults:StudioOperations - Should display login item', async function () { 31 | //Setup 32 | StateManager.instance.globalState_userApiKey = ''; 33 | 34 | //Get TreeView children 35 | const studioGraphTreeItems = 36 | await StateManager.instance.apolloStudioGraphsProvider.getChildren(); 37 | 38 | //Assert 39 | for (let i = 0; i < studioGraphTreeItems.length; i++) 40 | assert.notStrictEqual( 41 | studioGraphTreeItems[i] as NotLoggedInTreeItem, 42 | undefined, 43 | ); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/__tests__/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import { suite, before, afterEach } from 'mocha'; 3 | import { 4 | activateExtension, 5 | cleanupWorkbenchFiles, 6 | } from './helpers'; 7 | 8 | const key = 'Loaded-Folder'; 9 | 10 | suite(key, () => { 11 | before(activateExtension); 12 | afterEach(cleanupWorkbenchFiles); 13 | }); 14 | -------------------------------------------------------------------------------- /src/__tests__/suite/helpers.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import { readdirSync, unlinkSync } from 'fs'; 4 | import { FileProvider } from '../../workbench/file-system/fileProvider'; 5 | import { log } from 'util'; 6 | 7 | export const activateExtension = async () => { 8 | return new Promise((resolve) => { 9 | const extension = vscode.extensions.getExtension( 10 | 'ApolloGraphQL.apollo-workbench-vscode', 11 | ); 12 | if (extension) { 13 | extension.activate().then(resolve); 14 | } else { 15 | resolve(); 16 | } 17 | }); 18 | }; 19 | 20 | export function cleanupWorkbenchFiles() { 21 | try { 22 | const directory = path.resolve(__dirname, '..', './test-workbench'); 23 | const dirents = readdirSync(directory, { withFileTypes: true }); 24 | for (const dirent of dirents) { 25 | if (dirent.isFile() && dirent.name.includes('.apollo-workbench')) 26 | unlinkSync(path.resolve(directory, dirent.name)); 27 | } 28 | } catch (err) { 29 | log(`Cleanup Error: ${err}`); 30 | } 31 | } 32 | 33 | export async function createAndLoadEmptyWorkbenchFile() { 34 | // const workbenchFileName = 'empty-workbench'; 35 | // const workbenchFilePath = FileProvider.instance.createNewWorkbenchFile(workbenchFileName); 36 | // if (!workbenchFilePath) throw new Error('Workbench file was not created'); 37 | // await FileProvider.instance.loadWorkbenchFile(workbenchFileName, workbenchFilePath); 38 | } 39 | 40 | export const simpleSchema = ` 41 | type A @key(fields:"id"){ 42 | id: ID! 43 | } 44 | extend type Query { 45 | a: A 46 | } 47 | `; 48 | -------------------------------------------------------------------------------- /src/__tests__/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import Mocha from 'mocha'; 3 | import glob from 'glob'; 4 | import { StateManager } from '../../workbench/stateManager'; 5 | 6 | export function run(): Promise { 7 | // Create the mocha test 8 | const mocha = new Mocha({ 9 | ui: 'tdd', 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, '..'); 13 | const isWorkbenchFolderLoaded = StateManager.workspaceRoot ? true : false; 14 | 15 | return new Promise((c, e) => { 16 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 17 | if (err) { 18 | return e(err); 19 | } 20 | 21 | // Add files to the test suite 22 | files.forEach((f) => { 23 | if (f.includes('defaults')) mocha.addFile(path.resolve(testsRoot, f)); 24 | else if (isWorkbenchFolderLoaded && !f.includes('noFolder')) 25 | mocha.addFile(path.resolve(testsRoot, f)); 26 | else if (!isWorkbenchFolderLoaded && f.includes('noFolder')) 27 | mocha.addFile(path.resolve(testsRoot, f)); 28 | }); 29 | 30 | try { 31 | // Run the mocha test 32 | mocha.run((failures) => { 33 | if (failures > 0) { 34 | e(new Error(`${failures} tests failed.`)); 35 | } else { 36 | c(); 37 | } 38 | }); 39 | } catch (err) { 40 | console.error(`Mocha Run Error: ${err}`); 41 | e(err); 42 | } 43 | }); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /src/__tests__/suite/noFolder.test.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { suite, after, before } from 'mocha'; 3 | 4 | import { activateExtension } from './helpers'; 5 | 6 | suite('No Folder Loaded in Workbnech', () => { 7 | vscode.window.showInformationMessage('Start all tests.'); 8 | before(activateExtension); 9 | after(() => undefined); 10 | }); 11 | -------------------------------------------------------------------------------- /src/__tests__/test-workbench/index.ts: -------------------------------------------------------------------------------- 1 | //This is here to ensure the test workbench file is compiled over to out folder 2 | -------------------------------------------------------------------------------- /src/__tests__/testRunner.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from '@vscode/test-electron'; 4 | 5 | //Function for running the tests 6 | // @param `loadFolder` will load the testing folder and run the associated tests 7 | // default: No folder will be opened and default tests will be ran 8 | export async function testRunner(loadFolder = false) { 9 | try { 10 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 11 | const extensionTestsPath = path.resolve(__dirname, './suite/index'); 12 | const testWorkbenchFolder = path.resolve(__dirname, './test-workbench'); 13 | 14 | let testResults = 1; 15 | if (loadFolder) 16 | testResults = await runTests({ 17 | extensionDevelopmentPath, 18 | extensionTestsPath, 19 | launchArgs: [testWorkbenchFolder], 20 | }); 21 | else 22 | testResults = await runTests({ 23 | extensionDevelopmentPath, 24 | extensionTestsPath 25 | }); 26 | 27 | return testResults; 28 | } catch (err) { 29 | console.error(err); 30 | console.error('Failed to run tests'); 31 | process.exit(1); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/__tests__/testsNoStatus.ts: -------------------------------------------------------------------------------- 1 | import { testRunner } from './testRunner'; 2 | 3 | (async () => { 4 | let result = 1; 5 | result = await testRunner(); 6 | result = await testRunner(true); 7 | 8 | process.exit(result); 9 | })(); 10 | -------------------------------------------------------------------------------- /src/commands/extension.ts: -------------------------------------------------------------------------------- 1 | import { StateManager } from '../workbench/stateManager'; 2 | import { workspace, window, commands } from 'vscode'; 3 | import { isValidKey } from '../graphql/graphClient'; 4 | import { log } from '../utils/logger'; 5 | 6 | export function deleteStudioApiKey() { 7 | StateManager.instance.globalState_userApiKey = ''; 8 | } 9 | 10 | export async function ensureFolderIsOpen() { 11 | if ( 12 | !workspace.workspaceFolders || 13 | (workspace.workspaceFolders && !workspace.workspaceFolders[0]) 14 | ) { 15 | const action = 'Open Folder'; 16 | const response = await window.showErrorMessage( 17 | 'You must open a folder to create Apollo Workbench files', 18 | action, 19 | ); 20 | if (response == action) await openFolder(); 21 | } 22 | } 23 | 24 | export async function enterGraphOSUserApiKey() { 25 | const apiKey = await window.showInputBox({ 26 | placeHolder: 'Enter User API Key - user:gh.michael-watson:023jr324tj....', 27 | }); 28 | if (apiKey && (await isValidKey(apiKey))) { 29 | log('GraphOS User API key validated and stored succesfully'); 30 | StateManager.instance.globalState_userApiKey = apiKey; 31 | } else if (apiKey) { 32 | log('API key was invalid'); 33 | window.showErrorMessage('Invalid API key entered'); 34 | } else if (apiKey == '') { 35 | log('No API key entered, login cancelled.'); 36 | window.setStatusBarMessage('Login cancelled, no API key entered', 2000); 37 | } 38 | } 39 | 40 | export async function signUp() { 41 | commands.executeCommand( 42 | 'vscode.open', 43 | 'https://studio.apollographql.com/signup?referrer=workbench', 44 | ); 45 | } 46 | 47 | export async function openFolder() { 48 | const folder = await window.showOpenDialog({ 49 | canSelectFiles: false, 50 | canSelectFolders: true, 51 | canSelectMany: false, 52 | }); 53 | if (folder) { 54 | try { 55 | await commands.executeCommand('vscode.openFolder', folder[0]); 56 | } catch (err) { 57 | console.log(err); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/commands/preloaded.ts: -------------------------------------------------------------------------------- 1 | import { ProgressLocation, Uri, commands, window, workspace } from 'vscode'; 2 | import { FileProvider } from '../workbench/file-system/fileProvider'; 3 | import { PreloadedSubgraph } from '../workbench/tree-data-providers/tree-items/graphos-supergraphs/preloadedSubgraph'; 4 | import { resolve } from 'path'; 5 | import { 6 | ApolloRemoteSchemaProvider, 7 | PreloadedSchemaProvider, 8 | } from '../workbench/docProviders'; 9 | import { StateManager } from '../workbench/stateManager'; 10 | import { normalizePath } from '../utils/path'; 11 | 12 | export async function viewPreloadedSchema(item: PreloadedSubgraph) { 13 | await PreloadedSchemaProvider.Open(item.wbFilePath, item.subgraphName); 14 | await window 15 | .showInformationMessage( 16 | `You are opening an example design that will be read-only. Would you like to copy the design locally to edit?`, 17 | 'Copy design', 18 | ) 19 | .then(async (value) => { 20 | if (value == 'Copy design') { 21 | const root = StateManager.workspaceRoot; 22 | if (root) { 23 | const wbFileUri = Uri.parse(item.wbFilePath); 24 | const schemasFolder = Uri.parse( 25 | `${item.wbFilePath.split('.yaml')[0]}-schemas`, 26 | ); 27 | 28 | await workspace.fs.copy( 29 | wbFileUri, 30 | Uri.parse(normalizePath(resolve(root, `${item.wbFileName}.yaml`))), 31 | { overwrite: true }, 32 | ); 33 | await workspace.fs.copy( 34 | schemasFolder, 35 | Uri.parse( 36 | normalizePath(resolve(root, `${item.wbFileName}-schemas`)), 37 | ), 38 | { overwrite: true }, 39 | ); 40 | StateManager.instance.localSupergraphTreeDataProvider.refresh(); 41 | commands.executeCommand( 42 | 'vscode.open', 43 | Uri.parse( 44 | normalizePath( 45 | resolve( 46 | root, 47 | `${item.wbFileName}-schemas`, 48 | `${item.subgraphName}.graphql`, 49 | ), 50 | ), 51 | ), 52 | ); 53 | commands.executeCommand('workbench.action.closeActiveEditor'); 54 | } 55 | } 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /src/commands/studio-graphs.ts: -------------------------------------------------------------------------------- 1 | import { StateManager } from '../workbench/stateManager'; 2 | import { env, ProgressLocation, Uri, window } from 'vscode'; 3 | import { 4 | ApolloRemoteSchemaProvider, 5 | ApolloStudioOperationsProvider, 6 | } from '../workbench/docProviders'; 7 | import { getUserMemberships } from '../graphql/graphClient'; 8 | import { enterGraphOSUserApiKey } from './extension'; 9 | import { log } from 'console'; 10 | import { StudioGraphTreeItem } from '../workbench/tree-data-providers/tree-items/graphos-supergraphs/studioGraphTreeItem'; 11 | import { StudioOperationTreeItem } from '../workbench/tree-data-providers/tree-items/graphos-operations/studioOperationTreeItem'; 12 | import { PreloadedSubgraph } from '../workbench/tree-data-providers/tree-items/graphos-supergraphs/preloadedSubgraph'; 13 | import { FileProvider } from '../workbench/file-system/fileProvider'; 14 | 15 | export async function openInGraphOS(item: StudioGraphTreeItem) { 16 | const url = `https://studio.apollographql.com/graph/${item.graphId}/home`; 17 | await env.openExternal(Uri.parse(url)); 18 | } 19 | 20 | export function refreshStudioGraphs() { 21 | StateManager.instance.apolloStudioGraphsProvider.refresh(); 22 | } 23 | 24 | export async function loadOperationsFromGraphOS( 25 | graphTreeItem: any, 26 | graphVariant?: string, 27 | ) { 28 | StateManager.instance.setSelectedGraph(graphTreeItem.graphId, graphVariant); 29 | } 30 | 31 | export async function viewStudioOperation(operation: StudioOperationTreeItem) { 32 | await window.showTextDocument( 33 | ApolloStudioOperationsProvider.Uri( 34 | operation.operationName, 35 | operation.operationSignature, 36 | ), 37 | ); 38 | } 39 | 40 | export async function switchOrg() { 41 | if (!StateManager.instance.globalState_userApiKey) 42 | await enterGraphOSUserApiKey(); 43 | 44 | let accountId = ''; 45 | const myAccountIds = await getUserMemberships(); 46 | const memberships = (myAccountIds?.me as any)?.memberships; 47 | if (memberships?.length > 1) { 48 | const accountMapping: { [key: string]: string } = {}; 49 | memberships.map((membership) => { 50 | const accountId = membership.account.id; 51 | const accountName = membership.account.name; 52 | accountMapping[accountName] = accountId; 53 | }); 54 | 55 | const selectedOrgName = 56 | (await window.showQuickPick(Object.keys(accountMapping), { 57 | placeHolder: 'Select an account to load graphs from', 58 | })) ?? ''; 59 | accountId = accountMapping[selectedOrgName]; 60 | } else if (memberships && memberships.length == 1) { 61 | accountId = memberships[0]?.account?.id ?? ''; 62 | } 63 | 64 | if (accountId) { 65 | StateManager.instance.setSelectedGraph(''); 66 | StateManager.instance.globalState_selectedApolloAccount = accountId; 67 | StateManager.instance.apolloStudioGraphsProvider.refresh(); 68 | } else { 69 | log('Unable to get orgs'); 70 | window.showErrorMessage( 71 | `Unable to get orgs. Did you delete your API key? Try logging out and then logging back in.`, 72 | ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/commands/studio-operations.ts: -------------------------------------------------------------------------------- 1 | import { FileProvider } from '../workbench/file-system/fileProvider'; 2 | import { window } from 'vscode'; 3 | import { getFileName } from '../utils/path'; 4 | import { StudioOperationTreeItem } from '../workbench/tree-data-providers/tree-items/graphos-operations/studioOperationTreeItem'; 5 | 6 | export async function addToDesign(op: StudioOperationTreeItem) { 7 | const supergraphs = FileProvider.instance.getWorkbenchFiles(); 8 | const supergraphNames: { [subgraphName: string]: string } = {}; 9 | supergraphs.forEach((wbFile, wbFilePath) => { 10 | const subgraphName = getFileName(wbFilePath); 11 | supergraphNames[subgraphName] = wbFilePath; 12 | }); 13 | 14 | const supergraphToAddOperationTo = await window.showQuickPick( 15 | Object.keys(supergraphNames), 16 | { 17 | placeHolder: 'Select the design to add the operation to', 18 | }, 19 | ); 20 | if (supergraphToAddOperationTo) { 21 | const wbFilePath = supergraphNames[supergraphToAddOperationTo]; 22 | const wbFile = FileProvider.instance.workbenchFileFromPath(wbFilePath); 23 | if (wbFile) { 24 | wbFile.operations[op.operationName] = { 25 | document: op.operationSignature, 26 | }; 27 | 28 | await FileProvider.instance.writeWorkbenchConfig( 29 | supergraphNames[supergraphToAddOperationTo], 30 | wbFile, 31 | ); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import { 2 | workspace, 3 | commands, 4 | languages, 5 | window, 6 | ExtensionContext, 7 | Uri, 8 | StatusBarItem, 9 | StatusBarAlignment, 10 | } from 'vscode'; 11 | 12 | import { StateManager } from './workbench/stateManager'; 13 | import { 14 | FileProvider, 15 | schemaFileUri, 16 | } from './workbench/file-system/fileProvider'; 17 | import { federationCompletionProvider } from './workbench/federationCompletionProvider'; 18 | import { FederationCodeActionProvider } from './workbench/federationCodeActionProvider'; 19 | import { 20 | ApolloRemoteSchemaProvider, 21 | ApolloStudioOperationsProvider, 22 | DesignOperationsDocumentProvider, 23 | PreloadedSchemaProvider, 24 | SubgraphUrlSchemaProvider, 25 | } from './workbench/docProviders'; 26 | import { addToDesign } from './commands/studio-operations'; 27 | import { 28 | ensureFolderIsOpen, 29 | enterGraphOSUserApiKey, 30 | deleteStudioApiKey as logout, 31 | signUp, 32 | } from './commands/extension'; 33 | import { 34 | refreshStudioGraphs as refreshSupergraphsFromGraphOS, 35 | loadOperationsFromGraphOS, 36 | viewStudioOperation, 37 | switchOrg, 38 | openInGraphOS, 39 | } from './commands/studio-graphs'; 40 | import { 41 | editSubgraph, 42 | deleteSubgraph, 43 | refreshSupergraphs, 44 | addSubgraph, 45 | viewSupergraphSchema, 46 | newDesign, 47 | newDesignFromGraphOSSupergraph, 48 | exportSupergraphSchema, 49 | addFederationDirective, 50 | startRoverDevSession, 51 | stopRoverDevSession, 52 | enableMocking, 53 | disableMocking, 54 | viewOperationDesignSideBySide, 55 | addOperation, 56 | checkSubgraphSchema, 57 | deleteOperation, 58 | addCustomMocksToSubgraph, 59 | changeDesignFederationVersion, 60 | } from './commands/local-supergraph-designs'; 61 | import { Rover } from './workbench/rover'; 62 | import { viewOperationDesign } from './workbench/webviews/operationDesign'; 63 | import { openSandbox, refreshSandbox } from './workbench/webviews/sandbox'; 64 | import { FederationReferenceProvider } from './workbench/federationReferenceProvider'; 65 | import { viewPreloadedSchema } from './commands/preloaded'; 66 | import { log } from './utils/logger'; 67 | import { SubgraphWatcher } from './utils/subgraphWatcher'; 68 | 69 | export const outputChannel = window.createOutputChannel('Apollo Workbench'); 70 | 71 | process.stdin.resume(); // so the program will not close instantly 72 | 73 | function exitHandler(options, exitCode) { 74 | if (options.cleanup) Rover.instance.stopRoverDev(); 75 | if (exitCode || exitCode === 0) console.log(exitCode); 76 | if (options.exit) process.exit(); 77 | } 78 | 79 | // do something when app is closing 80 | process.on('exit', exitHandler.bind(null, { cleanup: true })); 81 | 82 | // catches ctrl+c event 83 | process.on('SIGINT', exitHandler.bind(null, { exit: true })); 84 | 85 | // catches "kill pid" (for example: nodemon restart) 86 | process.on('SIGUSR1', exitHandler.bind(null, { exit: true })); 87 | process.on('SIGUSR2', exitHandler.bind(null, { exit: true })); 88 | 89 | // catches uncaught exceptions 90 | process.on('uncaughtException', exitHandler.bind(null, { exit: true })); 91 | 92 | // Our event when vscode deactivates 93 | export async function deactivate(context: ExtensionContext) { 94 | await Rover.instance.stopRoverDev(); 95 | } 96 | 97 | export let statusBar: StatusBarItem; 98 | 99 | export async function activate(context: ExtensionContext) { 100 | StateManager.init(context); 101 | context.workspaceState.update('selectedWbFile', ''); 102 | context.globalState.update('APOLLO_SELECTED_GRAPH_ID', ''); 103 | statusBar = window.createStatusBarItem(StatusBarAlignment.Right, 100); 104 | context.subscriptions.push(statusBar); 105 | 106 | languages.registerCompletionItemProvider( 107 | 'graphql', 108 | federationCompletionProvider, 109 | ); 110 | languages.registerCodeActionsProvider( 111 | { language: 'graphql' }, 112 | new FederationCodeActionProvider(), 113 | ); 114 | 115 | //Register Tree Data Providers 116 | window.registerTreeDataProvider( 117 | 'local-supergraph-designs', 118 | StateManager.instance.localSupergraphTreeDataProvider, 119 | ); 120 | window.registerTreeDataProvider( 121 | 'studio-graphs', 122 | StateManager.instance.apolloStudioGraphsProvider, 123 | ); 124 | window.registerTreeDataProvider( 125 | 'studio-operations', 126 | StateManager.instance.apolloStudioGraphOpsProvider, 127 | ); 128 | 129 | //Register commands to ensure a folder is open in the window to store workbench files 130 | commands.registerCommand('extension.ensureFolderIsOpen', ensureFolderIsOpen); 131 | commands.executeCommand('extension.ensureFolderIsOpen'); 132 | //Global Extension Commands 133 | commands.registerCommand('extension.login', enterGraphOSUserApiKey); 134 | commands.registerCommand('extension.logout', logout); 135 | commands.registerCommand('extension.signUp', signUp); 136 | 137 | //*Local Supergraph Designs TreeView 138 | //**Navigation Menu Commands 139 | commands.registerCommand('local-supergraph-designs.newDesign', newDesign); 140 | commands.registerCommand( 141 | 'local-supergraph-designs.refresh', 142 | refreshSupergraphs, 143 | ); 144 | //***Supergraph Schema Commands 145 | commands.registerCommand( 146 | 'local-supergraph-designs.viewSupergraphSchema', 147 | viewSupergraphSchema, 148 | ); //on-click 149 | commands.registerCommand( 150 | 'local-supergraph-designs.exportSupergraphSchema', 151 | exportSupergraphSchema, 152 | ); //right-click 153 | //****Subgraph Summary Commands 154 | commands.registerCommand('local-supergraph-designs.addSubgraph', addSubgraph); 155 | //****Subgraph Commands 156 | commands.registerCommand( 157 | 'local-supergraph-designs.editSubgraph', 158 | editSubgraph, 159 | ); //on-click 160 | commands.registerCommand( 161 | 'local-supergraph-designs.deleteSubgraph', 162 | deleteSubgraph, 163 | ); 164 | commands.registerCommand( 165 | 'local-supergraph-designs.checkSubgraphSchema', 166 | checkSubgraphSchema, 167 | ); 168 | commands.registerCommand( 169 | 'local-supergraph-designs.enableMocking', 170 | enableMocking, 171 | ); 172 | commands.registerCommand( 173 | 'local-supergraph-designs.disableMocking', 174 | disableMocking, 175 | ); 176 | commands.registerCommand( 177 | 'local-supergraph-designs.addCustomMocksToSubgraph', 178 | addCustomMocksToSubgraph, 179 | ); 180 | commands.registerCommand( 181 | 'local-supergraph-designs.startRoverDevSession', 182 | startRoverDevSession, 183 | ); 184 | commands.registerCommand( 185 | 'local-supergraph-designs.changeDesignFederationVersion', 186 | changeDesignFederationVersion, 187 | ); 188 | 189 | commands.registerCommand( 190 | 'local-supergraph-designs.stopRoverDevSession', 191 | stopRoverDevSession, 192 | ); 193 | 194 | context.subscriptions.push( 195 | commands.registerCommand( 196 | 'local-supergraph-designs.viewOperationDesignSideBySide', 197 | viewOperationDesignSideBySide, 198 | ), 199 | ); 200 | context.subscriptions.push( 201 | commands.registerCommand('local-supergraph-designs.sandbox', openSandbox), 202 | ); 203 | context.subscriptions.push( 204 | commands.registerCommand( 205 | 'local-supergraph-designs.refreshSandbox', 206 | refreshSandbox, 207 | ), 208 | ); 209 | context.subscriptions.push( 210 | commands.registerCommand( 211 | 'local-supergraph-designs.addOperation', 212 | addOperation, 213 | ), 214 | ); 215 | context.subscriptions.push( 216 | commands.registerCommand( 217 | 'local-supergraph-designs.deleteOperation', 218 | deleteOperation, 219 | ), 220 | ); 221 | context.subscriptions.push( 222 | commands.registerCommand( 223 | 'local-supergraph-designs.viewOperationDesign', 224 | viewOperationDesign, 225 | ), 226 | ); 227 | 228 | commands.registerCommand( 229 | 'current-workbench-schemas.addFederationDirective', 230 | addFederationDirective, 231 | ); 232 | //Preloaded Commands 233 | commands.registerCommand( 234 | 'preloaded.viewPreloadedSchema', 235 | viewPreloadedSchema, 236 | ); 237 | 238 | //Apollo Studio Graphs Commands 239 | commands.registerCommand( 240 | 'studio-graphs.refreshSupergraphsFromGraphOS', 241 | refreshSupergraphsFromGraphOS, 242 | ); 243 | commands.registerCommand('studio-graphs.openInGraphOS', openInGraphOS); 244 | commands.registerCommand( 245 | 'studio-graphs.newDesignFromGraphOSSupergraph', 246 | newDesignFromGraphOSSupergraph, 247 | ); 248 | commands.registerCommand( 249 | 'studio-graphs.loadOperationsFromGraphOS', 250 | loadOperationsFromGraphOS, 251 | ); 252 | commands.registerCommand( 253 | 'studio-graphs.viewStudioOperation', 254 | viewStudioOperation, 255 | ); 256 | commands.registerCommand('studio-graphs.switchOrg', switchOrg); 257 | //Apollo Studio Graph Operations Commands 258 | commands.registerCommand('studio-operations.addToDesign', addToDesign); 259 | 260 | //Workspace - Register Providers and Events 261 | workspace.registerTextDocumentContentProvider( 262 | ApolloStudioOperationsProvider.scheme, 263 | new ApolloStudioOperationsProvider(), 264 | ); 265 | workspace.registerTextDocumentContentProvider( 266 | SubgraphUrlSchemaProvider.scheme, 267 | new SubgraphUrlSchemaProvider(), 268 | ); 269 | workspace.registerTextDocumentContentProvider( 270 | ApolloRemoteSchemaProvider.scheme, 271 | new ApolloRemoteSchemaProvider(), 272 | ); 273 | workspace.registerTextDocumentContentProvider( 274 | PreloadedSchemaProvider.scheme, 275 | new PreloadedSchemaProvider(), 276 | ); 277 | languages.registerReferenceProvider( 278 | { language: 'graphql' }, 279 | new FederationReferenceProvider(), 280 | ); 281 | 282 | workspace.registerFileSystemProvider( 283 | DesignOperationsDocumentProvider.scheme, 284 | new DesignOperationsDocumentProvider(), 285 | { 286 | isCaseSensitive: true, 287 | }, 288 | ); 289 | 290 | SubgraphWatcher.instance.start(); 291 | 292 | workspace.onDidDeleteFiles((e) => { 293 | let deletedWorkbenchFile = false; 294 | e.files.forEach((f) => { 295 | const wbFile = FileProvider.instance.workbenchFileFromPath(f.fsPath); 296 | if (wbFile) deletedWorkbenchFile = true; 297 | }); 298 | if (deletedWorkbenchFile) 299 | StateManager.instance.localSupergraphTreeDataProvider.refresh(); 300 | }); 301 | workspace.onDidSaveTextDocument((doc) => { 302 | const docPath = doc.uri.fsPath; 303 | 304 | FileProvider.instance 305 | .getWorkbenchFiles() 306 | .forEach(async (wbFile, wbFilePath) => { 307 | if (docPath == wbFilePath) 308 | StateManager.instance.localSupergraphTreeDataProvider.refresh(); 309 | else 310 | Object.keys(wbFile.subgraphs).forEach(async (subgraphName) => { 311 | const { file, workbench_design } = 312 | wbFile.subgraphs[subgraphName].schema; 313 | let schemaUri: Uri | undefined; 314 | if (workbench_design) 315 | schemaUri = schemaFileUri(workbench_design, wbFilePath); 316 | else if (file) schemaUri = schemaFileUri(file, wbFilePath); 317 | 318 | if (schemaUri && schemaUri.fsPath == docPath) { 319 | const composedSchema = 320 | await FileProvider.instance.refreshWorkbenchFileComposition( 321 | wbFilePath, 322 | ); 323 | 324 | if (!composedSchema && Rover.instance.primaryDevTerminal) { 325 | window.showErrorMessage( 326 | `Stopping rover dev session because of invalid composition`, 327 | ); 328 | Rover.instance.stopRoverDev(); 329 | commands.executeCommand('workbench.action.showErrorsWarnings'); 330 | } 331 | } else if ( 332 | docPath == 333 | wbFile.subgraphs[subgraphName].schema.mocks?.customMocks 334 | ) { 335 | await Rover.instance.restartMockedSubgraph( 336 | subgraphName, 337 | wbFile.subgraphs[subgraphName], 338 | ); 339 | } 340 | }); 341 | }); 342 | }); 343 | } 344 | -------------------------------------------------------------------------------- /src/graphql/graphClient.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import fetch from 'node-fetch'; 3 | import { 4 | createHttpLink, 5 | DocumentNode, 6 | execute, 7 | gql, 8 | toPromise, 9 | } from '@apollo/client/core'; 10 | import { GraphOperations } from './types/GraphOperations'; 11 | import { 12 | CheckUserApiKeyQuery, 13 | CheckUserApiKeyDocument, 14 | UserMembershipsDocument, 15 | UserMembershipsQuery, 16 | AccountServiceVariantsDocument, 17 | AccountServiceVariantsQuery, 18 | GetGraphSchemasDocument, 19 | GetGraphSchemasQuery, 20 | } from '../_generated_/typed-document-nodes'; 21 | import { StateManager } from '../workbench/stateManager'; 22 | import { getOperationName } from '@apollo/client/utilities'; 23 | 24 | const getGraphOperations = gql` 25 | query GraphOperations($id: ID!, $from: Timestamp!, $variant: String) { 26 | service(id: $id) { 27 | title 28 | statsWindow(from: $from) { 29 | queryStats(filter: { schemaTag: $variant }) { 30 | groupBy { 31 | queryName 32 | queryId 33 | querySignature 34 | } 35 | } 36 | } 37 | } 38 | } 39 | `; 40 | 41 | export async function isValidKey(apiKey: string) { 42 | const result = await toPromise( 43 | execute( 44 | createLink(getOperationName(CheckUserApiKeyDocument) ?? '', apiKey), 45 | { 46 | query: CheckUserApiKeyDocument, 47 | }, 48 | ), 49 | ); 50 | const data = result.data as CheckUserApiKeyQuery; 51 | if (data.me?.id) return true; 52 | return false; 53 | } 54 | 55 | export async function getUserMemberships() { 56 | const result = await useQuery(UserMembershipsDocument); 57 | return result.data as UserMembershipsQuery; 58 | } 59 | 60 | export async function getAccountGraphs(accountId: string) { 61 | const result = await useQuery(AccountServiceVariantsDocument, { 62 | accountId: accountId, 63 | }); 64 | return result.data as AccountServiceVariantsQuery; 65 | } 66 | 67 | export async function getGraphOps(graphId: string, graphVariant: string) { 68 | const days = StateManager.settings_daysOfOperationsToFetch; 69 | const result = await useQuery(getGraphOperations, { 70 | id: graphId, 71 | from: (-86400 * days).toString(), 72 | variant: graphVariant, 73 | }); 74 | 75 | return result.data as GraphOperations; 76 | } 77 | 78 | export async function getGraphSchemasByVariant( 79 | graphId: string, 80 | graphVariant: string, 81 | ) { 82 | const result = await useQuery(GetGraphSchemasDocument, { 83 | id: graphId, 84 | graphVariant, 85 | }); 86 | return result.data as GetGraphSchemasQuery; 87 | } 88 | 89 | const useQuery = (query: DocumentNode, variables = {}) => 90 | toPromise( 91 | execute(createLink(getOperationName(query) ?? ''), { 92 | query, 93 | variables, 94 | }), 95 | ); 96 | 97 | // eslint-disable-next-line @typescript-eslint/no-var-requires 98 | const { version } = require('../../package.json'); 99 | function createLink( 100 | operationName: string, 101 | apiKey: string = StateManager.instance.globalState_userApiKey, 102 | ) { 103 | const userId = apiKey?.split(':')[1]; 104 | const headers = { 105 | 'x-api-key': apiKey, 106 | 'studio-user-id': userId, 107 | 'apollographql-client-name': 'Apollo Workbench', 108 | 'apollographql-client-version': version, 109 | }; 110 | 111 | if (StateManager.settings_apolloOrg) headers['apollo-sudo'] = 'true'; 112 | 113 | let uri = 114 | operationName == 'GraphOperations' 115 | ? 'https://graphql.api.apollographql.com/api/graphql' 116 | : 'https://api.apollographql.com/graphql'; 117 | 118 | if (StateManager.settings_apolloApiUrl) 119 | uri = StateManager.settings_apolloApiUrl; 120 | 121 | return createHttpLink({ 122 | fetch: fetch as any, 123 | headers, 124 | uri, 125 | }); 126 | } 127 | -------------------------------------------------------------------------------- /src/graphql/queries/AccountServiceVariants.graphql: -------------------------------------------------------------------------------- 1 | query AccountGraphVariants($accountId: ID!) { 2 | organization(id: $accountId) { 3 | name 4 | graphs(includeDeleted: false) { 5 | id 6 | title 7 | variants { 8 | name 9 | } 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/graphql/queries/CheckUserApiKey.graphql: -------------------------------------------------------------------------------- 1 | query CheckUserApiKey { 2 | me { 3 | id 4 | } 5 | } -------------------------------------------------------------------------------- /src/graphql/queries/GetGraphSchemas.graphql: -------------------------------------------------------------------------------- 1 | query GetGraphSchemasForNewDesign($id: ID!, $graphVariant: String!) { 2 | graph(id: $id) { 3 | variant(name: $graphVariant) { 4 | subgraphs { 5 | name 6 | url 7 | activePartialSchema { 8 | sdl 9 | } 10 | } 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /src/graphql/queries/UserMemberships.graphql: -------------------------------------------------------------------------------- 1 | query UserMemberships { 2 | me { 3 | ... on User { 4 | memberships { 5 | account { 6 | id 7 | name 8 | } 9 | } 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/graphql/types/GraphOperations.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | // ==================================================== 7 | // GraphQL query operation: GraphOperations 8 | // ==================================================== 9 | 10 | export interface GraphOperations_service_statsWindow_queryStats_groupBy { 11 | __typename: "ServiceQueryStatsDimensions"; 12 | queryName: string | null; 13 | queryId: string | null; 14 | querySignature: string | null; 15 | } 16 | 17 | export interface GraphOperations_service_statsWindow_queryStats { 18 | __typename: "ServiceQueryStatsRecord"; 19 | /** 20 | * Dimensions of ServiceQueryStats that can be grouped by. 21 | */ 22 | groupBy: GraphOperations_service_statsWindow_queryStats_groupBy; 23 | } 24 | 25 | export interface GraphOperations_service_statsWindow { 26 | __typename: "ServiceStatsWindow"; 27 | queryStats: GraphOperations_service_statsWindow_queryStats[]; 28 | } 29 | 30 | export interface GraphOperations_service { 31 | __typename: "Service"; 32 | /** 33 | * The graph's name. 34 | */ 35 | title: string; 36 | statsWindow: GraphOperations_service_statsWindow | null; 37 | } 38 | 39 | export interface GraphOperations { 40 | /** 41 | * Service by ID 42 | */ 43 | service: GraphOperations_service | null; 44 | } 45 | 46 | export interface GraphOperationsVariables { 47 | id: string; 48 | from: any; 49 | variant?: string | null; 50 | } 51 | -------------------------------------------------------------------------------- /src/graphql/types/globalTypes.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | // @generated 4 | // This file was automatically generated and should not be edited. 5 | 6 | //============================================================== 7 | // START Enums and Input Objects 8 | //============================================================== 9 | 10 | //============================================================== 11 | // END Enums and Input Objects 12 | //============================================================== 13 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import { outputChannel } from "../extension"; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-var-requires 4 | const { name } = require('../../package.json'); 5 | 6 | export function log(str: string) { 7 | outputChannel.appendLine(`${str}`); 8 | } 9 | 10 | //Redirect log to Output tab in extension 11 | // console.log = function (str: string) { 12 | 13 | // if (str.includes('apollo-workbench:')) { 14 | // outputChannel.appendLine(str); 15 | // } else if (str.includes('Checking for composition updates')) { 16 | // } else if (str.includes('No change in service definitions since last check')) { 17 | // } else if (str.includes('Schema loaded and ready for execution')) { 18 | // outputChannel.appendLine(`${name}:${str}`); 19 | // } else { 20 | // const strings = str.split('\n'); 21 | // strings.forEach(s => outputChannel.appendLine(`\t${s}`)); 22 | // } 23 | // }; 24 | -------------------------------------------------------------------------------- /src/utils/path.ts: -------------------------------------------------------------------------------- 1 | import { Uri } from 'vscode'; 2 | import { Utils } from 'vscode-uri'; 3 | 4 | export function getFileName(filePath: string) { 5 | return Utils.basename(Uri.parse(filePath)).split('.')[0]; 6 | } 7 | 8 | export function normalizePath(filePath: string) { 9 | if (filePath.toLowerCase().includes('c:')) 10 | filePath = filePath.slice(2).replace(/\\/g, '/'); 11 | 12 | return filePath; 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/subgraphWatcher.ts: -------------------------------------------------------------------------------- 1 | import { Uri, workspace } from 'vscode'; 2 | import { FileProvider } from '../workbench/file-system/fileProvider'; 3 | import { Rover } from '../workbench/rover'; 4 | import { log } from './logger'; 5 | import { StateManager } from '../workbench/stateManager'; 6 | 7 | export interface Watcher { 8 | wbFilePath: string; 9 | subgraphName: string; 10 | url: string; 11 | } 12 | 13 | export class SubgraphWatcher { 14 | private watchers: Array = []; 15 | 16 | private _isRunning = false; 17 | 18 | static instance = new SubgraphWatcher(); 19 | 20 | refresh() { 21 | this.watchers = []; 22 | const wbFiles = FileProvider.instance.getWorkbenchFiles(); 23 | wbFiles.forEach((wbFile, wbFilePath) => { 24 | Object.keys(wbFile.subgraphs).forEach((subgraphName) => { 25 | const subgraph = wbFile.subgraphs[subgraphName]; 26 | if (subgraph.schema.subgraph_url != undefined) { 27 | this.watchers.push({ 28 | wbFilePath, 29 | subgraphName, 30 | url: subgraph.schema.subgraph_url, 31 | }); 32 | } 33 | }); 34 | }); 35 | 36 | this.start(); 37 | } 38 | 39 | start() { 40 | if (this._isRunning) return; 41 | this._isRunning = true; 42 | this.watch(); 43 | } 44 | stop() { 45 | this._isRunning = false; 46 | } 47 | 48 | private async watch() { 49 | while (this._isRunning && StateManager.settings_enableSubgraphUrlWatcher) { 50 | const loopStart = Date.now(); 51 | for (let i = 0; i < this.watchers.length; i++) { 52 | const watcher = this.watchers[i]; 53 | const didUpdate = 54 | await FileProvider.instance.updateTempSubgraphUrlSchema( 55 | watcher.wbFilePath, 56 | watcher.subgraphName, 57 | watcher.url, 58 | ); 59 | 60 | if (didUpdate) { 61 | log( 62 | `Found update for ${watcher.subgraphName} running at ${watcher.url}`, 63 | ); 64 | await FileProvider.instance.refreshWorkbenchFileComposition( 65 | watcher.wbFilePath, 66 | ); 67 | } 68 | } 69 | const loopTimeElapsed = Date.now() - loopStart; 70 | if (loopTimeElapsed < StateManager.settings_subgraphWatcherPingInterval) { 71 | await sleep( 72 | StateManager.settings_subgraphWatcherPingInterval - loopTimeElapsed, 73 | ); 74 | } 75 | } 76 | 77 | this._isRunning = false; 78 | } 79 | } 80 | const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); 81 | -------------------------------------------------------------------------------- /src/utils/uiHelpers.ts: -------------------------------------------------------------------------------- 1 | import { window } from 'vscode'; 2 | import { ApolloConfig } from '../workbench/file-system/ApolloConfig'; 3 | import { FileProvider } from '../workbench/file-system/fileProvider'; 4 | import { getFileName } from './path'; 5 | 6 | /** 7 | * Prompts the user to select a design from the currently loaded folder 8 | * @returns File path to config or `undefined` if cancelled by user 9 | */ 10 | export async function whichDesign() { 11 | const options: { 12 | [key: string]: string; 13 | } = {}; 14 | const wbFiles = FileProvider.instance.getWorkbenchFiles(); 15 | wbFiles.forEach((wbFile, wbFilePath) => { 16 | options[getFileName(wbFilePath)] = wbFilePath; 17 | }); 18 | const optionSelected = await window.showQuickPick(Object.keys(options), { 19 | title: 'Select a design', 20 | }); 21 | if (!optionSelected) return undefined; 22 | 23 | return options[optionSelected]; 24 | } 25 | /** 26 | * Prompts the user to select a design from the currently loaded folder 27 | * @returns File path to config or `undefined` if cancelled by user 28 | */ 29 | export async function whichSubgraph(wbFilePath: string) { 30 | const wbFile= FileProvider.instance.workbenchFileFromPath(wbFilePath); 31 | const optionSelected = await window.showQuickPick(Object.keys(wbFile.subgraphs).sort(), { 32 | title: 'Select a subgraph', 33 | }); 34 | if (!optionSelected) return undefined; 35 | 36 | return optionSelected; 37 | } 38 | /** 39 | * Prompts the user to select an operations from the design file 40 | * @returns File path to config or `undefined` if cancelled by user 41 | */ 42 | export async function whichOperation(wbFilePath: string) { 43 | const wbFile= FileProvider.instance.workbenchFileFromPath(wbFilePath); 44 | const optionSelected = await window.showQuickPick(Object.keys(wbFile.operations).sort(), { 45 | title: 'Select a subgraph', 46 | }); 47 | if (!optionSelected) return undefined; 48 | 49 | return optionSelected; 50 | } -------------------------------------------------------------------------------- /src/utils/uri.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { StateManager } from '../workbench/stateManager'; 3 | import { Uri } from 'vscode'; 4 | import { normalizePath } from './path'; 5 | 6 | export const resolvePath = (filePath: string) => { 7 | const path = resolve(StateManager.workspaceRoot ?? '', filePath); 8 | return Uri.parse(normalizePath(path)); 9 | }; 10 | -------------------------------------------------------------------------------- /src/workbench/diagnosticsManager.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Diagnostic, 3 | DiagnosticCollection, 4 | DiagnosticSeverity, 5 | languages, 6 | Range, 7 | Uri, 8 | } from 'vscode'; 9 | import { ApolloConfig } from './file-system/ApolloConfig'; 10 | import { RoverCompositionError } from './file-system/CompositionResults'; 11 | import { 12 | FileProvider, 13 | schemaFileUri, 14 | tempSchemaFilePath, 15 | } from './file-system/fileProvider'; 16 | import { StateManager } from './stateManager'; 17 | import { getFileName } from '../utils/path'; 18 | import { ApolloRemoteSchemaProvider } from './docProviders'; 19 | import { existsSync, readFileSync } from 'fs'; 20 | import { resolvePath } from '../utils/uri'; 21 | import { buildSchema } from 'graphql'; 22 | import { buildSubgraphSchema } from '@apollo/subgraph'; 23 | import gql from 'graphql-tag'; 24 | 25 | interface WorkbenchDiagnosticCollections { 26 | compositionDiagnostics: DiagnosticCollection; 27 | } 28 | 29 | export class WorkbenchDiagnostics { 30 | public static instance: WorkbenchDiagnostics = new WorkbenchDiagnostics(); 31 | 32 | diagnosticCollections: Map = new Map< 33 | string, 34 | WorkbenchDiagnosticCollections 35 | >(); 36 | 37 | createWorkbenchFileDiagnostics(graphName: string, wbFilePath: string) { 38 | if (this.diagnosticCollections.has(wbFilePath)) { 39 | const collection = this.getWorkbenchDiagnostics(wbFilePath); 40 | collection?.compositionDiagnostics.clear(); 41 | } else { 42 | const compositionDiagnostics = languages.createDiagnosticCollection( 43 | `${graphName}-composition`, 44 | ); 45 | const operationDiagnostics = languages.createDiagnosticCollection( 46 | `${graphName}-composition`, 47 | ); 48 | 49 | StateManager.instance.context?.subscriptions.push(compositionDiagnostics); 50 | StateManager.instance.context?.subscriptions.push(operationDiagnostics); 51 | 52 | this.diagnosticCollections.set(wbFilePath, { 53 | compositionDiagnostics, 54 | }); 55 | } 56 | } 57 | 58 | async setCompositionErrors( 59 | wbFilePath: string, 60 | wbFile: ApolloConfig, 61 | errors: RoverCompositionError[], 62 | ) { 63 | const compositionDiagnostics = this.getCompositionDiagnostics(wbFilePath); 64 | compositionDiagnostics.clear(); 65 | 66 | const diagnosticsGroups = this.handleErrors(wbFilePath, errors); 67 | for (const sn in diagnosticsGroups) { 68 | const subgraph = wbFile.subgraphs[sn]; 69 | if (subgraph) { 70 | if (subgraph.schema.file) { 71 | compositionDiagnostics.set( 72 | schemaFileUri(subgraph.schema.file, wbFilePath), 73 | diagnosticsGroups[sn], 74 | ); 75 | } else if (subgraph.schema.workbench_design) { 76 | compositionDiagnostics.set( 77 | schemaFileUri(subgraph.schema.workbench_design, wbFilePath), 78 | diagnosticsGroups[sn], 79 | ); 80 | } 81 | // Account for a local change to remote source that we can't edit 82 | else { 83 | compositionDiagnostics.set( 84 | ApolloRemoteSchemaProvider.Uri(wbFilePath, sn), 85 | diagnosticsGroups[sn], 86 | ); 87 | } 88 | } else { 89 | compositionDiagnostics.set( 90 | Uri.parse(wbFilePath), 91 | diagnosticsGroups[sn], 92 | ); 93 | } 94 | } 95 | } 96 | 97 | clearCompositionDiagnostics(wbFilePath: string) { 98 | this.getCompositionDiagnostics(wbFilePath).clear(); 99 | } 100 | clearAllDiagnostics() { 101 | this.diagnosticCollections.forEach((diagnosticCollection) => { 102 | diagnosticCollection.compositionDiagnostics.clear(); 103 | }); 104 | } 105 | private getCompositionDiagnostics(wbFilePath: string): DiagnosticCollection { 106 | return this.getWorkbenchDiagnostics(wbFilePath).compositionDiagnostics; 107 | } 108 | private getWorkbenchDiagnostics(wbFilePath: string) { 109 | const wbFileDiagnostics = this.diagnosticCollections.get(wbFilePath); 110 | if (wbFileDiagnostics != undefined) return wbFileDiagnostics; 111 | else throw new Error(`No Operation Diagnostic found for ${wbFilePath}`); 112 | } 113 | private handleErrors(wbFilePath: string, errors: RoverCompositionError[]) { 114 | const diagnosticsGroups: { [key: string]: Diagnostic[] } = {}; 115 | const wbFile = FileProvider.instance.workbenchFileFromPath(wbFilePath); 116 | 117 | for (let i = 0; i < errors.length; i++) { 118 | const error = errors[i]; 119 | const errorMessage = error.message; 120 | 121 | if (error.nodes && error.nodes.length > 0) { 122 | if (error.code == 'INVALID_GRAPHQL') { 123 | console.log(error); 124 | error.nodes.forEach((errorNode) => { 125 | const subgraphName = errorNode.subgraph; 126 | try { 127 | buildSubgraphSchema({ typeDefs: gql(errorNode.source) }); 128 | } catch (error) { 129 | console.log(error); 130 | let range: Range | undefined = undefined; 131 | 132 | if((error as any)?.locations){ 133 | const location = (error as any)?.locations[0]; 134 | range = new Range( 135 | location.line - 1, 136 | location.column - 1, 137 | location.line - 1, 138 | location.column - 1, 139 | ); 140 | } 141 | const diagnostic = this.generateDiagnostic(errorMessage, range); 142 | 143 | if (!diagnosticsGroups[subgraphName]) 144 | diagnosticsGroups[subgraphName] = [diagnostic]; 145 | else diagnosticsGroups[subgraphName].push(diagnostic); 146 | } 147 | }); 148 | } else { 149 | error.nodes.forEach((errorNode) => { 150 | let subgraphName = errorNode.subgraph; 151 | if (!subgraphName && errorMessage.slice(0, 1) == '[') { 152 | subgraphName = errorMessage.split(']')[0].split('[')[1]; 153 | } else if (!subgraphName) { 154 | Object.keys(wbFile.subgraphs).forEach((subgraph) => { 155 | const schema = wbFile.subgraphs[subgraph].schema; 156 | let schemaPath: Uri; 157 | if (schema.workbench_design) { 158 | schemaPath = resolvePath(schema.workbench_design); 159 | } else if (schema.file) { 160 | schemaPath = resolvePath(schema.file); 161 | } else { 162 | schemaPath = ApolloRemoteSchemaProvider.Uri( 163 | wbFilePath, 164 | subgraphName, 165 | ); 166 | } 167 | if (existsSync(schemaPath.fsPath)) { 168 | const typeDefs = readFileSync(schemaPath.fsPath, { 169 | encoding: 'utf-8', 170 | }); 171 | if (errorNode.source == typeDefs) subgraphName = subgraph; 172 | } 173 | }); 174 | 175 | if (!subgraphName) subgraphName = getFileName(wbFilePath); 176 | } 177 | 178 | const range = new Range( 179 | errorNode.start ? errorNode.start.line - 1 : 0, 180 | errorNode.start ? errorNode.start.column - 1 : 0, 181 | errorNode.end ? errorNode.end.line - 1 : 0, 182 | errorNode.start && errorNode.end 183 | ? errorNode.start.column + errorNode.end.column - 1 184 | : 0, 185 | ); 186 | const diagnostic = this.generateDiagnostic(errorMessage, range); 187 | 188 | if (!diagnosticsGroups[subgraphName]) 189 | diagnosticsGroups[subgraphName] = [diagnostic]; 190 | else diagnosticsGroups[subgraphName].push(diagnostic); 191 | }); 192 | } 193 | } else { 194 | const diagnostic = this.generateDiagnostic(errorMessage); 195 | 196 | if (!diagnosticsGroups[wbFilePath]) 197 | diagnosticsGroups[wbFilePath] = [diagnostic]; 198 | else diagnosticsGroups[wbFilePath].push(diagnostic); 199 | } 200 | } 201 | 202 | return diagnosticsGroups; 203 | } 204 | private generateDiagnostic( 205 | message: string, 206 | range: Range = new Range(0, 0, 0, 0), 207 | severity: DiagnosticSeverity = DiagnosticSeverity.Error, 208 | ) { 209 | const diagnostic = new Diagnostic(range, message, severity); 210 | 211 | if (message.includes('If you meant the "@')) 212 | diagnostic.code = `addDirective:${message.split('"')[1]}`; 213 | 214 | return diagnostic; 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/workbench/docProviders.ts: -------------------------------------------------------------------------------- 1 | import { getOperationAST, parse, print } from 'graphql'; 2 | import { resolve } from 'path'; 3 | import { TextDecoder, TextEncoder } from 'util'; 4 | import { 5 | Disposable, 6 | EventEmitter, 7 | FileChangeEvent, 8 | FileStat, 9 | FileSystemProvider, 10 | FileType, 11 | TextDocumentContentProvider, 12 | TreeItem, 13 | Uri, 14 | window, 15 | workspace, 16 | } from 'vscode'; 17 | import { FileProvider } from './file-system/fileProvider'; 18 | import { normalizePath } from '../utils/path'; 19 | import { Rover } from './rover'; 20 | 21 | export class DesignOperationsDocumentProvider implements FileSystemProvider { 22 | static scheme = 'workbench'; 23 | static Uri(wbFilePath: string, operationName: string): Uri { 24 | return Uri.parse( 25 | `${DesignOperationsDocumentProvider.scheme}:${resolve( 26 | operationName, 27 | )}.graphql?${wbFilePath}`, 28 | ); 29 | } 30 | 31 | onDidChangeEmitter = new EventEmitter(); 32 | onDidChangeFile = this.onDidChangeEmitter.event; 33 | 34 | readFile(uri: Uri): Uint8Array | Thenable { 35 | const { operationName, wbFilePath } = this.getDetails(uri); 36 | const wbFile = FileProvider.instance.workbenchFileFromPath(wbFilePath); 37 | const document = wbFile.operations[operationName].document; 38 | 39 | return new TextEncoder().encode(print(parse(document))); 40 | } 41 | writeFile( 42 | uri: Uri, 43 | content: Uint8Array, 44 | options: { readonly create: boolean; readonly overwrite: boolean }, 45 | ): void | Thenable { 46 | const { operationName, wbFilePath } = this.getDetails(uri); 47 | const formattedDoc = print(parse(content.toString())); 48 | const wbFile = FileProvider.instance.workbenchFileFromPath(wbFilePath); 49 | 50 | if (wbFile.operations[operationName].document) 51 | wbFile.operations[operationName].document = formattedDoc; 52 | else wbFile.operations[operationName] = { document: formattedDoc }; 53 | 54 | return FileProvider.instance.writeWorkbenchConfig( 55 | wbFilePath, 56 | wbFile, 57 | false, 58 | ); 59 | } 60 | 61 | private getDetails(uri: Uri) { 62 | return { 63 | operationName: uri.path.split('.')[0].replaceAll('/', ''), 64 | wbFilePath: uri.query, 65 | }; 66 | } 67 | 68 | //Methods not used 69 | watch( 70 | uri: Uri, 71 | options: { 72 | readonly recursive: boolean; 73 | readonly excludes: readonly string[]; 74 | }, 75 | ): Disposable { 76 | return new Disposable(() => undefined); 77 | } 78 | stat(uri: Uri): FileStat | Thenable { 79 | const now = Date.now(); 80 | return { 81 | ctime: now, 82 | mtime: now, 83 | size: 0, 84 | type: FileType.File, 85 | }; 86 | } 87 | readDirectory( 88 | uri: Uri, 89 | ): [string, FileType][] | Thenable<[string, FileType][]> { 90 | throw new Error('Method not implemented.'); 91 | } 92 | createDirectory(uri: Uri): void | Thenable { 93 | throw new Error('Method not implemented.'); 94 | } 95 | delete( 96 | uri: Uri, 97 | options: { readonly recursive: boolean }, 98 | ): void | Thenable { 99 | throw new Error('Method not implemented.'); 100 | } 101 | rename( 102 | oldUri: Uri, 103 | newUri: Uri, 104 | options: { readonly overwrite: boolean }, 105 | ): void | Thenable { 106 | throw new Error('Method not implemented.'); 107 | } 108 | copy?( 109 | source: Uri, 110 | destination: Uri, 111 | options: { readonly overwrite: boolean }, 112 | ): void | Thenable { 113 | throw new Error('Method not implemented.'); 114 | } 115 | } 116 | 117 | export class ApolloStudioOperationsProvider 118 | implements TextDocumentContentProvider 119 | { 120 | static scheme = 'apollo-studio-operations'; 121 | static Uri(operationName: string, document: string): Uri { 122 | return Uri.parse( 123 | `${ApolloStudioOperationsProvider.scheme}:${operationName}.graphql?${document}`, 124 | ); 125 | } 126 | 127 | provideTextDocumentContent(uri: Uri): string { 128 | const operationName = uri.path.split('.graphql')[0]; 129 | const doc = parse(uri.query); 130 | const ast = getOperationAST(doc, operationName); 131 | if (ast) return print(ast); 132 | return uri.query; 133 | } 134 | } 135 | 136 | export class SubgraphUrlSchemaProvider implements TextDocumentContentProvider { 137 | static scheme = 'apollo-workbench-subgraph-url'; 138 | static Uri(subgraphName: string, url: string): Uri { 139 | return Uri.parse( 140 | `${SubgraphUrlSchemaProvider.scheme}:${subgraphName}.graphql?${url}`, 141 | ); 142 | } 143 | 144 | async provideTextDocumentContent(uri: Uri): Promise { 145 | return await Rover.instance.subgraphIntrospect(uri.query); 146 | } 147 | } 148 | 149 | export class ApolloRemoteSchemaProvider implements TextDocumentContentProvider { 150 | static scheme = 'apollo-workbench-remote-schema'; 151 | static Uri(wbFilePath: string, subgraphName: string): Uri { 152 | return Uri.parse( 153 | `${ApolloRemoteSchemaProvider.scheme}:${subgraphName}.graphql?${wbFilePath}#${subgraphName}`, 154 | ); 155 | } 156 | async provideTextDocumentContent(uri: Uri): Promise { 157 | const wbFilePath = uri.query; 158 | const subgraphName = uri.fragment; 159 | const tempUri = await FileProvider.instance.writeTempSchemaFile( 160 | wbFilePath, 161 | subgraphName, 162 | ); 163 | if (!tempUri) return 'Unable to get remote schema'; 164 | 165 | const content = await workspace.fs.readFile(tempUri); 166 | return new TextDecoder().decode(content); 167 | } 168 | } 169 | 170 | export enum Preloaded { 171 | SpotifyShowcase = 1, 172 | RetailSupergraph = 2, 173 | } 174 | export class PreloadedSchemaProvider implements TextDocumentContentProvider { 175 | static scheme = 'apollo-workbench-preloaded-schema'; 176 | static Uri(wbFilePath: string, subgraphName: string): Uri { 177 | return Uri.parse( 178 | `${ApolloRemoteSchemaProvider.scheme}:${subgraphName}.graphql?${wbFilePath}#${subgraphName}`, 179 | ); 180 | } 181 | static async Open(wbFilePath: string, subgraphName: string) { 182 | const uri = Uri.parse( 183 | `${PreloadedSchemaProvider.scheme}:${subgraphName}.graphql?${wbFilePath}#${subgraphName}`, 184 | ); 185 | await window.showTextDocument(uri, { preview: true }); 186 | } 187 | async provideTextDocumentContent(uri: Uri): Promise { 188 | const wbFilePath = uri.query; 189 | const subgraphName = uri.fragment; 190 | const schemaFilePath = normalizePath( 191 | resolve( 192 | `${wbFilePath.split('.yaml')[0]}-schemas`, 193 | `${subgraphName}.graphql`, 194 | ), 195 | ); 196 | 197 | const content = await workspace.fs.readFile(Uri.parse(schemaFilePath)); 198 | return new TextDecoder().decode(content); 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/workbench/federationCodeActionProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CodeAction, 3 | CodeActionKind, 4 | CodeActionProvider, 5 | CodeActionContext, 6 | Range, 7 | TextDocument, 8 | } from 'vscode'; 9 | 10 | export class FederationCodeActionProvider implements CodeActionProvider { 11 | public provideCodeActions( 12 | document: TextDocument, 13 | range: Range, 14 | context: CodeActionContext, 15 | ): CodeAction[] | undefined { 16 | const code = context.diagnostics[0]?.code as string; 17 | const selectors: CodeAction[] = []; 18 | if (code?.includes('addDirective')) { 19 | const codeSplit = code.split(':'); 20 | const directive = codeSplit[1]; 21 | const selector = new CodeAction( 22 | `Add ${directive}`, 23 | CodeActionKind.QuickFix, 24 | ); 25 | selector.command = { 26 | command: 'current-workbench-schemas.addFederationDirective', 27 | title: `Add ${directive} to schema`, 28 | arguments: [directive, document], 29 | }; 30 | 31 | selectors.push(selector); 32 | } 33 | 34 | if (selectors.length > 0) return selectors; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/workbench/federationCompletionProvider.ts: -------------------------------------------------------------------------------- 1 | // import { getAutocompleteSuggestions } from '../graphql/getAutocompleteSuggestions'; 2 | import { 3 | CancellationToken, 4 | CompletionItem, 5 | CompletionItemKind, 6 | MarkdownString, 7 | SnippetString, 8 | TextDocument, 9 | } from 'vscode'; 10 | import { Position } from 'vscode-languageserver'; 11 | // import { getServiceAvailableTypes } from '../graphql/parsers/schemaParser'; 12 | import { StateManager } from './stateManager'; 13 | import { FileProvider } from './file-system/fileProvider'; 14 | import gql from 'graphql-tag'; 15 | import { 16 | buildSchema, 17 | EnumValueNode, 18 | StringValueNode, 19 | TypeInfo, 20 | visit, 21 | visitWithTypeInfo, 22 | } from 'graphql'; 23 | 24 | export interface FieldWithType { 25 | field: string; 26 | type?: string; 27 | } 28 | 29 | export type Entity = { 30 | type: string; 31 | keyString: string; 32 | fields: FieldWithType[]; 33 | }; 34 | function getFieldType(field: any) { 35 | switch (field.kind) { 36 | case 'ObjectTypeDefinition': 37 | case 'FieldDefinition': 38 | case 'ListType': 39 | return getFieldType(field.type); 40 | case 'NamedType': 41 | return field.name.value; 42 | case 'NonNullType': 43 | switch (field.type.kind) { 44 | case 'ListType': 45 | return getFieldType(field.type); 46 | case 'NamedType': 47 | return field.type.name.value; 48 | } 49 | return ''; 50 | default: 51 | return ''; 52 | } 53 | } 54 | export function extractEntities(supergraphSdl: string) { 55 | const entities: { [subgraphName: string]: Entity[] } = {}; 56 | const typeInfo = new TypeInfo(buildSchema(supergraphSdl)); 57 | const doc = gql(supergraphSdl); 58 | const enumSubgraphValues = {}; 59 | visit( 60 | doc, 61 | visitWithTypeInfo(typeInfo, { 62 | ObjectTypeDefinition(node) { 63 | const currentNode = node; 64 | node.directives?.forEach((d) => { 65 | if (d.name.value == 'join__type') { 66 | const graphArg = d.arguments?.find((a) => a.name.value == 'graph'); 67 | const keysArg = d.arguments?.find((a) => a.name.value == 'key'); 68 | if (graphArg && keysArg) { 69 | const subgraphName = 70 | enumSubgraphValues[(graphArg.value as EnumValueNode).value]; 71 | if (!entities[subgraphName]) entities[subgraphName] = []; 72 | const keyString = (keysArg.value as StringValueNode).value; 73 | const keys = keyString.split(' '); 74 | 75 | if (keys.length == 1) { 76 | const keyField = currentNode.fields?.find( 77 | (f) => f.name.value == keys[0], 78 | ); 79 | if (keyField) { 80 | console.log(keyField); 81 | entities[subgraphName].push({ 82 | type: currentNode.name.value, 83 | keyString, 84 | fields: [{ field: keys[0], type: getFieldType(keyField) }], 85 | }); 86 | } 87 | } else { 88 | //multiple keys entity 89 | const fields: FieldWithType[] = []; 90 | keys.forEach((key) => { 91 | const keyField = currentNode.fields?.find( 92 | (f) => f.name.value == key, 93 | ); 94 | if (keyField) { 95 | console.log(keyField); 96 | fields.push({ field: key, type: getFieldType(keyField) }); 97 | } 98 | }); 99 | 100 | entities[subgraphName].push({ 101 | type: currentNode.name.value, 102 | keyString, 103 | fields, 104 | }); 105 | } 106 | } 107 | } 108 | }); 109 | }, 110 | EnumTypeDefinition(node) { 111 | if (node.name.value == 'join__Graph') { 112 | node.values?.forEach((v) => { 113 | if (v?.directives && v?.directives[0]) { 114 | const subgraph = ( 115 | v?.directives[0].arguments?.find((a) => a.name.value == 'name') 116 | ?.value as StringValueNode 117 | )?.value; 118 | if (subgraph) enumSubgraphValues[v.name.value] = subgraph; 119 | } 120 | }); 121 | } 122 | }, 123 | }), 124 | ); 125 | 126 | return entities; 127 | } 128 | 129 | //Extremely basic/naive implementation to find extendable entities 130 | // This should be in language server 131 | export const federationCompletionProvider = { 132 | async provideCompletionItems( 133 | document: TextDocument, 134 | position: Position, 135 | token: CancellationToken, 136 | ) { 137 | //Only provide completion items for schemas open in workbench 138 | const uri = document.uri; 139 | const completionItems = new Array(); 140 | const line = document.lineAt(position.line); 141 | const lineText = line.text; 142 | const addedEntities: string[] = []; 143 | 144 | const { path: wbFilePath, subgraphName: originatingSubgraph } = 145 | FileProvider.instance.workbenchFilePathBySchemaFilePath(uri.fsPath); 146 | if (!lineText) { 147 | const extendableTypes = 148 | StateManager.instance.workspaceState_availableEntities; 149 | 150 | if (extendableTypes[wbFilePath]) { 151 | const originatingSubgraphEntities = 152 | extendableTypes[wbFilePath][originatingSubgraph]; 153 | Object.keys(extendableTypes[wbFilePath]).forEach((subgraphName) => { 154 | const subgraphEntities = extendableTypes[wbFilePath][subgraphName]; 155 | if (originatingSubgraph != subgraphName) { 156 | const subgraphEntities = extendableTypes[wbFilePath][subgraphName]; 157 | if (subgraphEntities) { 158 | subgraphEntities.forEach((entity) => { 159 | //Need to check if entity is already in originatingSubgraph 160 | if ( 161 | originatingSubgraphEntities?.find( 162 | (e) => 163 | e.type == entity.type && e.keyString == entity.keyString, 164 | ) 165 | ) 166 | return; 167 | const uniqueKey = `${entity.type}:${entity.keyString}`; 168 | if (!addedEntities.includes(uniqueKey)) { 169 | if ( 170 | extendableTypes[wbFilePath][originatingSubgraph]?.findIndex( 171 | (e) => 172 | e.type == entity.type && 173 | e.keyString == entity.keyString, 174 | ) ?? 175 | -1 < 0 176 | ) { 177 | addedEntities.push(uniqueKey); 178 | 179 | completionItems.push( 180 | new FederationEntityExtensionItem( 181 | entity.keyString, 182 | entity.type, 183 | entity.fields, 184 | ), 185 | ); 186 | } 187 | } 188 | }); 189 | } 190 | } 191 | }); 192 | } 193 | 194 | completionItems.push(new EntityObjectTypeCompletionItem()); 195 | } 196 | 197 | if (completionItems.length > 0) return completionItems; 198 | }, 199 | }; 200 | 201 | export class EntityObjectTypeCompletionItem extends CompletionItem { 202 | constructor() { 203 | super('Entity Object type', CompletionItemKind.Snippet); 204 | this.sortText = 'b'; 205 | 206 | const insertSnippet = new SnippetString(`type `); 207 | insertSnippet.appendTabstop(1); 208 | insertSnippet.appendText(` @key(fields:"`); 209 | insertSnippet.appendTabstop(2); 210 | insertSnippet.appendText(`") {\n\t`); 211 | insertSnippet.appendTabstop(3); 212 | insertSnippet.appendText(`\n}`); 213 | 214 | this.detail = 'Define a new Entity Object Type'; 215 | this.insertText = insertSnippet; 216 | this.documentation = new MarkdownString( 217 | `To learn more about entities, click [here](https://www.apollographql.com/docs/federation/entities/).`, 218 | ); 219 | } 220 | } 221 | 222 | export class FederationEntityExtensionItem extends CompletionItem { 223 | constructor(key: string, typeToUse: string, keyFields: FieldWithType[]) { 224 | super(`${typeToUse} by "${key}"`, CompletionItemKind.Reference); 225 | this.sortText = `${typeToUse}-${key}`; 226 | 227 | const insertSnippet = new SnippetString(`type `); 228 | let typeExtensionCodeBlock = `type ${typeToUse} @key(fields:"${key}") {\n`; 229 | 230 | insertSnippet.appendVariable('typeToUse', typeToUse); 231 | insertSnippet.appendText(' @key(fields:"'); 232 | insertSnippet.appendVariable('key', key); 233 | insertSnippet.appendText('") {\n'); 234 | 235 | for (let i = 0; i < keyFields.length; i++) { 236 | const keyField = keyFields[i]; 237 | const fieldLine = `\t${keyField.field}: ${keyField.type}\n`; 238 | typeExtensionCodeBlock += fieldLine; 239 | insertSnippet.appendText(fieldLine); 240 | } 241 | 242 | insertSnippet.appendText(`\t`); 243 | insertSnippet.appendTabstop(1); 244 | typeExtensionCodeBlock += '}'; 245 | insertSnippet.appendText(`\n}`); 246 | 247 | const mkdDocs = new MarkdownString(); 248 | mkdDocs.appendCodeblock(typeExtensionCodeBlock, 'graphql'); 249 | mkdDocs.appendMarkdown( 250 | `To learn more about extending entities, click [here](https://www.apollographql.com/docs/federation/entities/#extending).`, 251 | ); 252 | 253 | this.documentation = mkdDocs; 254 | this.detail = `Use entity ${typeToUse}`; 255 | this.insertText = insertSnippet; 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/workbench/federationReferenceProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Kind, 3 | NameNode, 4 | ObjectTypeDefinitionNode, 5 | TypeInfo, 6 | TypeNode, 7 | buildSchema, 8 | parse, 9 | visit, 10 | visitWithTypeInfo, 11 | } from 'graphql'; 12 | import gql from 'graphql-tag'; 13 | import { 14 | CancellationToken, 15 | Location, 16 | Position, 17 | ProviderResult, 18 | Range, 19 | ReferenceContext, 20 | ReferenceProvider, 21 | TextDocument, 22 | Uri, 23 | window, 24 | workspace, 25 | } from 'vscode'; 26 | import { getFileName } from '../utils/path'; 27 | import { ApolloConfig } from './file-system/ApolloConfig'; 28 | import { FileProvider, tempSchemaFilePath } from './file-system/fileProvider'; 29 | import { Rover } from './rover'; 30 | import { start } from 'repl'; 31 | import { log } from '../utils/logger'; 32 | 33 | export class FederationReferenceProvider implements ReferenceProvider { 34 | provideReferences( 35 | document: TextDocument, 36 | position: Position, 37 | context: ReferenceContext, 38 | token: CancellationToken, 39 | ): ProviderResult { 40 | const uri = document.uri; 41 | 42 | //We cannot assume the filepaths name is actually the subgraph name, we need to reference the config file 43 | //Go through config files and find out which one has a matching workbench_design, file or tempSchemaPath path 44 | const subgraphName = getFileName(uri.fsPath); 45 | 46 | if (subgraphName) { 47 | let wbFilePath = ''; 48 | const wbFiles = FileProvider.instance.getWorkbenchFiles(); 49 | wbFiles.forEach((wbFile, filePath) => { 50 | if (Object.keys(wbFile.subgraphs).includes(subgraphName)) { 51 | wbFilePath = filePath; 52 | } 53 | }); 54 | 55 | if (wbFilePath != '') { 56 | //Need to find references in other schema files 57 | const text = document.getWordRangeAtPosition(position); 58 | if (!text ?? !text?.isSingleLine) return; 59 | 60 | const wbFile = wbFiles.get(wbFilePath); 61 | if (!wbFile) return undefined; 62 | 63 | const lineText = document.lineAt(text.start.line).text; 64 | if (lineText.startsWith('type')) { 65 | const type = lineText.substring( 66 | text.start.character, 67 | text.end.character, 68 | ); 69 | 70 | return this.findTypeReferences(wbFile, wbFilePath, type); 71 | } 72 | } 73 | } 74 | 75 | return undefined; 76 | } 77 | 78 | private async findTypeReferences( 79 | wbFile: ApolloConfig, 80 | wbFilePath: string, 81 | type: string, 82 | ) { 83 | const locations: Location[] = []; 84 | 85 | for (const name in wbFile.subgraphs) { 86 | let sdl = ''; 87 | let uri: Uri; 88 | const subgraph = wbFile?.subgraphs[name]; 89 | if (subgraph.schema.workbench_design) { 90 | uri = Uri.parse(subgraph.schema.workbench_design); 91 | const file = await workspace.fs.readFile(uri); 92 | sdl = file.toString(); 93 | } else if (subgraph.schema.graphref) { 94 | sdl = await Rover.instance.subgraphGraphOSFetch( 95 | subgraph.schema.graphref, 96 | subgraph.schema.subgraph ?? name, 97 | ); 98 | if (!sdl) { 99 | log('Not authenticated. Must run rover config auth'); 100 | window 101 | .showErrorMessage( 102 | 'Fetching schemas from GraphOS requires you to authenticate the Rover CLI with your User API key.', 103 | { modal: true }, 104 | ) 105 | .then(() => { 106 | const term = window.createTerminal('rover config auth'); 107 | term.sendText('rover config auth'); 108 | term.show(); 109 | }); 110 | } 111 | 112 | uri = tempSchemaFilePath(wbFilePath, name); 113 | } else if (subgraph.schema.file) { 114 | uri = Uri.parse(subgraph.schema.file); 115 | const file = await workspace.fs.readFile(uri); 116 | sdl = file.toString(); 117 | } else { 118 | sdl = await Rover.instance.subgraphFetch(subgraph); 119 | } 120 | 121 | const getRange = ( 122 | node: ObjectTypeDefinitionNode | NameNode | TypeNode, 123 | ) => { 124 | if (!node.loc) return new Range(0, 0, 0, 0); 125 | const startLine = 126 | node.loc.startToken.line > 0 ? node.loc.startToken.line - 1 : 0; 127 | const startCharacter = 128 | node.loc.startToken.column > 0 ? node.loc.startToken.column - 1 : 0; 129 | const endLine = 130 | node.loc.endToken.line > 0 ? node.loc.endToken.line - 1 : 0; 131 | const endCharacter = 132 | startCharacter + (node.loc.endToken.end - node.loc.endToken.start); 133 | return new Range(startLine, startCharacter, endLine, endCharacter); 134 | }; 135 | 136 | visit(parse(sdl), { 137 | ObjectTypeDefinition(node) { 138 | if (node.name.value == type && node.loc) { 139 | locations.push({ 140 | uri, 141 | range: getRange(node.name), 142 | }); 143 | } else { 144 | node.fields?.forEach((field) => { 145 | let found = false; 146 | let fieldType = field.type; 147 | let fieldNamedType; 148 | while (!found) { 149 | if (fieldType.kind == Kind.NAMED_TYPE) { 150 | fieldNamedType = fieldType.name.value; 151 | found = true; 152 | } else { 153 | fieldType = fieldType.type; 154 | } 155 | } 156 | if (fieldNamedType == type) { 157 | if (fieldType.loc) { 158 | locations.push({ 159 | uri, 160 | range: getRange(fieldType), 161 | }); 162 | } 163 | } 164 | }); 165 | } 166 | }, 167 | }); 168 | } 169 | 170 | return locations; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/workbench/file-system/ApolloConfig.ts: -------------------------------------------------------------------------------- 1 | export class ApolloConfig { 2 | federation_version?: string; 3 | subgraphs: { [name: string]: Subgraph } = {}; 4 | operations: { [name: string]: Operation } = {}; 5 | 6 | constructor() { 7 | this.federation_version = '=2.7.2'; 8 | } 9 | 10 | public static copy(config: ApolloConfig) { 11 | return JSON.parse(JSON.stringify(config)); 12 | } 13 | } 14 | 15 | export type Subgraph = { 16 | routing_url?: string; 17 | schema: Schema; 18 | }; 19 | 20 | type Schema = { 21 | file?: string; 22 | graphref?: string; 23 | subgraph?: string; 24 | subgraph_url?: string; 25 | 26 | workbench_design?: string; 27 | mocks?: MockSubgraph; 28 | }; 29 | 30 | type MockSubgraph = { 31 | enabled: boolean; 32 | customMocks?: string; 33 | }; 34 | 35 | export type Operation = { 36 | document: string; 37 | ui_design?: string; 38 | }; 39 | -------------------------------------------------------------------------------- /src/workbench/file-system/CompositionResults.ts: -------------------------------------------------------------------------------- 1 | export class CompositionResults { 2 | data: { 3 | success: boolean; 4 | core_schema?: string; 5 | hints?: RoverCompositionHint[]; 6 | } = { success: false }; 7 | error?: RoverError; 8 | } 9 | 10 | type RoverCompositionHint = { 11 | message: string; 12 | }; 13 | 14 | export type RoverError = { 15 | code: string; 16 | message: string; 17 | details: { build_errors: RoverCompositionError[] }; 18 | }; 19 | 20 | export type RoverCompositionError = { 21 | code: string; 22 | message: string; 23 | nodes: ErrorNode[]; 24 | }; 25 | 26 | type ErrorNode = { 27 | subgraph: string; 28 | source: string; 29 | start: Position; 30 | end: Position; 31 | }; 32 | 33 | type Position = { 34 | start: number; 35 | end: number; 36 | line: number; 37 | column: number; 38 | }; 39 | -------------------------------------------------------------------------------- /src/workbench/stateManager.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLSchema } from 'graphql'; 2 | import { ExtensionContext, workspace } from 'vscode'; 3 | import { Entity } from './federationCompletionProvider'; 4 | import { ApolloStudioGraphsTreeDataProvider } from './tree-data-providers/apolloStudioGraphsTreeDataProvider'; 5 | import { ApolloStudioGraphOpsTreeDataProvider } from './tree-data-providers/apolloStudioGraphOpsTreeDataProvider'; 6 | import { LocalSupergraphTreeDataProvider } from './tree-data-providers/superGraphTreeDataProvider'; 7 | 8 | export class StateManager { 9 | context: ExtensionContext; 10 | 11 | private static _instance: StateManager; 12 | static get instance(): StateManager { 13 | if (!this._instance) 14 | throw new Error('You must call init() before using the state manager'); 15 | 16 | return this._instance; 17 | } 18 | constructor(context: ExtensionContext) { 19 | this.context = context; 20 | } 21 | 22 | static init(context: ExtensionContext) { 23 | this._instance = new StateManager(context); 24 | } 25 | 26 | apolloStudioGraphsProvider: ApolloStudioGraphsTreeDataProvider = 27 | new ApolloStudioGraphsTreeDataProvider(workspace.rootPath ?? '.'); 28 | apolloStudioGraphOpsProvider: ApolloStudioGraphOpsTreeDataProvider = 29 | new ApolloStudioGraphOpsTreeDataProvider(); 30 | localSupergraphTreeDataProvider: LocalSupergraphTreeDataProvider = 31 | new LocalSupergraphTreeDataProvider(); 32 | 33 | get extensionGlobalStoragePath(): string { 34 | try { 35 | //Running version 1.49+ 36 | return this.context.globalStorageUri.fsPath; 37 | } catch (err) { 38 | //Running version 1.48 or lower 39 | return this.context.globalStoragePath; 40 | } 41 | } 42 | 43 | static get workspaceRoot(): string | undefined { 44 | return workspace.workspaceFolders 45 | ? workspace.workspaceFolders[0]?.uri?.fsPath 46 | : undefined; 47 | } 48 | static get settings_startingServerPort(): number { 49 | return ( 50 | workspace 51 | ?.getConfiguration('apollo-workbench') 52 | ?.get('startingServerPort') ?? (4001 as number) 53 | ); 54 | } 55 | static get settings_openSandbox(): boolean { 56 | return ( 57 | workspace 58 | ?.getConfiguration('apollo-workbench') 59 | ?.get('openSandboxOnStartMocks') ?? true 60 | ); 61 | } 62 | static get settings_routerVersion(): string | undefined { 63 | return workspace 64 | ?.getConfiguration('apollo-workbench') 65 | ?.get('routerVersion'); 66 | } 67 | static get settings_routerPort(): number { 68 | return ( 69 | workspace?.getConfiguration('apollo-workbench')?.get('routerPort') ?? 70 | (4000 as number) 71 | ); 72 | } 73 | static get settings_routerConfigFile(): string { 74 | return ( 75 | workspace 76 | ?.getConfiguration('apollo-workbench') 77 | ?.get('routerConfigFile') ?? '' 78 | ); 79 | } 80 | static get settings_enableSubgraphUrlWatcher(): boolean { 81 | return ( 82 | workspace 83 | ?.getConfiguration('apollo-workbench') 84 | ?.get('enableSubgraphUrlWatcher') ?? true 85 | ); 86 | } 87 | static get settings_subgraphWatcherPingInterval(): number { 88 | return ( 89 | workspace 90 | ?.getConfiguration('apollo-workbench') 91 | ?.get('subgraphWatcherPingInterval') ?? 1000 92 | ); 93 | } 94 | static get settings_apiKey() { 95 | return ( 96 | (workspace 97 | .getConfiguration('apollo-workbench') 98 | .get('graphApiKey') as string) ?? 99 | process.env.APOLLO_KEY ?? 100 | '' 101 | ); 102 | } 103 | static get settings_graphVariant() { 104 | return ( 105 | (workspace 106 | .getConfiguration('apollo-workbench') 107 | .get('graphVariant') as string) ?? 108 | process.env.APOLLO_GRAPH_VARIANT ?? 109 | '' 110 | ); 111 | } 112 | static get settings_daysOfOperationsToFetch(): number { 113 | return workspace 114 | .getConfiguration('apollo-workbench') 115 | .get('daysOfOperationsToFetch') as number; 116 | } 117 | static get settings_displayExampleGraphs() { 118 | return workspace 119 | .getConfiguration('apollo-workbench') 120 | .get('displayExampleGraphs') as boolean; 121 | } 122 | static get settings_tlsRejectUnauthorized() { 123 | return workspace 124 | .getConfiguration('apollo-workbench') 125 | .get('tlsRejectUnauthorized') as boolean; 126 | } 127 | static get settings_apolloOrg() { 128 | return workspace 129 | .getConfiguration('apollo-workbench') 130 | .get('apolloOrg') as string; 131 | } 132 | static get settings_localDesigns_expandSubgraphsByDefault() { 133 | return workspace 134 | .getConfiguration('apollo-workbench') 135 | .get('local-designs.expandSubgraphsByDefault') as boolean; 136 | } 137 | static get settings_localDesigns_expandOperationsByDefault() { 138 | return workspace 139 | .getConfiguration('apollo-workbench') 140 | .get('local-designs.expandOperationsByDefault') as boolean; 141 | } 142 | static get settings_graphRef(): string { 143 | return workspace 144 | .getConfiguration('apollo-workbench') 145 | .get('graphRef') as string; 146 | } 147 | static set settings_graphRef(profile: string) { 148 | workspace 149 | .getConfiguration('apollo-workbench') 150 | .update('graphRef', profile); 151 | } 152 | static get settings_roverConfigProfile(): string { 153 | return workspace 154 | .getConfiguration('apollo-workbench') 155 | .get('roverConfigProfile') as string; 156 | } 157 | static get settings_apolloApiUrl(): string { 158 | return workspace 159 | .getConfiguration('apollo-workbench') 160 | .get('apolloApiUrl') as string; 161 | } 162 | static set settings_roverConfigProfile(profile: string) { 163 | workspace 164 | .getConfiguration('apollo-workbench') 165 | .update('roverConfigProfile', profile); 166 | } 167 | get globalState_userApiKey() { 168 | return this.context?.globalState.get('APOLLO_KEY') as string; 169 | } 170 | set globalState_userApiKey(apiKey: string) { 171 | this.context?.globalState.update('APOLLO_KEY', apiKey); 172 | 173 | if (!apiKey) { 174 | this.globalState_selectedApolloAccount = ''; 175 | this.setSelectedGraph(); 176 | } 177 | 178 | this.apolloStudioGraphsProvider.refresh(); 179 | this.apolloStudioGraphOpsProvider.refresh(); 180 | } 181 | get globalState_selectedApolloAccount() { 182 | if (StateManager.settings_apolloOrg) return StateManager.settings_apolloOrg; 183 | return this.context?.globalState.get('APOLLO_SELCTED_ACCOUNT') as string; 184 | } 185 | set globalState_selectedApolloAccount(accountId: string) { 186 | this.context?.globalState.update('APOLLO_SELCTED_ACCOUNT', accountId); 187 | } 188 | setSelectedGraph(graphId?: string, variant?: string) { 189 | this.context?.globalState.update('APOLLO_SELCTED_GRAPH_ID', graphId); 190 | this.context?.globalState.update('APOLLO_SELCTED_GRAPH_VARIANT', variant); 191 | 192 | this.apolloStudioGraphOpsProvider.refresh(); 193 | } 194 | get globalState_selectedGraphVariant() { 195 | return this.context?.globalState.get( 196 | 'APOLLO_SELCTED_GRAPH_VARIANT', 197 | ) as string; 198 | } 199 | get globalState_selectedGraph() { 200 | return this.context?.globalState.get('APOLLO_SELCTED_GRAPH_ID') as string; 201 | } 202 | get workspaceState_availableEntities() { 203 | return this.context?.workspaceState.get('availableEntities') as { 204 | [wbFilePath: string]: { 205 | [serviceName: string]: Entity[]; 206 | }; 207 | }; 208 | } 209 | workspaceState_clearEntities() { 210 | this.context?.workspaceState.update('availableEntities', {}); 211 | } 212 | workspaceState_setEntities(input: { 213 | designPath: string; 214 | entities: { 215 | [serviceName: string]: Entity[]; 216 | }; 217 | }) { 218 | const savedEntities = this.workspaceState_availableEntities ?? {}; 219 | savedEntities[input.designPath] = input.entities; 220 | this.context?.workspaceState.update('availableEntities', savedEntities); 221 | } 222 | clearWorkspaceSchema() { 223 | this.context?.workspaceState.update('schema', undefined); 224 | } 225 | get workspaceState_schema() { 226 | return this.context?.workspaceState.get('schema') as GraphQLSchema; 227 | } 228 | set workspaceState_schema(schema: GraphQLSchema) { 229 | this.context?.workspaceState.update('schema', schema); 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/workbench/tree-data-providers/apolloStudioGraphOpsTreeDataProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { getGraphOps } from '../../graphql/graphClient'; 3 | import { StateManager } from '../stateManager'; 4 | import { StudioOperationTreeItem } from './tree-items/graphos-operations/studioOperationTreeItem'; 5 | 6 | export class ApolloStudioGraphOpsTreeDataProvider 7 | implements vscode.TreeDataProvider 8 | { 9 | private _onDidChangeTreeData: vscode.EventEmitter< 10 | vscode.TreeItem | undefined 11 | > = new vscode.EventEmitter(); 12 | readonly onDidChangeTreeData: vscode.Event = 13 | this._onDidChangeTreeData.event; 14 | 15 | refresh(): void { 16 | this._onDidChangeTreeData.fire(undefined); 17 | } 18 | 19 | getTreeItem(element: vscode.TreeItem): vscode.TreeItem { 20 | return element; 21 | } 22 | 23 | async getChildren(element?: any): Promise { 24 | if (element) return element.versions ?? element.operations; 25 | 26 | let noOperationsFoundMessage = ''; 27 | const items: { 28 | [operationName: string]: { 29 | operationId: string; 30 | operationSignature: string; 31 | }; 32 | } = {}; 33 | 34 | const selectedGraphId = StateManager.instance.globalState_selectedGraph; 35 | const graphVariant = StateManager.instance.globalState_selectedGraphVariant; 36 | if (selectedGraphId) { 37 | //Create objects for next for loop 38 | // Return A specific account with all graphs 39 | const graphOps = await getGraphOps(selectedGraphId, graphVariant); 40 | 41 | noOperationsFoundMessage = graphVariant 42 | ? `No operations found for ${graphOps.service?.title}@${graphVariant}` 43 | : `No operations found for ${graphOps.service?.title}`; 44 | if (graphOps?.service?.statsWindow?.queryStats) { 45 | graphOps?.service?.statsWindow?.queryStats.map((queryStat) => { 46 | if ( 47 | queryStat.groupBy.queryName && 48 | queryStat.groupBy.queryId && 49 | queryStat.groupBy.querySignature 50 | ) { 51 | items[queryStat.groupBy.queryName] = { 52 | operationId: queryStat.groupBy.queryId, 53 | operationSignature: queryStat.groupBy.querySignature, 54 | }; 55 | } 56 | }); 57 | } 58 | } else 59 | return [ 60 | new vscode.TreeItem( 61 | 'Select a graph above to load operations from Apollo Studio', 62 | vscode.TreeItemCollapsibleState.None, 63 | ), 64 | ]; 65 | 66 | const itemsToReturn: vscode.TreeItem[] = new Array(); 67 | 68 | for (const operationName in items) { 69 | const op = items[operationName]; 70 | itemsToReturn.push( 71 | new StudioOperationTreeItem( 72 | op.operationId, 73 | operationName, 74 | op.operationSignature, 75 | ), 76 | ); 77 | } 78 | 79 | if (itemsToReturn.length > 0) return itemsToReturn; 80 | 81 | return [ 82 | new vscode.TreeItem( 83 | `${noOperationsFoundMessage} in last ${StateManager.settings_daysOfOperationsToFetch} days`, 84 | vscode.TreeItemCollapsibleState.None, 85 | ), 86 | ]; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/workbench/tree-data-providers/apolloStudioGraphsTreeDataProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getAccountGraphs, 3 | getUserMemberships, 4 | } from '../../graphql/graphClient'; 5 | import { StateManager } from '../stateManager'; 6 | import { 7 | TreeItem, 8 | TreeDataProvider, 9 | EventEmitter, 10 | Event, 11 | window, 12 | commands, 13 | } from 'vscode'; 14 | import { SignupTreeItem } from './tree-items/graphos-supergraphs/signupTreeItem'; 15 | import { PreloadedWorkbenchTopLevel } from './tree-items/graphos-supergraphs/preloadedWorkbenchTopLevel'; 16 | import { StudioGraphVariantTreeItem } from './tree-items/graphos-supergraphs/studioGraphVariantTreeItem'; 17 | import { StudioGraphTreeItem } from './tree-items/graphos-supergraphs/studioGraphTreeItem'; 18 | import { StudioAccountTreeItem } from './tree-items/graphos-supergraphs/studioAccountTreeItem'; 19 | import NotLoggedInTreeItem from './tree-items/graphos-supergraphs/notLoggedInTreeItem'; 20 | 21 | export class ApolloStudioGraphsTreeDataProvider 22 | implements TreeDataProvider 23 | { 24 | constructor(private workspaceRoot: string) {} 25 | 26 | private _onDidChangeTreeData: EventEmitter = 27 | new EventEmitter(); 28 | readonly onDidChangeTreeData: Event = 29 | this._onDidChangeTreeData.event; 30 | 31 | refresh(): void { 32 | this._onDidChangeTreeData.fire(undefined); 33 | } 34 | 35 | getTreeItem(element: TreeItem): TreeItem { 36 | return element; 37 | } 38 | 39 | async getChildren(element?: StudioAccountTreeItem): Promise { 40 | if (element) return element.children; 41 | const items: TreeItem[] = new Array(); 42 | 43 | const apiKey = StateManager.instance.globalState_userApiKey; 44 | if (apiKey) { 45 | let accountId = StateManager.instance.globalState_selectedApolloAccount; 46 | if (!accountId) { 47 | const myAccountIds = await getUserMemberships(); 48 | const memberships = (myAccountIds?.me as any)?.memberships; 49 | if (memberships?.length > 1) { 50 | const accountIds: string[] = new Array(); 51 | memberships.map((membership) => 52 | accountIds.push(membership.account.id), 53 | ); 54 | 55 | accountId = 56 | (await window.showQuickPick(accountIds, { 57 | placeHolder: 'Select an account to load graphs from', 58 | })) ?? ''; 59 | } else { 60 | accountId = memberships[0]?.account?.id ?? ''; 61 | } 62 | } 63 | 64 | if (accountId) { 65 | StateManager.instance.globalState_selectedApolloAccount = accountId; 66 | 67 | //Create objects for next for loop 68 | // Return A specific account with all graphs 69 | 70 | //Change to single root query 71 | // 2. commands/studio-graphs/switchOrg should actually just refresh this provider to re-use this query 72 | // query ExampleQuery($name: String!) { 73 | // frontendUrlRoot 74 | // me { 75 | // id 76 | // ... on InternalIdentity { 77 | // accounts { 78 | // id 79 | // name 80 | // graphs { 81 | // id 82 | // title 83 | // variant(name: $name) { 84 | // name 85 | // } 86 | // } 87 | // } 88 | // } 89 | // } 90 | // } 91 | 92 | const services = await getAccountGraphs(accountId); 93 | const accountTreeItem = new StudioAccountTreeItem( 94 | accountId, 95 | services?.organization?.name, 96 | ); 97 | 98 | if (services?.organization?.graphs) { 99 | const accountServiceTreeItems = new Array(); 100 | 101 | for (let j = 0; j < services?.organization?.graphs.length ?? 0; j++) { 102 | //Cast graph 103 | const graph = services?.organization?.graphs[j]; 104 | 105 | //Create objects for next for loop 106 | // Return A specific Graph with all variants 107 | const graphTreeItem = new StudioGraphTreeItem( 108 | graph.id, 109 | graph.title, 110 | ); 111 | const graphVariantTreeItems = 112 | new Array(); 113 | 114 | //Loop through graph variants and add to return objects 115 | for (let k = 0; k < graph.variants.length; k++) { 116 | //Cast graph variant 117 | const graphVariant = graph.variants[k]; 118 | graphTreeItem.variants.push(graphVariant.name); 119 | 120 | const accountgraphVariantTreeItem = 121 | new StudioGraphVariantTreeItem(graph.id, graphVariant.name); 122 | graphVariantTreeItems.push(accountgraphVariantTreeItem); 123 | } 124 | if (graphVariantTreeItems.length == 0) 125 | graphVariantTreeItems.push( 126 | new StudioGraphVariantTreeItem(graph.id, 'current'), 127 | ); 128 | 129 | //Set the implementing service tree items on the return objects 130 | graphTreeItem.children = graphVariantTreeItems; 131 | accountServiceTreeItems.push(graphTreeItem); 132 | } 133 | 134 | accountTreeItem.children = accountServiceTreeItems; 135 | } 136 | items.push(accountTreeItem); 137 | } 138 | 139 | if (StateManager.settings_displayExampleGraphs) 140 | items.push(new PreloadedWorkbenchTopLevel()); 141 | } else { 142 | items.push(new NotLoggedInTreeItem()); 143 | items.push(new SignupTreeItem()); 144 | 145 | if (StateManager.settings_displayExampleGraphs) 146 | items.push(new PreloadedWorkbenchTopLevel()); 147 | 148 | window 149 | .showInformationMessage('No user api key was found.', 'Login') 150 | .then((response) => { 151 | if (response === 'Login') commands.executeCommand('extension.login'); 152 | }); 153 | } 154 | 155 | return items; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/workbench/tree-data-providers/superGraphTreeDataProvider.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { FileProvider } from '../file-system/fileProvider'; 3 | import { 4 | TreeItem, 5 | TreeDataProvider, 6 | EventEmitter, 7 | Event, 8 | window, 9 | } from 'vscode'; 10 | import { newDesign } from '../../commands/local-supergraph-designs'; 11 | import { SupergraphTreeItem } from './tree-items/local-supergraph-designs/supergraphTreeItem'; 12 | import { SubgraphSummaryTreeItem } from './tree-items/local-supergraph-designs/subgraphSummaryTreeItem'; 13 | import { OperationSummaryTreeItem } from './tree-items/local-supergraph-designs/operationSummaryTreeItem'; 14 | import { AddDesignOperationTreeItem } from './tree-items/local-supergraph-designs/addDesignOperationTreeItem'; 15 | import { FederationVersionItem } from './tree-items/local-supergraph-designs/federationVersionItem'; 16 | 17 | export const media = (file: string) => 18 | path.join(__dirname, '..', 'media', file); 19 | 20 | export class LocalSupergraphTreeDataProvider 21 | implements TreeDataProvider 22 | { 23 | private _onDidChangeTreeData: EventEmitter = 24 | new EventEmitter(); 25 | readonly onDidChangeTreeData: Event = 26 | this._onDidChangeTreeData.event; 27 | 28 | refresh(): void { 29 | this._onDidChangeTreeData.fire(undefined); 30 | } 31 | 32 | getTreeItem(element: SupergraphTreeItem): TreeItem { 33 | return element; 34 | } 35 | 36 | items = new Array(); 37 | 38 | async getChildren(element?: TreeItem): Promise { 39 | if (element == undefined) { 40 | this.items = new Array(); 41 | await FileProvider.instance.refreshLocalWorkbenchFiles(); 42 | const files = FileProvider.instance.getWorkbenchFiles(); 43 | files.forEach((wbFile, wbFilePath) => { 44 | this.items.push(new SupergraphTreeItem(wbFile, wbFilePath)); 45 | }); 46 | 47 | if (this.items.length == 0) { 48 | window 49 | .showInformationMessage( 50 | 'No workspace files found in current directory', 51 | 'Create New Workbench', 52 | ) 53 | .then((value) => { 54 | if (value === 'Create New Workbench') newDesign(); 55 | }); 56 | } 57 | 58 | return this.items; 59 | } else { 60 | switch (element.contextValue) { 61 | case 'supergraphTreeItem': { 62 | const supergraphItem = element as SupergraphTreeItem; 63 | const federationIdentifierItem = new FederationVersionItem( 64 | supergraphItem.wbFilePath, 65 | supergraphItem.wbFile.federation_version, 66 | ); 67 | const treeItems: any[] = [ 68 | federationIdentifierItem, 69 | supergraphItem.subgraphsChild, 70 | ]; 71 | if ( 72 | supergraphItem.wbFile.operations && 73 | Object.keys(supergraphItem.wbFile.operations).length > 0 74 | ) { 75 | treeItems.push(supergraphItem.operationsChild); 76 | } else { 77 | treeItems.push( 78 | new AddDesignOperationTreeItem(supergraphItem.wbFilePath), 79 | ); 80 | } 81 | 82 | return treeItems; 83 | } 84 | case 'subgraphSummaryTreeItem': 85 | return ( 86 | (element as SubgraphSummaryTreeItem).subgraphs 87 | ); 88 | case 'operationSummaryTreeItem': 89 | return (element as OperationSummaryTreeItem).operations; 90 | default: 91 | return []; 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/workbench/tree-data-providers/tree-items/graphos-operations/studioOperationTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, TreeItemCollapsibleState } from 'vscode'; 2 | 3 | export class StudioOperationTreeItem extends TreeItem { 4 | constructor( 5 | public readonly operationId: string, 6 | public readonly operationName: string, 7 | public readonly operationSignature: string, 8 | ) { 9 | super(operationName, TreeItemCollapsibleState.None); 10 | this.contextValue = 'studioOperationTreeItem'; 11 | this.description = `id:${operationId.substring(0, 6)}`; 12 | this.command = { 13 | title: 'View Operation', 14 | command: 'studio-graphs.viewStudioOperation', 15 | arguments: [this], 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/workbench/tree-data-providers/tree-items/graphos-supergraphs/notLoggedInTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, TreeItemCollapsibleState } from 'vscode'; 2 | 3 | export default class NotLoggedInTreeItem extends TreeItem { 4 | constructor() { 5 | super('Login with GraphOS', TreeItemCollapsibleState.None); 6 | this.command = { 7 | title: 'Login to Apollo', 8 | command: 'extension.login', 9 | }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/workbench/tree-data-providers/tree-items/graphos-supergraphs/preloadedSubgraph.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, TreeItemCollapsibleState, Uri, workspace } from 'vscode'; 2 | import { FileProvider } from '../../../file-system/fileProvider'; 3 | import { media } from '../../superGraphTreeDataProvider'; 4 | import { resolve } from 'path'; 5 | 6 | export class PreloadedSubgraph extends TreeItem { 7 | constructor( 8 | public readonly subgraphName: string, 9 | public readonly wbFileName: string, 10 | public readonly wbFilePath: string, 11 | ) { 12 | super(subgraphName, TreeItemCollapsibleState.None); 13 | 14 | this.tooltip = this.subgraphName; 15 | this.iconPath = { 16 | light: media('graphql-logo.png'), 17 | dark: media('graphql-logo.png'), 18 | }; 19 | this.contextValue = 'preloadedSubgraph'; 20 | this.command = { 21 | command: 'preloaded.viewPreloadedSchema', 22 | title: 'View Schema', 23 | arguments: [this], 24 | }; 25 | } 26 | 27 | async getSchema() { 28 | const schemaFilePath = resolve( 29 | this.wbFilePath, 30 | this.wbFileName, 31 | `${this.subgraphName}.graphql`, 32 | ); 33 | const schema = await workspace.fs.readFile(Uri.parse(schemaFilePath)); 34 | const result = schema.toString(); 35 | return result; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/workbench/tree-data-providers/tree-items/graphos-supergraphs/preloadedWorkbenchFile.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, TreeItemCollapsibleState } from 'vscode'; 2 | import { FileProvider } from '../../../file-system/fileProvider'; 3 | import { PreloadedSubgraph } from './preloadedSubgraph'; 4 | 5 | export class PreloadedWorkbenchFile extends TreeItem { 6 | children: PreloadedSubgraph[] = new Array(); 7 | 8 | constructor( 9 | public readonly fileName: string, 10 | public readonly filePath: string, 11 | ) { 12 | super(fileName, TreeItemCollapsibleState.Expanded); 13 | 14 | this.contextValue = 'preloadedWorkbenchFile'; 15 | this.getChildren(); 16 | } 17 | async getChildren(element?: PreloadedWorkbenchFile): Promise { 18 | const wbFile = await FileProvider.instance.getPreloadedWorkbenchFile( 19 | this.filePath, 20 | ); 21 | if (!wbFile || !wbFile.subgraphs) { 22 | return []; 23 | } 24 | Object.keys(wbFile.subgraphs).forEach((s) => 25 | this.children.push( 26 | new PreloadedSubgraph(s, this.fileName, this.filePath), 27 | ), 28 | ); 29 | 30 | return this.children; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/workbench/tree-data-providers/tree-items/graphos-supergraphs/preloadedWorkbenchTopLevel.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, TreeItemCollapsibleState } from 'vscode'; 2 | import { PreloadedWorkbenchFile } from './preloadedWorkbenchFile'; 3 | import { FileProvider } from '../../../file-system/fileProvider'; 4 | 5 | export class PreloadedWorkbenchTopLevel extends TreeItem { 6 | children: PreloadedWorkbenchFile[] = new Array(); 7 | 8 | constructor() { 9 | super('Example Designs', TreeItemCollapsibleState.Collapsed); 10 | this.getChildren(); 11 | } 12 | 13 | async getChildren(element?: PreloadedWorkbenchTopLevel): Promise { 14 | const preloadedFiles = 15 | await FileProvider.instance?.getPreloadedWorkbenchFiles(); 16 | preloadedFiles.map((preloadedFile) => { 17 | this.children.push( 18 | new PreloadedWorkbenchFile(preloadedFile.fileName, preloadedFile.path), 19 | ); 20 | }); 21 | return this.children; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/workbench/tree-data-providers/tree-items/graphos-supergraphs/signupTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, TreeItemCollapsibleState } from 'vscode'; 2 | 3 | export class SignupTreeItem extends TreeItem { 4 | constructor() { 5 | super('Sign-up for GraphOS (free)', TreeItemCollapsibleState.None); 6 | this.command = { 7 | title: 'Sign-up with GraphOS', 8 | command: 'extension.signUp', 9 | }; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/workbench/tree-data-providers/tree-items/graphos-supergraphs/studioAccountTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, TreeItemCollapsibleState } from 'vscode'; 2 | import { StudioGraphTreeItem } from './studioGraphTreeItem'; 3 | 4 | export class StudioAccountTreeItem extends TreeItem { 5 | children: StudioGraphTreeItem[] = new Array(); 6 | 7 | constructor( 8 | public readonly accountId: string, 9 | public readonly accountName?: string, 10 | ) { 11 | super(accountName ?? accountId, TreeItemCollapsibleState.Expanded); 12 | this.contextValue = 'studioAccountTreeItem'; 13 | } 14 | getChildren(element?: TreeItem): Thenable { 15 | return new Promise(() => this.children); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/workbench/tree-data-providers/tree-items/graphos-supergraphs/studioGraphTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, TreeItemCollapsibleState } from 'vscode'; 2 | import { StudioGraphVariantTreeItem } from './studioGraphVariantTreeItem'; 3 | 4 | export class StudioGraphTreeItem extends TreeItem { 5 | children: StudioGraphVariantTreeItem[] = 6 | new Array(); 7 | variants: string[] = []; 8 | 9 | constructor( 10 | public readonly graphId: string, 11 | public readonly graphName: string, 12 | ) { 13 | super(graphName, TreeItemCollapsibleState.Collapsed); 14 | this.contextValue = 'studioGraphTreeItem'; 15 | this.command = { 16 | title: 'Load Graph Operations', 17 | command: 'studio-graphs.loadOperationsFromGraphOS', 18 | arguments: [this], 19 | }; 20 | } 21 | getChildren(element?: TreeItem): Thenable { 22 | return new Promise(() => this.children); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/workbench/tree-data-providers/tree-items/graphos-supergraphs/studioGraphVariantServiceTreeItem.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { TreeItem, TreeItemCollapsibleState } from 'vscode'; 3 | 4 | export class StudioGraphVariantServiceTreeItem extends TreeItem { 5 | constructor( 6 | public readonly graphId: string, 7 | public readonly graphVariant: string, 8 | public readonly name, 9 | public readonly sdl: string, 10 | ) { 11 | super(name, TreeItemCollapsibleState.None); 12 | this.contextValue = 'studioGraphVariantServiceTreeItem'; 13 | this.iconPath = { 14 | light: path.join(__dirname, '..', 'media', 'graphql-logo.png'), 15 | dark: path.join(__dirname, '..', 'media', 'graphql-logo.png'), 16 | }; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/workbench/tree-data-providers/tree-items/graphos-supergraphs/studioGraphVariantTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, TreeItemCollapsibleState } from 'vscode'; 2 | import { StudioGraphVariantServiceTreeItem } from './studioGraphVariantServiceTreeItem'; 3 | 4 | export class StudioGraphVariantTreeItem extends TreeItem { 5 | children: StudioGraphVariantServiceTreeItem[] = 6 | new Array(); 7 | 8 | constructor( 9 | public readonly graphId: string, 10 | public readonly graphVariant: string, 11 | ) { 12 | super(graphVariant, TreeItemCollapsibleState.None); 13 | this.contextValue = 'studioGraphVariantTreeItem'; 14 | this.command = { 15 | title: 'Load Graph Operations', 16 | command: 'studio-graphs.loadOperationsFromGraphOS', 17 | arguments: [this, graphVariant], 18 | }; 19 | } 20 | getChildren(element?: TreeItem): Thenable { 21 | return new Promise(() => this.children); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/workbench/tree-data-providers/tree-items/local-supergraph-designs/addDesignOperationTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem } from 'vscode'; 2 | 3 | export class AddDesignOperationTreeItem extends TreeItem { 4 | constructor(public readonly wbFilePath: string) { 5 | super(`Add operation to design`); 6 | this.tooltip = `Add a GraphQL operation and UI design to your workbench design`; 7 | this.contextValue = 'addDesignOperationTreeItem'; 8 | this.command = { 9 | command: 'local-supergraph-designs.addOperation', 10 | title: `Add operation to design`, 11 | arguments: [this], 12 | }; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/workbench/tree-data-providers/tree-items/local-supergraph-designs/federationVersionItem.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, TreeItemCollapsibleState } from 'vscode'; 2 | import { media } from '../../superGraphTreeDataProvider'; 3 | 4 | export class FederationVersionItem extends TreeItem { 5 | constructor( 6 | public readonly wbFilePath: string, 7 | public readonly federation_version: string = '2', 8 | ) { 9 | super( 10 | `Apollo Federation v${federation_version}`, 11 | TreeItemCollapsibleState.None, 12 | ); 13 | 14 | this.contextValue = 'federationVersionItem'; 15 | this.iconPath = media('versions.svg'); 16 | this.command = { 17 | command: 'local-supergraph-designs.changeDesignFederationVersion', 18 | title: 'Change Apollo Federation Version', 19 | arguments: [this], 20 | }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/workbench/tree-data-providers/tree-items/local-supergraph-designs/operationSummaryTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, TreeItemCollapsibleState } from 'vscode'; 2 | import { StateManager } from '../../../stateManager'; 3 | import { Operation } from '../../../file-system/ApolloConfig'; 4 | import { OperationTreeItem } from './operationTreeItem'; 5 | 6 | export class OperationSummaryTreeItem extends TreeItem { 7 | operations: TreeItem[] = new Array(); 8 | 9 | constructor( 10 | private readonly operationConfigs: { 11 | [operationName: string]: Operation; 12 | } = {}, 13 | public readonly wbFilePath: string, 14 | ) { 15 | super( 16 | `${Object.keys(operationConfigs ?? []).length} Operations`, 17 | StateManager.settings_localDesigns_expandOperationsByDefault 18 | ? TreeItemCollapsibleState.Expanded 19 | : TreeItemCollapsibleState.Collapsed, 20 | ); 21 | 22 | this.tooltip = `${Object.keys(operationConfigs ?? []).length} operations`; 23 | this.contextValue = 'operationSummaryTreeItem'; 24 | 25 | Object.keys(operationConfigs ?? []).forEach((operationName) => 26 | this.operations.push( 27 | new OperationTreeItem( 28 | wbFilePath, 29 | operationName, 30 | operationConfigs[operationName], 31 | ), 32 | ), 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/workbench/tree-data-providers/tree-items/local-supergraph-designs/operationTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, TreeItemCollapsibleState } from 'vscode'; 2 | import { Operation } from '../../../file-system/ApolloConfig'; 3 | import { media } from '../../superGraphTreeDataProvider'; 4 | 5 | export class OperationTreeItem extends TreeItem { 6 | constructor( 7 | public readonly wbFilePath: string, 8 | public readonly operationName: string, 9 | public readonly operationConfig: Operation, 10 | ) { 11 | super(operationName, TreeItemCollapsibleState.None); 12 | 13 | this.contextValue = 'operationTreeItem'; 14 | this.tooltip = this.operationName; 15 | this.command = { 16 | command: 'local-supergraph-designs.viewOperationDesignSideBySide', 17 | title: 'Open operation in Sandbox', 18 | arguments: [this], 19 | }; 20 | 21 | if (operationConfig.document.includes('mutation')) 22 | this.iconPath = media('m.svg'); 23 | else this.iconPath = media('q.svg'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/workbench/tree-data-providers/tree-items/local-supergraph-designs/subgraphSummaryTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, TreeItemCollapsibleState } from 'vscode'; 2 | import { Subgraph } from '../../../file-system/ApolloConfig'; 3 | import { StateManager } from '../../../stateManager'; 4 | import { media } from '../../superGraphTreeDataProvider'; 5 | import { SubgraphTreeItem } from './subgraphTreeItem'; 6 | 7 | export class SubgraphSummaryTreeItem extends TreeItem { 8 | subgraphs: TreeItem[] = new Array(); 9 | 10 | constructor( 11 | private readonly subgraphConfigs: { [subgraphName: string]: Subgraph }, 12 | public readonly wbFilePath: string, 13 | ) { 14 | super( 15 | `${Object.keys(subgraphConfigs).length} subgraphs`, 16 | StateManager.settings_localDesigns_expandSubgraphsByDefault 17 | ? TreeItemCollapsibleState.Expanded 18 | : TreeItemCollapsibleState.Collapsed, 19 | ); 20 | 21 | this.tooltip = `${Object.keys(subgraphConfigs).length} Subgraphs`; 22 | this.contextValue = 'subgraphSummaryTreeItem'; 23 | 24 | Object.keys(subgraphConfigs).forEach((subgraphName) => { 25 | this.subgraphs.push( 26 | new SubgraphTreeItem( 27 | subgraphName, 28 | subgraphConfigs[subgraphName], 29 | wbFilePath, 30 | ), 31 | ); 32 | }); 33 | this.iconPath = { 34 | light: media('subgraph.svg'), 35 | dark: media('subgraph.svg'), 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/workbench/tree-data-providers/tree-items/local-supergraph-designs/subgraphTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, TreeItemCollapsibleState } from 'vscode'; 2 | import { Subgraph } from '../../../file-system/ApolloConfig'; 3 | import { media } from '../../superGraphTreeDataProvider'; 4 | 5 | export class SubgraphTreeItem extends TreeItem { 6 | constructor( 7 | public readonly subgraphName: string, 8 | public readonly subgraph: Subgraph, 9 | public readonly wbFilePath: string, 10 | ) { 11 | super( 12 | subgraph.schema.mocks?.enabled 13 | ? `${subgraphName} (mocked)` 14 | : subgraphName, 15 | TreeItemCollapsibleState.None, 16 | ); 17 | 18 | this.contextValue = 'subgraphTreeItem'; 19 | this.tooltip = this.subgraphName; 20 | this.command = { 21 | command: 'local-supergraph-designs.editSubgraph', 22 | title: 'Edit Schema', 23 | arguments: [this], 24 | }; 25 | this.iconPath = { 26 | light: media('graphql-logo.png'), 27 | dark: media('graphql-logo.png'), 28 | }; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/workbench/tree-data-providers/tree-items/local-supergraph-designs/supergraphTreeItem.ts: -------------------------------------------------------------------------------- 1 | import { TreeItem, TreeItemCollapsibleState } from 'vscode'; 2 | import { getFileName } from '../../../../utils/path'; 3 | import { SubgraphSummaryTreeItem } from './subgraphSummaryTreeItem'; 4 | import { OperationSummaryTreeItem } from './operationSummaryTreeItem'; 5 | import { ApolloConfig } from '../../../file-system/ApolloConfig'; 6 | 7 | export class SupergraphTreeItem extends TreeItem { 8 | subgraphsChild: SubgraphSummaryTreeItem; 9 | operationsChild: OperationSummaryTreeItem; 10 | 11 | constructor( 12 | public readonly wbFile: ApolloConfig, 13 | public readonly wbFilePath: string, 14 | ) { 15 | super( 16 | getFileName(wbFilePath) ?? 'unknown', 17 | TreeItemCollapsibleState.Expanded, 18 | ); 19 | this.subgraphsChild = new SubgraphSummaryTreeItem( 20 | wbFile.subgraphs, 21 | wbFilePath, 22 | ); 23 | this.operationsChild = new OperationSummaryTreeItem( 24 | wbFile.operations, 25 | wbFilePath, 26 | ); 27 | this.tooltip = (this.label as string) ?? ''; 28 | 29 | this.contextValue = 'supergraphTreeItem'; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/workbench/webviews/operationDesign.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Uri, 3 | ViewColumn, 4 | Webview, 5 | WebviewPanel, 6 | window, 7 | workspace, 8 | } from 'vscode'; 9 | import { whichDesign, whichOperation } from '../../utils/uiHelpers'; 10 | import { FileProvider } from '../file-system/fileProvider'; 11 | import { StateManager } from '../stateManager'; 12 | import { OperationTreeItem } from '../tree-data-providers/tree-items/local-supergraph-designs/operationTreeItem'; 13 | import { existsSync } from 'fs'; 14 | import { dirname, join } from 'path'; 15 | 16 | function getWebviewContent(webview: Webview, src: any) { 17 | return ` 18 | 19 | 20 | 24 | 29 | 30 | 31 |
32 | 33 |
34 | 35 | 36 | `; 37 | } 38 | let operationDesignPanel: WebviewPanel | undefined; 39 | 40 | function viewDesignImage(url: Uri) { 41 | if (operationDesignPanel) 42 | operationDesignPanel.webview.html = getWebviewContent( 43 | operationDesignPanel.webview, 44 | url, 45 | ); 46 | 47 | return operationDesignPanel; 48 | } 49 | 50 | async function viewDesignImageFromFile( 51 | wbFilePath: string, 52 | path: string, 53 | operationName?: string, 54 | ) { 55 | const panel = operationDesignPanel; 56 | if (panel) { 57 | //Copy image file to extension directory, media/temp is in gitignore 58 | const tempUri = Uri.joinPath( 59 | StateManager.instance.context.extensionUri, 60 | 'media', 61 | 'temp', 62 | `${operationName}.graphql`, 63 | ); 64 | let uri = Uri.parse(path); 65 | if (!existsSync(path)) { 66 | const dir = dirname(wbFilePath); 67 | const newPath = join(dir, path); 68 | uri = Uri.parse(newPath); 69 | } 70 | await workspace.fs.copy(uri, tempUri, { overwrite: true }); 71 | const localFile = panel.webview.asWebviewUri(tempUri); 72 | panel.webview.html = getWebviewContent(panel.webview, localFile); 73 | 74 | return panel; 75 | } 76 | } 77 | 78 | export async function viewOperationDesign(item?: OperationTreeItem) { 79 | const wbFilePath = item ? item.wbFilePath : await whichDesign(); 80 | if (!wbFilePath) return; 81 | const operationName = item 82 | ? item.operationName 83 | : await whichOperation(wbFilePath); 84 | if (!operationName) return; 85 | 86 | const wbFile = FileProvider.instance.workbenchFileFromPath(wbFilePath); 87 | const operation = wbFile.operations[operationName]; 88 | if (!operation.ui_design) { 89 | const response = await window.showInformationMessage( 90 | `No UI design saved for operation`, 91 | 'Add image source', 92 | ); 93 | if (response == 'Add image source') { 94 | const uiDesign = await window.showInputBox({ 95 | title: 'Enter the file path or remote url where the UI design image is', 96 | prompt: 97 | 'https://my-website.com/images/a.png or /Users/Me/Desktop/a.png', 98 | }); 99 | if (uiDesign) wbFile.operations[operationName].ui_design = uiDesign; 100 | 101 | await FileProvider.instance.writeWorkbenchConfig(wbFilePath, wbFile); 102 | 103 | await viewOperationDesign(item); 104 | } 105 | return; 106 | } 107 | 108 | if (!operationDesignPanel) { 109 | operationDesignPanel = window.createWebviewPanel( 110 | 'operationDesign', 111 | 'Operation Design', 112 | { viewColumn: ViewColumn.One, preserveFocus: false }, 113 | { 114 | enableScripts: false, 115 | }, 116 | ); 117 | 118 | operationDesignPanel.onDidDispose( 119 | (e) => (operationDesignPanel = undefined), 120 | ); 121 | } 122 | 123 | if (operation.ui_design) { 124 | const uri = Uri.parse(operation.ui_design); 125 | if (uri.scheme === 'file') 126 | return await viewDesignImageFromFile(wbFilePath, uri.fsPath); 127 | else return viewDesignImage(uri); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/workbench/webviews/sandbox.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { 3 | ProgressLocation, 4 | Uri, 5 | ViewColumn, 6 | WebviewPanel, 7 | window, 8 | } from 'vscode'; 9 | import { Rover } from '../rover'; 10 | import { StateManager } from '../stateManager'; 11 | import { OperationTreeItem } from '../tree-data-providers/tree-items/local-supergraph-designs/operationTreeItem'; 12 | import { parse, print } from 'graphql'; 13 | 14 | let panel: WebviewPanel | undefined; 15 | 16 | export async function refreshSandbox() { 17 | panel?.dispose(); 18 | await openSandboxWebview(); 19 | } 20 | 21 | export async function openSandbox(item?: OperationTreeItem, document?: string) { 22 | if (!Rover.instance.primaryDevTerminal && item?.wbFilePath) { 23 | await window.withProgress( 24 | { 25 | title: `Starting rover dev session`, 26 | cancellable: false, 27 | location: ProgressLocation.Notification, 28 | }, 29 | async (progress) => { 30 | await Rover.instance.startRoverDev(item.wbFilePath, progress); 31 | }, 32 | ); 33 | await openSandboxWebview(print(parse(item?.operationConfig?.document))); 34 | } else window.showErrorMessage('Unable to open sandbox, no design running.'); 35 | } 36 | 37 | export async function openSandboxWebview(document?: string) { 38 | if (!panel) { 39 | panel = window.createWebviewPanel( 40 | 'apolloSandbox', 41 | 'Apollo Sandbox', 42 | ViewColumn.One, 43 | { 44 | enableScripts: true, 45 | retainContextWhenHidden: true, 46 | }, 47 | ); 48 | panel.iconPath = Uri.parse( 49 | path.join(__dirname, '..', 'media', 'logo-apollo.svg'), 50 | ); 51 | 52 | panel.onDidDispose(() => (panel = undefined)); 53 | } 54 | 55 | const routerPort = StateManager.settings_routerPort; 56 | const url = document 57 | ? `http://localhost:${routerPort}?document=${encodeURIComponent(document)}` 58 | : `http://localhost:${routerPort}`; 59 | panel.webview.html = getWebviewContent(url); 60 | 61 | await new Promise((resolve) => setTimeout(resolve, 500)); 62 | 63 | panel.reveal(ViewColumn.One, false); 64 | } 65 | 66 | function getWebviewContent(url: string) { 67 | return ` 68 | 69 | 70 | 75 | 76 | 77 | 78 |