├── .changeset └── config.json ├── .circleci └── config.yml ├── .codesandbox └── ci.json ├── .envrc ├── .eslintrc.cjs ├── .github └── workflows │ └── release-pr.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── cspell-dict.txt ├── cspell.yaml ├── jest.config.ts ├── package-lock.json ├── package.json ├── renovate.json5 ├── src ├── __tests__ │ ├── integration.test.ts │ └── koa.test.ts ├── index.ts └── utils.ts ├── tsconfig.base.json ├── tsconfig.build.json ├── tsconfig.eslint.json ├── tsconfig.json └── tsconfig.test.json /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.6.3/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "apollo-server-integrations/apollo-server-integration-koa" } 6 | ], 7 | "commit": false, 8 | "access": "public", 9 | "baseBranch": "main" 10 | } 11 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | node: circleci/node@5.3.0 5 | 6 | commands: 7 | install-volta: 8 | description: Install volta to manage Node/npm versions 9 | steps: 10 | - run: 11 | name: Install volta 12 | # Teach the volta installer to update Circle's special env 13 | # file instead of the default. 14 | command: | 15 | curl https://get.volta.sh | PROFILE="$BASH_ENV" bash 16 | 17 | setup-node: 18 | parameters: 19 | node-version: 20 | type: string 21 | default: '' 22 | steps: 23 | - install-volta 24 | - checkout 25 | - when: 26 | condition: << parameters.node-version >> 27 | steps: 28 | - run: volta pin node@<< parameters.node-version >> 29 | - run: node --version 30 | - run: npm --version 31 | - node/install-packages 32 | 33 | jobs: 34 | NodeJS: 35 | parameters: 36 | node-version: 37 | type: string 38 | docker: 39 | - image: cimg/base:stable 40 | steps: 41 | - setup-node: 42 | node-version: <> 43 | - run: npm run build 44 | - run: npm run test:ci 45 | - store_test_results: 46 | path: junit.xml 47 | 48 | Full incremental delivery tests with graphql-js 17 canary: 49 | docker: 50 | - image: cimg/base:stable 51 | environment: 52 | INCREMENTAL_DELIVERY_TESTS_ENABLED: t 53 | steps: 54 | - setup-node: 55 | node-version: "18" 56 | # Install a prerelease of graphql-js 17 with incremental delivery support. 57 | # --legacy-peer-deps because nothing expects v17 yet. 58 | # --no-engine-strict because Node v18 doesn't match the engines fields 59 | # on some old stuff. 60 | - run: npm i --legacy-peer-deps --no-engine-strict graphql@17.0.0-alpha.1.canary.pr.3361.04ab27334641e170ce0e05bc927b972991953882 61 | - run: npm run test:ci 62 | - store_test_results: 63 | path: junit.xml 64 | 65 | Prettier: 66 | docker: 67 | - image: cimg/base:stable 68 | steps: 69 | - setup-node 70 | - run: npm run prettier-check 71 | 72 | Lint: 73 | docker: 74 | - image: cimg/base:stable 75 | steps: 76 | - setup-node 77 | - run: npm run lint 78 | 79 | Spell Check: 80 | docker: 81 | - image: cimg/base:stable 82 | steps: 83 | - setup-node 84 | - run: npm run spell-check 85 | 86 | workflows: 87 | version: 2 88 | Build: 89 | jobs: 90 | - NodeJS: 91 | name: NodeJS << matrix.node-version >> 92 | matrix: 93 | parameters: 94 | node-version: 95 | - "16" 96 | - "18" 97 | - "20" 98 | - Full incremental delivery tests with graphql-js 17 canary 99 | - Prettier 100 | - Lint 101 | - Spell Check 102 | -------------------------------------------------------------------------------- /.codesandbox/ci.json: -------------------------------------------------------------------------------- 1 | { 2 | "sandboxes": ["apollo-server-koa-js", "apollo-server-koa-ts"], 3 | "node": "18" 4 | } 5 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | export VOLTA_HOME="$PWD/.volta" 2 | PATH_add "$VOLTA_HOME/bin" 3 | 4 | if ! [ -f "$VOLTA_HOME/bin/volta" ]; then 5 | echo "Volta not found in $VOLTA_HOME/bin/volta, installing..." 6 | curl https://get.volta.sh/ | bash 7 | fi 8 | 9 | # Allow you to run jest and other things in node_modules/.bin without npx. 10 | layout node -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | overrides: [ 3 | { 4 | files: ['src/**/*.ts'], 5 | parser: '@typescript-eslint/parser', 6 | plugins: ['@typescript-eslint'], 7 | parserOptions: { 8 | project: 'tsconfig.eslint.json', 9 | tsconfigRootDir: __dirname, 10 | }, 11 | rules: { 12 | '@typescript-eslint/consistent-type-imports': 'error', 13 | }, 14 | }, 15 | ], 16 | root: true, 17 | }; 18 | -------------------------------------------------------------------------------- /.github/workflows/release-pr.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@v4 15 | with: 16 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 17 | fetch-depth: 0 18 | 19 | - name: Setup Node.js 16.x 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: 16.x 23 | 24 | - name: Install Dependencies 25 | run: npm i 26 | 27 | - name: Create Release Pull Request / NPM Publish 28 | uses: changesets/action@v1 29 | with: 30 | publish: npm run publish-changeset 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore the compiled output. 2 | dist/ 3 | 4 | # TypeScript incremental compilation cache 5 | *.tsbuildinfo 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Coverage (from Jest) 15 | coverage/ 16 | 17 | # JUnit Reports (used mainly in CircleCI) 18 | reports/ 19 | junit.xml 20 | 21 | # Node modules 22 | node_modules/ 23 | 24 | # Mac OS 25 | .DS_Store 26 | 27 | # Intellij Configuration Files 28 | .idea/ 29 | 30 | # Volta binaries (when using direnv/.envrc) 31 | .volta -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !src/**/* 3 | src/**/__tests__/** 4 | !dist/**/* 5 | dist/**/__tests__/** 6 | !package.json 7 | !README.md 8 | !tsconfig.base.json 9 | !tsconfig.json 10 | !tsconfig.build.json -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.json 2 | *.json5 3 | *.yml 4 | *.md 5 | 6 | dist/ 7 | 8 | .volta -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 1.1.1 4 | 5 | ### Patch Changes 6 | 7 | - [#132](https://github.com/apollo-server-integrations/apollo-server-integration-koa/pull/132) [`01fa9d1`](https://github.com/apollo-server-integrations/apollo-server-integration-koa/commit/01fa9d15aa8ee24ad7469d4787cf9c8407988ee5) Thanks [@ryota-ka](https://github.com/ryota-ka)! - Parameterize `KoaContextFunctionArgument` and `KoaMiddlewareOptions` so that the `ctx` parameter in the `context` function is correctly typed when this library is used along with a context-parameterized Koa app. 8 | 9 | ## 1.1.0 10 | 11 | ### Minor Changes 12 | 13 | - [#130](https://github.com/apollo-server-integrations/apollo-server-integration-koa/pull/130) [`c504479`](https://github.com/apollo-server-integrations/apollo-server-integration-koa/commit/c5044795f3553009f4e20dc3c0757e3303e70a5a) Thanks [@ryota-ka](https://github.com/ryota-ka)! - Parameterize the returned Middleware type so that this library can be used along with a context-parameterized Koa app. 14 | 15 | In order to utilize this feature, provide your Koa app's `State` and `Context` types as type arguments to the `koaMiddleware` function. 16 | 17 | If you use a `context` function (for Apollo Server), the `State` and `Context` types are positions 1 and 2 like so: 18 | 19 | ```ts 20 | type KoaState = { state: object }; 21 | type KoaContext = { context: object }; 22 | 23 | koaMiddleware( 24 | new ApolloServer(), 25 | //... 26 | { 27 | context: async ({ ctx }) => ({ graphqlContext: {} }), 28 | }, 29 | ); 30 | ``` 31 | 32 | If you don't use a `context` function, the `State` and `Context` types are positions 0 and 1 like so: 33 | 34 | ```ts 35 | type KoaState = { state: object }; 36 | type KoaContext = { context: object }; 37 | 38 | koaMiddleware( 39 | new ApolloServer(), 40 | //... 41 | ); 42 | ``` 43 | 44 | ## 1.0.0 45 | 46 | ### Major Changes 47 | 48 | - [#117](https://github.com/apollo-server-integrations/apollo-server-integration-koa/pull/117) [`4567c98`](https://github.com/apollo-server-integrations/apollo-server-integration-koa/commit/4567c982adeaa4a201ec133f1d7afa77eddb3b93) Thanks [@trevor-scheer](https://github.com/trevor-scheer)! - Drop support for Node.js 14, require v16+ 49 | 50 | The only change required for users to take this update is to upgrade their Node.js version to v16+. No other breaking changes. 51 | 52 | ## 0.3.0 53 | 54 | ### Minor Changes 55 | 56 | - [#85](https://github.com/apollo-server-integrations/apollo-server-integration-koa/pull/85) [`bb28b66`](https://github.com/apollo-server-integrations/apollo-server-integration-koa/commit/bb28b66c60151289e4fee51ce58443b000e06056) Thanks [@laverdet](https://github.com/laverdet)! - Implement support for the @defer directive. In order to use this feature, you must be using an appropriate version of `graphql`. At the time of writing this, @defer is only available in v17 alpha versions of the `graphql` package, which is currently not officially supported. Due to peer dependencies, you must install graphql like so in order to force v17: 57 | `npm i graphql@alpha --legacy-peer-deps` 58 | 59 | ## 0.2.1 60 | 61 | ### Patch Changes 62 | 63 | - [#43](https://github.com/apollo-server-integrations/apollo-server-integration-koa/pull/43) [`997b160`](https://github.com/apollo-server-integrations/apollo-server-integration-koa/commit/997b160c888f970b3f39abdfd01fb95f83d3fa57) Thanks [@trevor-scheer](https://github.com/trevor-scheer)! - Remove effectful install 64 | 65 | ## 0.2.0 66 | 67 | ### Minor Changes 68 | 69 | - [#26](https://github.com/apollo-server-integrations/apollo-server-integration-koa/pull/26) [`a284fe2`](https://github.com/apollo-server-integrations/apollo-server-integration-koa/commit/a284fe2bab5da9fad13d8cf5d4cb5a011443fe15) Thanks [@renovate](https://github.com/apps/renovate)! - Officially support Apollo Server v4.0.0 70 | 71 | ## 0.1.0 72 | 73 | ### Minor Changes 74 | 75 | - [#19](https://github.com/apollo-server-integrations/apollo-server-integration-koa/pull/19) [`61106d1`](https://github.com/apollo-server-integrations/apollo-server-integration-koa/commit/61106d1ed4f7a0e3f94feb117ed69c4ca86efe5d) Thanks [@matthew-gordon](https://github.com/matthew-gordon)! - Update Apollo Server dependencies, add explicit non-support for incremental delivery for now" 76 | 77 | ## 0.0.12 78 | 79 | ### Patch Changes 80 | 81 | - [#17](https://github.com/apollo-server-integrations/apollo-server-integration-koa/pull/17) [`0aef5c3`](https://github.com/apollo-server-integrations/apollo-server-integration-koa/commit/0aef5c3d83d9f9495a785b350712c4703b9257b4) Thanks [@matthew-gordon](https://github.com/matthew-gordon)! - Update repository docs/ownership 82 | 83 | ## 0.0.11 84 | 85 | ### Patch Changes 86 | 87 | - [#15](https://github.com/apollo-server-integrations/apollo-server-integration-koa/pull/15) [`a192085`](https://github.com/apollo-server-integrations/apollo-server-integration-koa/commit/a1920855fecd5a0bb1afc0961a86123c960e0508) Thanks [@matthew-gordon](https://github.com/matthew-gordon)! - Update keywords 88 | 89 | ## 0.0.10 90 | 91 | ### Patch Changes 92 | 93 | - [`7b90754`](https://github.com/apollo-server-integrations/apollo-server-integration-koa/commit/7b9075459e4937be136a841793a279abf826dbbe) Thanks [@matthew-gordon](https://github.com/matthew-gordon)! - Update docs 94 | 95 | ## 0.0.9 96 | 97 | ### Patch Changes 98 | 99 | - [#10](https://github.com/apollo-server-integrations/apollo-server-integration-koa/pull/10) [`ab78fd4`](https://github.com/apollo-server-integrations/apollo-server-integration-koa/commit/ab78fd42d99d4ba1d52975f718c9fb292a85008a) Thanks [@matthew-gordon](https://github.com/matthew-gordon)! - Update deprecated call 100 | 101 | ## 0.0.8 102 | 103 | ### Patch Changes 104 | 105 | - [#8](https://github.com/apollo-server-integrations/apollo-server-integration-koa/pull/8) [`51fcd01`](https://github.com/apollo-server-integrations/apollo-server-integration-koa/commit/51fcd01923b785d1dd707a994c705f645e20efaf) Thanks [@matthew-gordon](https://github.com/matthew-gordon)! - Update build scripts for better DX 106 | 107 | ## 0.0.7 108 | 109 | ### Patch Changes 110 | 111 | - [`69ac68f`](https://github.com/apollo-server-integrations/apollo-server-integration-koa/commit/69ac68f4d86be8a1c629ac777c1f13509cccd7a4) Thanks [@matthew-gordon](https://github.com/matthew-gordon)! - Update LICENSE 112 | 113 | ## 0.0.6 114 | 115 | ### Patch Changes 116 | 117 | - [`734f7c7`](https://github.com/apollo-server-integrations/apollo-server-integration-koa/commit/734f7c7a2e1bd9aa850294f44f8c504baaea15e2) Thanks [@matthew-gordon](https://github.com/matthew-gordon)! - small cleanup 118 | 119 | - [`734f7c7`](https://github.com/apollo-server-integrations/apollo-server-integration-koa/commit/734f7c7a2e1bd9aa850294f44f8c504baaea15e2) Thanks [@matthew-gordon](https://github.com/matthew-gordon)! - small cleanup 120 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 - Matt Gordon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `@as-integrations/koa` 2 | 3 | ## A TypeScript/JavaScript GraphQL middleware for `@apollo/server` 4 | 5 | ## Getting started: Koa middleware 6 | 7 | Apollo Server enables the ability to add middleware that lets you run your GraphQL server as part of an app built with Koa, one of the most popular web frameworks for Node. 8 | 9 | First, install Apollo Server, the JavaScript implementation of the core GraphQL algorithms, Koa, and two common Koa middleware packages: 10 | 11 | ``` 12 | npm install @as-integrations/koa @apollo/server graphql koa @koa/cors koa-bodyparser 13 | ``` 14 | 15 | Then, write the following to server.mjs. (By using the .mjs extension, Node lets you use the await keyword at the top level.) 16 | 17 | ```js 18 | import http from "http"; 19 | import Koa from "koa"; 20 | import bodyParser from "koa-bodyparser"; 21 | import cors from "@koa/cors"; 22 | import { ApolloServer } from "@apollo/server"; 23 | import { ApolloServerPluginDrainHttpServer } from "@apollo/server/plugin/drainHttpServer"; 24 | import { koaMiddleware } from "@as-integrations/koa"; 25 | 26 | // The GraphQL schema 27 | const typeDefs = `#graphql 28 | type Query { 29 | hello: String 30 | } 31 | `; 32 | 33 | // A map of functions which return data for the schema. 34 | const resolvers = { 35 | Query: { 36 | hello: () => "world", 37 | }, 38 | }; 39 | 40 | const app = new Koa(); 41 | const httpServer = http.createServer(app.callback()); 42 | 43 | // Set up Apollo Server 44 | const server = new ApolloServer({ 45 | typeDefs, 46 | resolvers, 47 | plugins: [ApolloServerPluginDrainHttpServer({ httpServer })], 48 | }); 49 | await server.start(); 50 | 51 | app.use(cors()); 52 | app.use(bodyParser()); 53 | app.use( 54 | koaMiddleware(server, { 55 | context: async ({ ctx }) => ({ token: ctx.headers.token }), 56 | }) 57 | ); 58 | 59 | await new Promise((resolve) => httpServer.listen({ port: 4000 }, resolve)); 60 | console.log(`🚀 Server ready at http://localhost:4000`); 61 | ``` 62 | 63 | Now run your server with: 64 | 65 | ``` 66 | node server.mjs 67 | ``` 68 | 69 | Open the URL it prints in a web browser. It will show Apollo Sandbox, a web-based tool for running GraphQL operations. Try running the operation `query { hello }`! -------------------------------------------------------------------------------- /cspell-dict.txt: -------------------------------------------------------------------------------- 1 | apollographql 2 | asynciterable 3 | changesets 4 | cimg 5 | circleci 6 | codesandbox 7 | direnv 8 | opde 9 | bodyparser 10 | testsuite 11 | withrequired 12 | effectful -------------------------------------------------------------------------------- /cspell.yaml: -------------------------------------------------------------------------------- 1 | # Configuration for the cspell command-line tool and the Code Spell Checker 2 | # VSCode extension 3 | # (https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker). 4 | 5 | # Add a repo-specific dictionary. Any new real words can be added to 6 | # cspell-dict.txt, one per line. 7 | dictionaryDefinitions: 8 | - name: workspace 9 | path: './cspell-dict.txt' 10 | description: Custom Workspace Dictionary 11 | addWords: true 12 | scope: workspace 13 | dictionaries: 14 | - workspace 15 | 16 | # Ignore files that aren't check in to git as well as files that aren't written 17 | # by hand. Note that we do want to check, say, JSON files (as package.json 18 | # contains English text like package descriptions). 19 | useGitignore: true 20 | ignorePaths: 21 | - cspell.yaml 22 | - .changeset/**/* 23 | 24 | ignoreRegExpList: 25 | # GitHub Security Advisories 26 | - GHSA-[-\w]+ 27 | 28 | overrides: 29 | # Ignore anything in a changelog file that looks like a GitHub username. 30 | - filename: '**/CHANGELOG.md' 31 | ignoreRegExpList: 32 | - "@[-\\w]+" 33 | # Ignore the targets of links in Markdown/MDX files. 34 | - filename: '**/*.md*' 35 | ignoreRegExpList: 36 | - "\\]\\([^)]+\\)" 37 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types'; 2 | 3 | const config: Config.InitialOptions = { 4 | preset: 'ts-jest', 5 | testEnvironment: 'node', 6 | roots: ['src'], 7 | globals: { 8 | 'ts-jest': { 9 | tsconfig: 'tsconfig.test.json', 10 | }, 11 | }, 12 | testRegex: '/__tests__/.*.test.ts$', 13 | verbose: true, 14 | }; 15 | 16 | export default config; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@as-integrations/koa", 3 | "description": "Apollo server integration for koa framework", 4 | "version": "1.1.1", 5 | "author": "Matt Gordon ", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/apollo-server-integrations/apollo-server-integration-koa" 10 | }, 11 | "keywords": [ 12 | "GraphQL", 13 | "Apollo", 14 | "Server", 15 | "Koa", 16 | "Javascript" 17 | ], 18 | "homepage": "https://github.com/apollo-server-integrations/apollo-server-integration-koa#readme", 19 | "bugs": { 20 | "url": "https://github.com/apollo-server-integrations/apollo-server-integration-koa/issues" 21 | }, 22 | "main": "dist/index.js", 23 | "types": "dist/index.d.ts", 24 | "engines": { 25 | "node": ">=16.0" 26 | }, 27 | "scripts": { 28 | "build": "tsc --build tsconfig.build.json", 29 | "clean": "git clean -dfqX", 30 | "prepack": "npm run build", 31 | "prettier-check": "prettier --check .", 32 | "prettier-fix": "prettier --write .", 33 | "publish-changeset": "changeset publish", 34 | "spell-check": "cspell lint '**' --no-progress || (echo 'Add any real words to cspell-dict.txt.'; exit 1)", 35 | "test": "jest", 36 | "test:ci": "jest --coverage --ci --maxWorkers=2 --reporters=default --reporters=jest-junit", 37 | "watch": "tsc --build --watch", 38 | "lint": "eslint src/**/*.ts" 39 | }, 40 | "devDependencies": { 41 | "@apollo/server": "4.11.3", 42 | "@apollo/server-integration-testsuite": "4.11.3", 43 | "@changesets/changelog-github": "0.5.1", 44 | "@changesets/cli": "2.28.1", 45 | "@jest/globals": "29.7.0", 46 | "@koa/cors": "4.0.0", 47 | "@types/jest": "29.5.14", 48 | "@types/koa": "2.15.0", 49 | "@types/koa__cors": "4.0.3", 50 | "@types/koa-bodyparser": "4.3.12", 51 | "@types/node": "16.18.126", 52 | "@types/supertest": "2.0.16", 53 | "@typescript-eslint/eslint-plugin": "6.21.0", 54 | "@typescript-eslint/parser": "6.21.0", 55 | "cspell": "7.3.9", 56 | "eslint": "8.57.1", 57 | "graphql": "16.10.0", 58 | "jest": "29.7.0", 59 | "jest-junit": "16.0.0", 60 | "koa": "2.16.0", 61 | "koa-bodyparser": "4.4.1", 62 | "prettier": "3.5.3", 63 | "supertest": "6.3.4", 64 | "ts-jest": "29.3.0", 65 | "ts-node": "10.9.2", 66 | "typescript": "5.8.2" 67 | }, 68 | "peerDependencies": { 69 | "@apollo/server": "^4.0.0", 70 | "koa": "^2.0.0" 71 | }, 72 | "volta": { 73 | "node": "20.19.0", 74 | "npm": "10.9.2" 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:js-lib", 4 | "group:allNonMajor", 5 | "group:jestPlusTSJest", 6 | "group:jestPlusTypes", 7 | // Our default configuration. See 8 | // https://github.com/apollographql/renovate-config-apollo-open-source/blob/master/package.json 9 | "apollo-open-source", 10 | ], 11 | "packageRules": [ 12 | // We set this to the lowest supported Node.js version to ensure we don't 13 | // use newer Node.js APIs unknowingly during development which are going to 14 | // fail in CI anyway when they're run against the full range of Node.js 15 | // versions we support. 16 | { 17 | "matchPackageNames": ["@types/node"], 18 | "allowedVersions": "16.x" 19 | }, 20 | ], 21 | } 22 | -------------------------------------------------------------------------------- /src/__tests__/integration.test.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import Koa from 'koa'; 3 | import bodyParser from 'koa-bodyparser'; 4 | import cors from '@koa/cors'; 5 | import { 6 | type ApolloServerOptions, 7 | type BaseContext, 8 | ApolloServer, 9 | } from '@apollo/server'; 10 | import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer'; 11 | import type { CreateServerForIntegrationTestsOptions } from '@apollo/server-integration-testsuite'; 12 | import { defineIntegrationTestSuite } from '@apollo/server-integration-testsuite'; 13 | import { koaMiddleware } from '..'; 14 | import { urlForHttpServer } from '../utils'; 15 | 16 | defineIntegrationTestSuite(async function ( 17 | serverOptions: ApolloServerOptions, 18 | testOptions?: CreateServerForIntegrationTestsOptions, 19 | ) { 20 | const app = new Koa(); 21 | // disable logs to console.error 22 | app.silent = true; 23 | 24 | const httpServer = http.createServer(app.callback()); 25 | const server = new ApolloServer({ 26 | ...serverOptions, 27 | plugins: [ 28 | ...(serverOptions.plugins ?? []), 29 | ApolloServerPluginDrainHttpServer({ 30 | httpServer, 31 | }), 32 | ], 33 | }); 34 | 35 | await server.start(); 36 | app.use(cors()); 37 | app.use(bodyParser()); 38 | app.use( 39 | koaMiddleware(server, { 40 | context: testOptions?.context, 41 | }), 42 | ); 43 | await new Promise((resolve) => { 44 | httpServer.listen({ port: 0 }, resolve); 45 | }); 46 | return { server, url: urlForHttpServer(httpServer) }; 47 | }); 48 | -------------------------------------------------------------------------------- /src/__tests__/koa.test.ts: -------------------------------------------------------------------------------- 1 | import { ApolloServer } from '@apollo/server'; 2 | import koa from 'koa'; 3 | import request from 'supertest'; 4 | import { it, expect } from '@jest/globals'; 5 | import { koaMiddleware } from '..'; 6 | 7 | it('gives helpful error if body-parser middleware is not installed', async () => { 8 | const server = new ApolloServer({ typeDefs: 'type Query {f: ID}' }); 9 | await server.start(); 10 | const app = new koa(); 11 | 12 | // Note lack of `bodyParser` here. 13 | app.use(koaMiddleware(server)); 14 | 15 | await request(app.callback()) 16 | .post('/') 17 | .send({ query: '{hello}' }) 18 | .expect(500, /forgot to set up the `koa-bodyparser`/); 19 | await server.stop(); 20 | }); 21 | 22 | it('not calling start causes a clear error', async () => { 23 | const server = new ApolloServer({ typeDefs: 'type Query {f: ID}' }); 24 | expect(() => koaMiddleware(server)).toThrow( 25 | 'You must `await server.start()`', 26 | ); 27 | }); 28 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Readable } from 'node:stream'; 2 | import { parse } from 'node:url'; 3 | import type { WithRequired } from '@apollo/utils.withrequired'; 4 | import { 5 | type ApolloServer, 6 | type BaseContext, 7 | type ContextFunction, 8 | type HTTPGraphQLRequest, 9 | HeaderMap, 10 | } from '@apollo/server'; 11 | import type Koa from 'koa'; 12 | // we need the extended `Request` type from `koa-bodyparser`, 13 | // this is similar to an effectful import but for types, since 14 | // the `koa-bodyparser` types "polyfill" the `koa` types 15 | import type * as _ from 'koa-bodyparser'; 16 | 17 | export interface KoaContextFunctionArgument< 18 | StateT = Koa.DefaultState, 19 | ContextT = Koa.DefaultContext, 20 | > { 21 | ctx: Koa.ParameterizedContext; 22 | } 23 | 24 | interface KoaMiddlewareOptions { 25 | context?: ContextFunction< 26 | [KoaContextFunctionArgument], 27 | TContext 28 | >; 29 | } 30 | 31 | export function koaMiddleware< 32 | StateT = Koa.DefaultState, 33 | ContextT = Koa.DefaultContext, 34 | >( 35 | server: ApolloServer, 36 | options?: KoaMiddlewareOptions, 37 | ): Koa.Middleware; 38 | export function koaMiddleware< 39 | TContext extends BaseContext, 40 | StateT = Koa.DefaultState, 41 | ContextT = Koa.DefaultContext, 42 | >( 43 | server: ApolloServer, 44 | options: WithRequired< 45 | KoaMiddlewareOptions, 46 | 'context' 47 | >, 48 | ): Koa.Middleware; 49 | export function koaMiddleware< 50 | TContext extends BaseContext, 51 | StateT = Koa.DefaultState, 52 | ContextT = Koa.DefaultContext, 53 | >( 54 | server: ApolloServer, 55 | options?: KoaMiddlewareOptions, 56 | ): Koa.Middleware { 57 | server.assertStarted('koaMiddleware()'); 58 | 59 | // This `any` is safe because the overload above shows that context can 60 | // only be left out if you're using BaseContext as your context, and {} is a 61 | // valid BaseContext. 62 | const defaultContext: ContextFunction< 63 | [KoaContextFunctionArgument], 64 | any 65 | > = async () => ({}); 66 | 67 | const context: ContextFunction< 68 | [KoaContextFunctionArgument], 69 | TContext 70 | > = options?.context ?? defaultContext; 71 | 72 | return async (ctx) => { 73 | if (!ctx.request.body) { 74 | // The json koa-bodyparser *always* sets ctx.request.body to {} if it's unset (even 75 | // if the Content-Type doesn't match), so if it isn't set, you probably 76 | // forgot to set up koa-bodyparser. 77 | ctx.status = 500; 78 | ctx.body = 79 | '`ctx.request.body` is not set; this probably means you forgot to set up the ' + 80 | '`koa-bodyparser` middleware before the Apollo Server middleware.'; 81 | return; 82 | } 83 | 84 | const incomingHeaders = new HeaderMap(); 85 | for (const [key, value] of Object.entries(ctx.headers)) { 86 | if (value !== undefined) { 87 | // Node/Koa headers can be an array or a single value. We join 88 | // multi-valued headers with `, ` just like the Fetch API's `Headers` 89 | // does. We assume that keys are already lower-cased (as per the Node 90 | // docs on IncomingMessage.headers) and so we don't bother to lower-case 91 | // them or combine across multiple keys that would lower-case to the 92 | // same value. 93 | incomingHeaders.set( 94 | key, 95 | Array.isArray(value) ? value.join(', ') : value, 96 | ); 97 | } 98 | } 99 | 100 | const httpGraphQLRequest: HTTPGraphQLRequest = { 101 | method: ctx.method.toUpperCase(), 102 | headers: incomingHeaders, 103 | search: parse(ctx.url).search ?? '', 104 | body: ctx.request.body, 105 | }; 106 | 107 | const { body, headers, status } = await server.executeHTTPGraphQLRequest({ 108 | httpGraphQLRequest, 109 | context: () => context({ ctx }), 110 | }); 111 | 112 | if (body.kind === 'complete') { 113 | ctx.body = body.string; 114 | } else if (body.kind === 'chunked') { 115 | ctx.body = Readable.from( 116 | (async function* () { 117 | for await (const chunk of body.asyncIterator) { 118 | yield chunk; 119 | if (typeof ctx.body.flush === 'function') { 120 | // If this response has been piped to a writable compression stream then `flush` after 121 | // each chunk. 122 | // This is identical to the Express integration: 123 | // https://github.com/apollographql/apollo-server/blob/a69580565dadad69de701da84092e89d0fddfa00/packages/server/src/express4/index.ts#L96-L105 124 | ctx.body.flush(); 125 | } 126 | } 127 | })(), 128 | ); 129 | } else { 130 | throw Error(`Delivery method ${(body as any).kind} not implemented`); 131 | } 132 | 133 | if (status !== undefined) { 134 | ctx.status = status; 135 | } 136 | for (const [key, value] of headers) { 137 | ctx.set(key, value); 138 | } 139 | }; 140 | } 141 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { Server } from 'http'; 2 | import type { AddressInfo } from 'net'; 3 | import { format } from 'url'; 4 | 5 | export function urlForHttpServer(httpServer: Server): string { 6 | const { address, port } = httpServer.address() as AddressInfo; 7 | 8 | // Convert IPs which mean "any address" (IPv4 or IPv6) into localhost 9 | // corresponding loopback ip. Note that the url field we're setting is 10 | // primarily for consumption by our test suite. If this heuristic is wrong for 11 | // your use case, explicitly specify a frontend host (in the `host` option 12 | // when listening). 13 | const hostname = address === '' || address === '::' ? 'localhost' : address; 14 | 15 | return format({ 16 | protocol: 'http', 17 | hostname, 18 | port, 19 | pathname: '/', 20 | }); 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "./src", 4 | "outDir": "./dist", 5 | "target": "es2019", 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "esModuleInterop": true, 9 | "sourceMap": true, 10 | "declaration": true, 11 | "declarationMap": true, 12 | "removeComments": true, 13 | "strict": true, 14 | "noImplicitAny": true, 15 | "noImplicitReturns": true, 16 | "noImplicitOverride": true, 17 | "noFallthroughCasesInSwitch": true, 18 | "noUnusedParameters": true, 19 | "noUnusedLocals": true, 20 | "forceConsistentCasingInFileNames": true, 21 | "lib": ["es2019", "esnext.asynciterable"], 22 | "types": ["node"], 23 | "baseUrl": ".", 24 | "paths": { 25 | "*" : ["types/*"] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["src/**/*"], 4 | "exclude": ["**/__tests__"], 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["src/**/*.ts"], 4 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | }, 5 | "files": [], 6 | "include": [], 7 | "references": [ 8 | { "path": "./tsconfig.build.json" }, 9 | { "path": "./tsconfig.test.json" }, 10 | ] 11 | } -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": ["src/**/*"], 4 | "compilerOptions": { 5 | "noEmit": true, 6 | "types": ["node", "jest"] 7 | } 8 | } --------------------------------------------------------------------------------