├── .github ├── FUNDING.yml └── workflows │ ├── main.yml │ └── publish.yml ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── audit_test.ts ├── deno.json ├── deno.lock ├── deps.ts ├── egg.json ├── examples ├── denoflare │ ├── .denoflare │ ├── README.md │ └── mod.ts ├── oak.ts ├── req-ctx.ts └── vanilla.ts ├── graphiql ├── README.md ├── markup.ts ├── render.ts └── render_test.ts ├── logo.png ├── mod.ts ├── mod_test.ts └── types.ts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | issuehunt: talentlessguy 4 | github: [talentlessguy] 5 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # Controls when the action will run. Triggers the workflow on push or pull request 4 | # events but only for the master branch 5 | on: 6 | push: 7 | branches: [master] 8 | pull_request: 9 | branches: [master] 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: denoland/setup-deno@v2 17 | with: 18 | deno-version: v2.x 19 | - name: Run tests 20 | run: deno task test 21 | - name: Create coverage report 22 | run: deno coverage ./coverage --lcov > coverage.lcov 23 | - name: Coveralls 24 | uses: coverallsapp/github-action@master 25 | with: 26 | github-token: ${{ secrets.GITHUB_TOKEN }} 27 | path-to-lcov: ./coverage.lcov 28 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | branches: 5 | - master 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | 11 | permissions: 12 | contents: read 13 | id-token: write 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Publish package 19 | run: npx jsr publish 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage.lcov 2 | coverage -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "deno.enable": true, 3 | "deno.lint": true, 4 | "deno.unstable": false, 5 | "editor.formatOnSave": true, 6 | "[typescript]": { "editor.defaultFormatter": "denoland.vscode-deno" }, 7 | "eslint.enable": false 8 | } 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Deno libraries 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 |

4 | 5 | [![nest badge][nest-badge]](https://nest.land/package/gql) 6 | [![GitHub Workflow Status][gh-actions-img]][github-actions] 7 | [![Codecov][cov-badge]][cov] [![][docs-badge]][docs] 8 | [![][code-quality-img]][code-quality] 9 | 10 |
11 | 12 | # gql 13 | 14 | Universal and spec-compliant [GraphQL](https://www.graphql.com/) HTTP middleware 15 | for Deno and Bun. Based on [graphql-http](https://github.com/graphql/graphql-http). 16 | 17 | ## Features 18 | 19 | - ✨ Works with `Deno.serve`, `Bun.serve` and [oak](https://github.com/oakserver/oak) 20 | - ⚡ 21 | [GraphQL Playground](https://github.com/graphql/graphql-playground/tree/master/packages/graphql-playground-html) 22 | integration (via `graphiql: true`) 23 | 24 | ## Get started 25 | 26 | The simplest setup with `Deno.serve`: 27 | 28 | ```ts 29 | import { GraphQLHTTP } from 'jsr:@deno-libs/gql@3.0.1/mod.ts' 30 | import { makeExecutableSchema } from 'npm:@graphql-tools/schema@10.0.3' 31 | import { gql } from 'https://deno.land/x/graphql_tag@0.1.2/mod.ts' 32 | 33 | const typeDefs = gql` 34 | type Query { 35 | hello: String 36 | } 37 | ` 38 | 39 | const resolvers = { 40 | Query: { 41 | hello: () => `Hello World!`, 42 | }, 43 | } 44 | 45 | const schema = makeExecutableSchema({ resolvers, typeDefs }) 46 | 47 | Deno.serve({ 48 | port: 3000, 49 | onListen({ hostname, port }) { 50 | console.log(`☁ Started on http://${hostname}:${port}`) 51 | }, 52 | }, async (req) => { 53 | const { pathname } = new URL(req.url) 54 | return pathname === '/graphql' 55 | ? await GraphQLHTTP({ 56 | schema, 57 | graphiql: true, 58 | })(req) 59 | : new Response('Not Found', { status: 404 }) 60 | }) 61 | ``` 62 | 63 | Then run: 64 | 65 | ```sh 66 | $ curl -X POST localhost:3000/graphql -d '{ "query": "{ hello }" }' -H "Content-Type: application/json" 67 | { 68 | "data": { 69 | "hello": "Hello World!" 70 | } 71 | } 72 | ``` 73 | 74 | Or in the GraphQL Playground: 75 | 76 | ![image](https://user-images.githubusercontent.com/35937217/112218821-4133c800-8c35-11eb-984a-5c21fa71c229.png) 77 | 78 | [docs-badge]: https://img.shields.io/github/v/release/deno-libs/gql?label=Docs&logo=deno&style=for-the-badge&color=DD3FAA 79 | [docs]: https://doc.deno.land/https/deno.land/x/gql/mod.ts 80 | [gh-actions-img]: https://img.shields.io/github/actions/workflow/status/deno-libs/gql/main.yml?branch=master&style=for-the-badge&logo=github&label=&color=DD3FAA& 81 | [github-actions]: https://github.com/deno-libs/gql/actions 82 | [cov]: https://coveralls.io/github/deno-libs/gql 83 | [cov-badge]: https://img.shields.io/coveralls/github/deno-libs/gql?style=for-the-badge&color=DD3FAA 84 | [nest-badge]: https://img.shields.io/badge/publushed%20on-nest.land-DD3FAA?style=for-the-badge 85 | [code-quality-img]: https://img.shields.io/codefactor/grade/github/deno-libs/gql?style=for-the-badge&color=DD3FAA 86 | [code-quality]: https://www.codefactor.io/repository/github/deno-libs/gql 87 | -------------------------------------------------------------------------------- /audit_test.ts: -------------------------------------------------------------------------------- 1 | import { serverAudits } from 'npm:graphql-http@1.22.4' 2 | 3 | for ( 4 | const audit of serverAudits({ 5 | url: 'http://localhost:3000/graphql', 6 | }) 7 | ) { 8 | Deno.test(audit.name, { sanitizeResources: false }, async () => { 9 | const result = await audit.fn() 10 | if (result.status === 'error') { 11 | throw result.reason 12 | } 13 | if (result.status === 'warn') { 14 | console.warn(result.reason) 15 | } 16 | if ('body' in result && result.body instanceof ReadableStream) { 17 | await result.body.cancel() 18 | } 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /deno.json: -------------------------------------------------------------------------------- 1 | { 2 | "fmt": { 3 | "options": { 4 | "useTabs": false, 5 | "lineWidth": 80, 6 | "indentWidth": 2, 7 | "singleQuote": true, 8 | "semiColons": false 9 | } 10 | }, 11 | "tasks": { 12 | "test": "deno test --allow-net --coverage=coverage", 13 | "test:audit": "deno test --allow-net audit_test.ts" 14 | }, 15 | "test": { 16 | "exclude": ["audit_test.ts"] 17 | }, 18 | "name": "@deno-libs/gql", 19 | "version": "4.0.0", 20 | "exports": { 21 | ".": "./mod.ts", 22 | "./types": "./types.ts", 23 | "./graphiql/render": "./graphiql/render.ts" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4", 3 | "specifiers": { 4 | "jsr:@deno-libs/superfetch@2.0.1": "2.0.1", 5 | "jsr:@deno-libs/superfetch@3.0.0": "3.0.0", 6 | "jsr:@oak/commons@0.11": "0.11.0", 7 | "jsr:@std/assert@0.223": "0.223.0", 8 | "jsr:@std/assert@0.224": "0.224.0", 9 | "jsr:@std/assert@0.226": "0.226.0", 10 | "jsr:@std/assert@1.0.0": "1.0.0", 11 | "jsr:@std/bytes@0.223": "0.223.0", 12 | "jsr:@std/bytes@0.224": "0.224.0", 13 | "jsr:@std/crypto@0.223": "0.223.0", 14 | "jsr:@std/crypto@0.224": "0.224.0", 15 | "jsr:@std/encoding@0.223": "0.223.0", 16 | "jsr:@std/encoding@1.0.0-rc.2": "1.0.0-rc.2", 17 | "jsr:@std/http@0.223": "0.223.0", 18 | "jsr:@std/http@0.224": "0.224.5", 19 | "jsr:@std/http@0.224.5": "0.224.5", 20 | "jsr:@std/internal@^1.0.1": "1.0.5", 21 | "jsr:@std/io@0.223": "0.223.0", 22 | "jsr:@std/media-types@0.223": "0.223.0", 23 | "jsr:@std/media-types@0.224": "0.224.1", 24 | "jsr:@std/media-types@1.0.1": "1.0.1", 25 | "jsr:@std/path@0.223": "0.223.0", 26 | "jsr:@std/testing@0.225.3": "0.225.3", 27 | "npm:@graphql-tools/schema@10.0.3": "10.0.3_graphql@16.10.0", 28 | "npm:@types/node@*": "22.5.4", 29 | "npm:graphql-http@1.22.4": "1.22.4_graphql@16.10.0", 30 | "npm:graphql@16.10.0": "16.10.0", 31 | "npm:path-to-regexp@6.2.1": "6.2.1", 32 | "npm:xss@1.0.15": "1.0.15" 33 | }, 34 | "jsr": { 35 | "@deno-libs/superfetch@2.0.1": { 36 | "integrity": "16f40d931baf3075e53e9d119a40bf3ddc49a55f8bd25f80a18baa36f29b2ce0", 37 | "dependencies": [ 38 | "jsr:@std/assert@1.0.0", 39 | "jsr:@std/media-types@1.0.1" 40 | ] 41 | }, 42 | "@deno-libs/superfetch@3.0.0": { 43 | "integrity": "8504f17cd6cab62789aaa9cafe9ef07a263a4cafa46f96ee8caae52aa3d821f7", 44 | "dependencies": [ 45 | "jsr:@std/assert@1.0.0", 46 | "jsr:@std/media-types@1.0.1" 47 | ] 48 | }, 49 | "@oak/commons@0.11.0": { 50 | "integrity": "07702bfe5c07cd8144c422022994da1f9fea466b185824f4be63a2b1b1a65125", 51 | "dependencies": [ 52 | "jsr:@std/assert@0.226", 53 | "jsr:@std/bytes@0.224", 54 | "jsr:@std/crypto@0.224", 55 | "jsr:@std/http@0.224", 56 | "jsr:@std/media-types@0.224" 57 | ] 58 | }, 59 | "@std/assert@0.223.0": { 60 | "integrity": "eb8d6d879d76e1cc431205bd346ed4d88dc051c6366365b1af47034b0670be24" 61 | }, 62 | "@std/assert@0.224.0": { 63 | "integrity": "8643233ec7aec38a940a8264a6e3eed9bfa44e7a71cc6b3c8874213ff401967f" 64 | }, 65 | "@std/assert@0.226.0": { 66 | "integrity": "0dfb5f7c7723c18cec118e080fec76ce15b4c31154b15ad2bd74822603ef75b3" 67 | }, 68 | "@std/assert@1.0.0": { 69 | "integrity": "0e4f6d873f7f35e2a1e6194ceee39686c996b9e5d134948e644d35d4c4df2008", 70 | "dependencies": [ 71 | "jsr:@std/internal" 72 | ] 73 | }, 74 | "@std/bytes@0.223.0": { 75 | "integrity": "84b75052cd8680942c397c2631318772b295019098f40aac5c36cead4cba51a8" 76 | }, 77 | "@std/bytes@0.224.0": { 78 | "integrity": "a2250e1d0eb7d1c5a426f21267ab9bdeac2447fa87a3d0d1a467d3f7a6058e49" 79 | }, 80 | "@std/crypto@0.223.0": { 81 | "integrity": "1aa9555ff56b09e197ad988ea200f84bc6781fd4fd83f3a156ee44449af93000", 82 | "dependencies": [ 83 | "jsr:@std/assert@0.223", 84 | "jsr:@std/encoding@0.223" 85 | ] 86 | }, 87 | "@std/crypto@0.224.0": { 88 | "integrity": "154ef3ff08ef535562ef1a718718c5b2c5fc3808f0f9100daad69e829bfcdf2d", 89 | "dependencies": [ 90 | "jsr:@std/assert@0.224" 91 | ] 92 | }, 93 | "@std/encoding@0.223.0": { 94 | "integrity": "2b5615a75e00337ce113f34cf2f9b8c18182c751a8dcc8b1a2c2fc0e117bef00" 95 | }, 96 | "@std/encoding@1.0.0-rc.2": { 97 | "integrity": "160d7674a20ebfbccdf610b3801fee91cf6e42d1c106dd46bbaf46e395cd35ef" 98 | }, 99 | "@std/http@0.223.0": { 100 | "integrity": "15ab8a0c5a7e9d5be017a15b01600f20f66602ceec48b378939fa24fcec522aa", 101 | "dependencies": [ 102 | "jsr:@std/assert@0.223", 103 | "jsr:@std/encoding@0.223" 104 | ] 105 | }, 106 | "@std/http@0.224.5": { 107 | "integrity": "b03b5d1529f6c423badfb82f6640f9f2557b4034cd7c30655ba5bb447ff750a4", 108 | "dependencies": [ 109 | "jsr:@std/encoding@1.0.0-rc.2" 110 | ] 111 | }, 112 | "@std/internal@1.0.5": { 113 | "integrity": "54a546004f769c1ac9e025abd15a76b6671ddc9687e2313b67376125650dc7ba" 114 | }, 115 | "@std/io@0.223.0": { 116 | "integrity": "2d8c3c2ab3a515619b90da2c6ff5ea7b75a94383259ef4d02116b228393f84f1", 117 | "dependencies": [ 118 | "jsr:@std/bytes@0.223" 119 | ] 120 | }, 121 | "@std/media-types@0.223.0": { 122 | "integrity": "84684680c2eb6bc6d9369c6d6f26a49decaf2c7603ff531862dda575d9d6776e" 123 | }, 124 | "@std/media-types@0.224.1": { 125 | "integrity": "9e69a5daed37c5b5c6d3ce4731dc191f80e67f79bed392b0957d1d03b87f11e1" 126 | }, 127 | "@std/media-types@1.0.1": { 128 | "integrity": "f2ddc3497be0bd87ac0c9b9b26bb454f76bdc45e1b9a12146af47fab3ba2828c" 129 | }, 130 | "@std/path@0.223.0": { 131 | "integrity": "593963402d7e6597f5a6e620931661053572c982fc014000459edc1f93cc3989", 132 | "dependencies": [ 133 | "jsr:@std/assert@0.223" 134 | ] 135 | }, 136 | "@std/testing@0.225.3": { 137 | "integrity": "348c24d0479d44ab3dbb4f26170f242e19f24051b45935d4a9e7ca0ab7e37780" 138 | } 139 | }, 140 | "npm": { 141 | "@graphql-tools/merge@9.0.17_graphql@16.10.0": { 142 | "integrity": "sha512-3K4g8KKbIqfdmK0L5+VtZsqwAeElPkvT5ejiH+KEhn2wyKNCi4HYHxpQk8xbu+dSwLlm9Lhet1hylpo/mWCkuQ==", 143 | "dependencies": [ 144 | "@graphql-tools/utils", 145 | "graphql", 146 | "tslib" 147 | ] 148 | }, 149 | "@graphql-tools/schema@10.0.3_graphql@16.10.0": { 150 | "integrity": "sha512-p28Oh9EcOna6i0yLaCFOnkcBDQECVf3SCexT6ktb86QNj9idnkhI+tCxnwZDh58Qvjd2nURdkbevvoZkvxzCog==", 151 | "dependencies": [ 152 | "@graphql-tools/merge", 153 | "@graphql-tools/utils", 154 | "graphql", 155 | "tslib", 156 | "value-or-promise" 157 | ] 158 | }, 159 | "@graphql-tools/utils@10.7.2_graphql@16.10.0": { 160 | "integrity": "sha512-Wn85S+hfkzfVFpXVrQ0hjnePa3p28aB6IdAGCiD1SqBCSMDRzL+OFEtyAyb30nV9Mqflqs9lCqjqlR2puG857Q==", 161 | "dependencies": [ 162 | "@graphql-typed-document-node/core", 163 | "cross-inspect", 164 | "dset", 165 | "graphql", 166 | "tslib" 167 | ] 168 | }, 169 | "@graphql-typed-document-node/core@3.2.0_graphql@16.10.0": { 170 | "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", 171 | "dependencies": [ 172 | "graphql" 173 | ] 174 | }, 175 | "@types/node@22.5.4": { 176 | "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", 177 | "dependencies": [ 178 | "undici-types" 179 | ] 180 | }, 181 | "commander@2.20.3": { 182 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" 183 | }, 184 | "cross-inspect@1.0.1": { 185 | "integrity": "sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==", 186 | "dependencies": [ 187 | "tslib" 188 | ] 189 | }, 190 | "cssfilter@0.0.10": { 191 | "integrity": "sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==" 192 | }, 193 | "dset@3.1.4": { 194 | "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==" 195 | }, 196 | "graphql-http@1.22.4_graphql@16.10.0": { 197 | "integrity": "sha512-OC3ucK988teMf+Ak/O+ZJ0N2ukcgrEurypp8ePyJFWq83VzwRAmHxxr+XxrMpxO/FIwI4a7m/Fzv3tWGJv0wPA==", 198 | "dependencies": [ 199 | "graphql" 200 | ] 201 | }, 202 | "graphql@16.10.0": { 203 | "integrity": "sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==" 204 | }, 205 | "path-to-regexp@6.2.1": { 206 | "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==" 207 | }, 208 | "tslib@2.8.1": { 209 | "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" 210 | }, 211 | "undici-types@6.19.8": { 212 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" 213 | }, 214 | "value-or-promise@1.0.12": { 215 | "integrity": "sha512-Z6Uz+TYwEqE7ZN50gwn+1LCVo9ZVrpxRPOhOLnncYkY1ZzOYtrX8Fwf/rFktZ8R5mJms6EZf5TqNOMeZmnPq9Q==" 216 | }, 217 | "xss@1.0.15": { 218 | "integrity": "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg==", 219 | "dependencies": [ 220 | "commander", 221 | "cssfilter" 222 | ] 223 | } 224 | }, 225 | "remote": { 226 | "https://deno.land/std@0.221.0/testing/_test_suite.ts": "f10a8a6338b60c403f07a76f3f46bdc9f1e1a820c0a1decddeb2949f7a8a0546", 227 | "https://deno.land/std@0.221.0/testing/bdd.ts": "7a8ac58eded80e6fefa7cf7538927e88781cf5f247c04b35261c3213316e2dd0", 228 | "https://deno.land/std@0.97.0/fmt/colors.ts": "db22b314a2ae9430ae7460ce005e0a7130e23ae1c999157e3bb77cf55800f7e4", 229 | "https://deno.land/std@0.97.0/testing/_diff.ts": "961eaf6d9f5b0a8556c9d835bbc6fa74f5addd7d3b02728ba7936ff93364f7a3", 230 | "https://deno.land/std@0.97.0/testing/asserts.ts": "341292d12eebc44be4c3c2ca101ba8b6b5859cef2fa69d50c217f9d0bfbcfd1f", 231 | "https://deno.land/x/expect@v0.4.0/expect.ts": "1d1856758a750f440d0b65d74f19e5d4829bb76d8e576d05546abd8e7b1dfb9e", 232 | "https://deno.land/x/expect@v0.4.0/matchers.ts": "55acf74a3c4a308d079798930f05ab11da2080ec7acd53517193ca90d1296bf7", 233 | "https://deno.land/x/expect@v0.4.0/mock.ts": "562d4b1d735d15b0b8e935f342679096b64fe452f86e96714fe8616c0c884914", 234 | "https://deno.land/x/expect@v0.4.0/mod.ts": "0304d2430e1e96ba669a8495e24ba606dcc3d152e1f81aaa8da898cea24e36c2", 235 | "https://deno.land/x/graphql_tag@0.1.2/deps.ts": "5696461c8bb42db7c83486db452e125f7cfdc62a2c628bb470a4447d934b90b3", 236 | "https://deno.land/x/graphql_tag@0.1.2/mod.ts": "57fd56de5f7cbc66e23ce896cc8e99521d286e89969d83e09d960642b0a9d652", 237 | "https://deno.land/x/oak@v16.1.0/application.ts": "c6361a3c3fb3607c5dfe1800b156f07b612979dc0498c746aeca54bb9f643a92", 238 | "https://deno.land/x/oak@v16.1.0/body.ts": "0f8a6843720ea6cebba2132a5e4dda5a97bbd49e7fb64e96ade0b7c8009bba2d", 239 | "https://deno.land/x/oak@v16.1.0/context.ts": "a93a5b41dde2ceb52232ae31b3496be76e8a42ea421a73d7cffe9e640adb8dfd", 240 | "https://deno.land/x/oak@v16.1.0/deps.ts": "f960515f71091adecc3df5e2295b3acfc4f1c003e08be29fceec1f7616053433", 241 | "https://deno.land/x/oak@v16.1.0/http_server_bun.ts": "cb3a66c735cd0533c4c3776b37ae627ab42344c82f91dff63e3030d9656fd3a0", 242 | "https://deno.land/x/oak@v16.1.0/http_server_native.ts": "3bea00ebb9638203d2449fbf9a14a6b87f119bd45012f13282ececdf7b4c4242", 243 | "https://deno.land/x/oak@v16.1.0/http_server_native_request.ts": "a4da6f4939736e6323720db2d4b0d19ff2e83f02e52ab1eea9ae80f2c047fa56", 244 | "https://deno.land/x/oak@v16.1.0/http_server_node.ts": "9bb5291c15305b297fd634aa4c6b1d5054368f4b7a171d7c7c302c73eb2489ed", 245 | "https://deno.land/x/oak@v16.1.0/middleware.ts": "4170180fe5009d2581a0bdc995e5953b90ccb5b1c3767f3eae8a4fe238b8bd81", 246 | "https://deno.land/x/oak@v16.1.0/middleware/etag.ts": "310ed4ed01f2af5384ab78617c82a2bdd7f84d66539172e45ee16452846f0754", 247 | "https://deno.land/x/oak@v16.1.0/middleware/proxy.ts": "a0b4964509d4320735ffbe52ae2629c78e302dd9b934fcf2ddf189d491469892", 248 | "https://deno.land/x/oak@v16.1.0/middleware/serve.ts": "efceebd70afb73bcabe0a6a8981f3d8474a2f2f30e85b46761aee49e81bd9d6a", 249 | "https://deno.land/x/oak@v16.1.0/mod.ts": "38e53e01e609583e843f3e2b2677de9872d23d68939ce0de85b402e7a8db01a7", 250 | "https://deno.land/x/oak@v16.1.0/node_shims.ts": "4db1569b2b79b73f37c4d947f4aaa50a93e266d48fe67601c8a31af17a28884d", 251 | "https://deno.land/x/oak@v16.1.0/request.ts": "1e7f2c338cd6889b616bbdc9e2062eac27acbffde05c685adfb1c60ecc80682a", 252 | "https://deno.land/x/oak@v16.1.0/response.ts": "bc47174d3d797ffc802f40fba006c16de8390e776a36f6f4a4d4f58b278bf36f", 253 | "https://deno.land/x/oak@v16.1.0/router.ts": "882f36a576e280b0d617cc174feca320f02deed77bdea8264444ba8b55cc0422", 254 | "https://deno.land/x/oak@v16.1.0/send.ts": "c05e5985b356b568ae4954a40373f93451a3f8cc9ae8706c8b8879e5e0c8c86b", 255 | "https://deno.land/x/oak@v16.1.0/testing.ts": "c879789ac721144c3dcad0a06f5afbe2cd94fb20afc32302992942a49a7def90", 256 | "https://deno.land/x/oak@v16.1.0/types.ts": "cd4ccd3e182d0cba2117cd27f560267970470ab9c0ff6556cadd73f605193be7", 257 | "https://deno.land/x/oak@v16.1.0/utils/clone_state.ts": "cf8989ddd56816b36ada253ae0acdbd46cdf3d68dbe674d2b66c46640fab3500", 258 | "https://deno.land/x/oak@v16.1.0/utils/consts.ts": "137c4f73479f5e98a13153b9305f06f0d85df3cf2aacad2c9f82d4c1f3a2f105", 259 | "https://deno.land/x/oak@v16.1.0/utils/create_promise_with_resolvers.ts": "de99e9a998162b929a011f8873eaf0895cf4742492b3ce6f6866d39217342523", 260 | "https://deno.land/x/oak@v16.1.0/utils/decode_component.ts": "d3e2c40ecdd2fdb79761c6e9ae224cf01a4643f7c5f4c1e0b69698d43025261b", 261 | "https://deno.land/x/oak@v16.1.0/utils/encode_url.ts": "c0ed6b318eb9523adeebba32eb9acd059c0f94d3511b2b9e3b024722d1b3dfb8", 262 | "https://deno.land/x/oak@v16.1.0/utils/resolve_path.ts": "aa39d54a003b38fee55f340a0cba3f93a7af85b8ddd5fbfb049a98fc0109b36d", 263 | "https://deno.land/x/oak@v16.1.0/utils/streams.ts": "6d55543fdeddc3c9c6f512de227b9b33ff4b0ec5e320edc109f0cbf9bef8963f", 264 | "https://deno.land/x/oak@v16.1.0/utils/type_guards.ts": "a1b42aa4c431c4c07d3f8a45a2142020f86d5a3e1e319af94fdf2f4c6c72465f", 265 | "https://esm.sh/graphql@16.6.0/denonext/language/parser.mjs": "21defd87cff28344044b53da1b11d1b027ea075148abbdc2c3457321d45b3194", 266 | "https://esm.sh/graphql@16.6.0/language/parser#=": "18b458e1ae94961e5aae7f758aa56c5b3ea3a98fc25caf17ef5a53b7ece8f8d7" 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /deps.ts: -------------------------------------------------------------------------------- 1 | export { 2 | type ExecutionResult, 3 | graphql, 4 | type GraphQLArgs, 5 | type GraphQLSchema, 6 | } from 'npm:graphql@16.10.0' 7 | export { 8 | createHandler, 9 | type HandlerOptions, 10 | type OperationContext, 11 | parseRequestParams as rawParseRequestParams, 12 | type Request as RawRequest, 13 | type RequestParams, 14 | } from 'npm:graphql-http@1.22.4' 15 | export { STATUS_TEXT, type StatusCode } from 'jsr:@std/http@0.224.5/status' 16 | export { accepts } from 'jsr:@std/http@0.224.5/negotiation' 17 | -------------------------------------------------------------------------------- /egg.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://x.nest.land/eggs@0.3.8/src/schema.json", 3 | "name": "gql", 4 | "entry": "./mod.ts", 5 | "description": "Universal GraphQL HTTP middleware for Deno", 6 | "homepage": "https://github.com/deno-libs/gql", 7 | "version": "1.2.1", 8 | "files": [ 9 | "./*.ts", 10 | "./graphiql/*.ts", 11 | "README.md" 12 | ], 13 | "ignore": [ 14 | "examples", 15 | ".github", 16 | "mod_test.ts" 17 | ], 18 | "checkFormat": false, 19 | "checkTests": true, 20 | "checkInstallation": true, 21 | "check": true, 22 | "unlisted": false, 23 | "checkAll": true, 24 | "repository": "https://github.com/deno-libs/gql", 25 | "releaseType": "patch" 26 | } 27 | -------------------------------------------------------------------------------- /examples/denoflare/.denoflare: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/skymethod/denoflare/v0.5.11/common/config.schema.json", 3 | "scripts": { 4 | "graphql": { 5 | "path": "mod.ts", 6 | "localPort": 3030 7 | } 8 | } 9 | 10 | } -------------------------------------------------------------------------------- /examples/denoflare/README.md: -------------------------------------------------------------------------------- 1 | # [Denoflare](https://denoflare.dev) example 2 | 3 | You can use gql with Cloudflare workers using Denoflare. 4 | 5 | ## Run 6 | 7 | ``` 8 | denoflare serve graphql 9 | ``` 10 | 11 | ## Publish 12 | 13 | ``` 14 | denoflare push graphql 15 | ``` 16 | -------------------------------------------------------------------------------- /examples/denoflare/mod.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLHTTP } from '../../mod.ts' 2 | import { makeExecutableSchema } from 'npm:@graphql-tools/schema@10.0.3' 3 | import { gql } from 'https://deno.land/x/graphql_tag@0.1.2/mod.ts' 4 | 5 | const typeDefs = gql` 6 | type Query { 7 | hello: String 8 | } 9 | ` 10 | 11 | const resolvers = { 12 | Query: { 13 | hello: () => `Hello World!`, 14 | }, 15 | } 16 | 17 | const schema = makeExecutableSchema({ resolvers, typeDefs }) 18 | 19 | export default { 20 | async fetch(req: Request) { 21 | const { pathname } = new URL(req.url) 22 | 23 | return pathname === '/graphql' 24 | ? await GraphQLHTTP({ 25 | schema, 26 | graphiql: true, 27 | })(req) 28 | : new Response('Not Found', { status: 404 }) 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /examples/oak.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Application, 3 | type Middleware, 4 | type Request as OakRequest, 5 | Router, 6 | } from 'https://deno.land/x/oak@v16.1.0/mod.ts' 7 | import { GraphQLHTTP } from '../mod.ts' 8 | import { makeExecutableSchema } from 'npm:@graphql-tools/schema@10.0.3' 9 | import { gql } from 'https://deno.land/x/graphql_tag@0.1.2/mod.ts' 10 | 11 | const typeDefs = gql` 12 | type Query { 13 | hello: String 14 | } 15 | ` 16 | 17 | const resolvers = { 18 | Query: { 19 | hello: (_root: undefined, _args: unknown, ctx: { request: OakRequest }) => { 20 | return `Hello from ${ctx.request.url}` 21 | }, 22 | }, 23 | } 24 | 25 | const schema = makeExecutableSchema({ typeDefs, resolvers }) 26 | 27 | const handleGraphQL: Middleware = async (ctx) => { 28 | // cast Oak request into a normal Request 29 | const req = new Request(ctx.request.url.toString(), { 30 | body: await ctx.request.body.blob(), 31 | headers: ctx.request.headers, 32 | method: ctx.request.method, 33 | }) 34 | 35 | const res = await GraphQLHTTP({ 36 | schema, 37 | graphiql: true, 38 | context: () => ({ request: ctx.request }), 39 | })(req) 40 | 41 | for (const [k, v] of res.headers.entries()) ctx.response.headers.append(k, v) 42 | 43 | ctx.response.status = res.status 44 | ctx.response.body = res.body 45 | } 46 | 47 | const graphqlRouter = new Router().all('/graphql', handleGraphQL) 48 | 49 | const app = new Application().use( 50 | graphqlRouter.routes(), 51 | graphqlRouter.allowedMethods(), 52 | ) 53 | 54 | app.addEventListener('listen', ({ secure, hostname, port }) => { 55 | if (hostname === '0.0.0.0') hostname = 'localhost' 56 | 57 | const protocol = secure ? 'https' : 'http' 58 | const url = `${protocol}://${hostname ?? 'localhost'}:${port}` 59 | 60 | console.log('☁ Started on ' + url) 61 | }) 62 | 63 | await app.listen({ port: 3000 }) 64 | -------------------------------------------------------------------------------- /examples/req-ctx.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLHTTP } from '../mod.ts' 2 | import { makeExecutableSchema } from 'npm:@graphql-tools/schema@10.0.3' 3 | import { gql } from 'https://deno.land/x/graphql_tag@0.1.2/mod.ts' 4 | import type { Request as GQLRequest } from 'npm:graphql-http@1.22.4' 5 | 6 | const typeDefs = gql` 7 | type Query { 8 | hello: String 9 | } 10 | ` 11 | 12 | type ReqContext = { 13 | request: Request 14 | isRequestContext: boolean 15 | } 16 | 17 | type Context = { 18 | request: Request 19 | originalReq: GQLRequest 20 | } 21 | 22 | const resolvers = { 23 | Query: { 24 | hello: (_root: unknown, _args: unknown, ctx: Context) => { 25 | return `Hello from request context: ${ctx.originalReq.context.isRequestContext}` 26 | }, 27 | }, 28 | } 29 | 30 | const schema = makeExecutableSchema({ resolvers, typeDefs }) 31 | 32 | Deno.serve({ 33 | port: 3000, 34 | onListen({ hostname, port }) { 35 | console.log(`☁ Started on http://${hostname}:${port}`) 36 | }, 37 | }, async (req) => { 38 | const { pathname } = new URL(req.url) 39 | return pathname === '/graphql' 40 | ? await GraphQLHTTP({ 41 | schema, 42 | graphiql: true, 43 | context: (request) => ({ request: req, originalReq: request }), 44 | }, () => ({ request: req, isRequestContext: true }))(req) 45 | : new Response('Not Found', { status: 404 }) 46 | }) 47 | -------------------------------------------------------------------------------- /examples/vanilla.ts: -------------------------------------------------------------------------------- 1 | import { GraphQLHTTP } from '../mod.ts' 2 | import { makeExecutableSchema } from 'npm:@graphql-tools/schema@10.0.3' 3 | import { gql } from 'https://deno.land/x/graphql_tag@0.1.2/mod.ts' 4 | 5 | const typeDefs = gql` 6 | type Query { 7 | hello: String 8 | } 9 | ` 10 | 11 | const resolvers = { 12 | Query: { 13 | hello: () => `Hello World!`, 14 | }, 15 | } 16 | 17 | const schema = makeExecutableSchema({ resolvers, typeDefs }) 18 | 19 | Deno.serve({ 20 | port: 3000, 21 | onListen({ hostname, port }) { 22 | console.log(`☁ Started on http://${hostname}:${port}`) 23 | }, 24 | }, async (req) => { 25 | const { pathname } = new URL(req.url) 26 | return pathname === '/graphql' 27 | ? await GraphQLHTTP({ 28 | schema, 29 | graphiql: true, 30 | })(req) 31 | : new Response('Not Found', { status: 404 }) 32 | }) 33 | -------------------------------------------------------------------------------- /graphiql/README.md: -------------------------------------------------------------------------------- 1 | # gql/graphiql 2 | 3 | [![][docs-badge]][docs] 4 | 5 | Tweaked version of 6 | [graphql-playground-html](https://github.com/graphql/graphql-playground/tree/main/packages/graphql-playground-html) 7 | without Electron and React environments. 8 | 9 | ## Get Started 10 | 11 | ```ts 12 | import { renderPlaygroundPage } from 'https://deno.land/x/gql@2.0.0/graphiql/render.ts' 13 | 14 | const playground = renderPlaygroundPage({ 15 | endpoint: '/graphql', 16 | }) 17 | 18 | return new Response(playground, { 19 | headers: new Headers({ 20 | 'Content-Type': 'text/html', 21 | }), 22 | }) 23 | ``` 24 | 25 | [docs-badge]: https://img.shields.io/github/v/release/deno-libs/gql?label=Docs&logo=deno&style=for-the-badge&color=DD3FAA 26 | [docs]: https://doc.deno.land/https/deno.land/x/gql/graphiql/render.ts 27 | -------------------------------------------------------------------------------- /graphiql/markup.ts: -------------------------------------------------------------------------------- 1 | export const getLoadingMarkup = () => ({ 2 | script: ` 3 | const loadingWrapper = document.getElementById('loading-wrapper'); 4 | if (loadingWrapper) { 5 | loadingWrapper.classList.add('fadeOut'); 6 | } 7 | `, 8 | container: ` 9 | 430 |
431 | 464 |
Loading 465 | GraphQL Playground 466 |
467 |
468 | `, 469 | }) 470 | -------------------------------------------------------------------------------- /graphiql/render.ts: -------------------------------------------------------------------------------- 1 | import { filterXSS } from 'npm:xss@1.0.15' 2 | import { getLoadingMarkup } from './markup.ts' 3 | 4 | export interface MiddlewareOptions { 5 | endpoint?: string 6 | subscriptionEndpoint?: string 7 | workspaceName?: string 8 | config?: unknown 9 | settings?: ISettings 10 | schema?: IntrospectionResult 11 | tabs?: Tab[] 12 | codeTheme?: EditorColours 13 | } 14 | 15 | export type CursorShape = 'line' | 'block' | 'underline' 16 | export type Theme = 'dark' | 'light' 17 | 18 | export interface ISettings { 19 | 'general.betaUpdates': boolean 20 | 'editor.cursorShape': CursorShape 21 | 'editor.theme': Theme 22 | 'editor.reuseHeaders': boolean 23 | 'tracing.hideTracingResponse': boolean 24 | 'tracing.tracingSupported': boolean 25 | 'editor.fontSize': number 26 | 'editor.fontFamily': string 27 | 'request.credentials': string 28 | 'request.globalHeaders': { [key: string]: string } 29 | 'schema.polling.enable': boolean 30 | 'schema.polling.endpointFilter': string 31 | 'schema.polling.interval': number 32 | } 33 | 34 | export interface EditorColours { 35 | property: string 36 | comment: string 37 | punctuation: string 38 | keyword: string 39 | def: string 40 | qualifier: string 41 | attribute: string 42 | number: string 43 | string: string 44 | builtin: string 45 | string2: string 46 | variable: string 47 | meta: string 48 | atom: string 49 | ws: string 50 | selection: string 51 | cursorColor: string 52 | editorBackground: string 53 | resultBackground: string 54 | leftDrawerBackground: string 55 | rightDrawerBackground: string 56 | } 57 | 58 | export interface IntrospectionResult { 59 | // deno-lint-ignore no-explicit-any 60 | __schema: any 61 | } 62 | 63 | export interface RenderPageOptions extends MiddlewareOptions { 64 | version?: string 65 | cdnUrl?: string 66 | title?: string 67 | faviconUrl?: string | null 68 | } 69 | 70 | export interface Tab { 71 | endpoint: string 72 | query: string 73 | name?: string 74 | variables?: string 75 | responses?: string[] 76 | headers?: { [key: string]: string } 77 | } 78 | 79 | const loading = getLoadingMarkup() 80 | 81 | const CONFIG_ID = 'playground-config' 82 | 83 | const filter = (val: string) => { 84 | return filterXSS(val, { 85 | stripIgnoreTag: true, 86 | stripIgnoreTagBody: ['script'], 87 | }) 88 | } 89 | 90 | const getCdnMarkup = ({ 91 | version, 92 | cdnUrl = '//cdn.jsdelivr.net/npm', 93 | faviconUrl, 94 | }: { 95 | faviconUrl?: string | null 96 | version?: string 97 | cdnUrl?: string 98 | }) => { 99 | const buildCDNUrl = (packageName: string, suffix: string) => 100 | filter( 101 | `${cdnUrl}/${packageName}${version ? `@${version}` : ''}/${suffix}` || '', 102 | ) 103 | return ` 104 | 110 | ${ 111 | typeof faviconUrl === 'string' 112 | ? `` 113 | : '' 114 | } 115 | ${ 116 | faviconUrl === undefined 117 | ? `` 120 | : '' 121 | } 122 | 127 | ` 128 | } 129 | 130 | const renderConfig = (config: unknown) => { 131 | return filterXSS(`
${JSON.stringify(config)}
`, { 132 | whiteList: { div: ['id'] }, 133 | }) 134 | } 135 | 136 | export function renderPlaygroundPage(options: RenderPageOptions): string { 137 | const extendedOptions: 138 | & Partial<{ 139 | canSaveConfig: boolean 140 | configString: string 141 | }> 142 | & RenderPageOptions = { 143 | ...options, 144 | canSaveConfig: false, 145 | } 146 | 147 | if (options.config) { 148 | extendedOptions.configString = JSON.stringify(options.config, null, 2) 149 | } 150 | if (!extendedOptions.endpoint && !extendedOptions.configString) { 151 | console.warn( 152 | `WARNING: You didn't provide an endpoint and don't have a config. Make sure you have at least one of them.`, 153 | ) 154 | } else if (extendedOptions.endpoint) { 155 | extendedOptions.endpoint = filter(extendedOptions.endpoint || '') 156 | } 157 | 158 | return ` 159 | 160 | 161 | 162 | 163 | 164 | 165 | ${extendedOptions.title || 'GraphQL Playground'} 166 | ${getCdnMarkup(extendedOptions)} 167 | 168 | 169 | 218 | ${loading.container} 219 | ${renderConfig(extendedOptions)} 220 |
221 | 242 | 243 | 244 | ` 245 | } 246 | -------------------------------------------------------------------------------- /graphiql/render_test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it } from 'https://deno.land/std@0.221.0/testing/bdd.ts' 2 | import { expect } from 'https://deno.land/x/expect@v0.4.0/mod.ts' 3 | import { renderPlaygroundPage } from './render.ts' 4 | 5 | describe('renderPlaygroundPage', () => { 6 | it('supports custom CDNs', () => { 7 | const html = renderPlaygroundPage({ cdnUrl: 'https://unpkg.com' }) 8 | 9 | expect(html).toContain( 10 | `https://unpkg.com/graphql-playground-react/build/static/css/index.css`, 11 | ) 12 | expect(html).toContain( 13 | `https://unpkg.com/graphql-playground-react/build/favicon.png`, 14 | ) 15 | expect(html).toContain( 16 | `https://unpkg.com/graphql-playground-react/build/static/js/middleware.js`, 17 | ) 18 | }) 19 | it('supports custom versions of graphql-playground-react', () => { 20 | const html = renderPlaygroundPage({ version: '1.7.27' }) 21 | 22 | expect(html).toContain( 23 | `jsdelivr.net/npm/graphql-playground-react@1.7.27/build/favicon.png`, 24 | ) 25 | expect(html).toContain( 26 | `jsdelivr.net/npm/graphql-playground-react@1.7.27/build/static/js/middleware.js`, 27 | ) 28 | }) 29 | it('supports custom GraphQL endpoint', () => { 30 | const html = renderPlaygroundPage({ endpoint: '/grafql' }) 31 | 32 | expect(html).toContain(`/grafql`) 33 | }) 34 | it('supports custom favicon', () => { 35 | const html = renderPlaygroundPage({ 36 | faviconUrl: 'https://tinyhttp.v1rtl.site/favicon.png', 37 | }) 38 | 39 | expect(html).toContain( 40 | ``, 41 | ) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deno-libs/gql/637a862558e60cfe4b48afa3beb4ecee28dd1d6a/logo.png -------------------------------------------------------------------------------- /mod.ts: -------------------------------------------------------------------------------- 1 | import { 2 | accepts, 3 | createHandler, 4 | type OperationContext, 5 | type RawRequest, 6 | STATUS_TEXT, 7 | type StatusCode, 8 | } from './deps.ts' 9 | import type { GQLOptions } from './types.ts' 10 | 11 | function toRequest( 12 | req: Pick, 13 | context: Ctx, 14 | ): RawRequest { 15 | return { 16 | method: req.method, 17 | url: req.url, 18 | headers: req.headers, 19 | body: () => req.text(), 20 | raw: req as Req, 21 | context, 22 | } 23 | } 24 | 25 | export function GraphQLHTTP< 26 | Req = Request, 27 | Context extends OperationContext = { request: Req }, 28 | ReqCtx extends { request: Req } = { request: Req }, 29 | >( 30 | { 31 | headers = {}, 32 | graphiql, 33 | playgroundOptions = {}, 34 | ...options 35 | }: GQLOptions, 36 | reqCtx?: (req: Req) => ReqCtx, 37 | ): (req: Request) => Promise { 38 | const handler = createHandler(options) 39 | 40 | return async function handleRequest(req: Request): Promise { 41 | try { 42 | if ( 43 | req.method === 'GET' && graphiql && accepts(req)[0] === 'text/html' 44 | ) { 45 | const urlQuery = req.url.substring(req.url.indexOf('?')) 46 | const queryParams = new URLSearchParams(urlQuery) 47 | 48 | if (!queryParams.has('raw')) { 49 | const { renderPlaygroundPage } = await import('./graphiql/render.ts') 50 | const playground = renderPlaygroundPage({ 51 | ...playgroundOptions, 52 | endpoint: '/graphql', 53 | }) 54 | 55 | return new Response(playground, { 56 | headers: new Headers({ 57 | 'Content-Type': 'text/html', 58 | ...headers, 59 | }), 60 | }) 61 | } 62 | } 63 | const [body, init] = await handler( 64 | toRequest( 65 | req, 66 | reqCtx ? reqCtx(req as Req) : { request: req } as ReqCtx, 67 | ), 68 | ) 69 | 70 | return new Response( 71 | body || STATUS_TEXT[init.status as StatusCode], 72 | { 73 | ...init, 74 | headers: new Headers({ ...init.headers, ...headers }), 75 | }, 76 | ) 77 | } catch (e) { 78 | console.error( 79 | 'Internal error occurred during request handling. ' + 80 | 'Please check your implementation.', 81 | e, 82 | ) 83 | return new Response(null, { status: 500 }) 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /mod_test.ts: -------------------------------------------------------------------------------- 1 | import { makeFetch } from 'jsr:@deno-libs/superfetch@3.0.0' 2 | import { describe, it } from 'jsr:@std/testing@0.225.3/bdd' 3 | import { buildSchema } from 'npm:graphql@16.10.0' 4 | import { GraphQLHTTP } from './mod.ts' 5 | 6 | const schema = buildSchema(` 7 | type Query { 8 | hello: String 9 | } 10 | `) 11 | 12 | const rootValue = { 13 | hello: () => 'Hello World!', 14 | } 15 | 16 | const app = GraphQLHTTP({ schema, rootValue }) 17 | 18 | describe('GraphQLHTTP({ schema, rootValue })', () => { 19 | it('should send 400 on malformed request query', async () => { 20 | const fetch = makeFetch(app) 21 | 22 | const res = await fetch('/', { 23 | headers: { 'Content-Type': 'application/json' }, 24 | }) 25 | res.expectBody({ 26 | 'errors': [{ 'message': 'Missing query' }], 27 | }) 28 | res.expectStatus(400) 29 | }) 30 | it('should send unsupported media type on empty request body', async () => { 31 | const fetch = makeFetch(app) 32 | 33 | const res = await fetch('/', { 34 | method: 'POST', 35 | }) 36 | res.expectStatus(415) 37 | }) 38 | 39 | it('should send resolved POST GraphQL query', async () => { 40 | const fetch = makeFetch(app) 41 | 42 | const res = await fetch('/', { 43 | method: 'POST', 44 | headers: { 'Content-Type': 'application/json' }, 45 | body: JSON.stringify({ 46 | query: '\n{\n hello\n}', 47 | }), 48 | }) 49 | res.expectBody({ data: { hello: 'Hello World!' } }) 50 | res.expectStatus(200) 51 | }) 52 | 53 | it('should send resolved GET GraphQL query', async () => { 54 | const fetch = makeFetch(app) 55 | 56 | const res = await fetch('/?query={hello}') 57 | res.expectBody({ data: { hello: 'Hello World!' } }) 58 | res.expectStatus(200) 59 | }) 60 | 61 | it('should send resolved GET GraphQL query when Accept is application/json', async () => { 62 | const fetch = makeFetch(app) 63 | 64 | const res = await fetch('/?query={hello}', { 65 | headers: { Accept: 'application/json' }, 66 | }) 67 | res.expectBody({ data: { hello: 'Hello World!' } }) 68 | res.expectStatus(200) 69 | res.expectHeader('Content-Type', 'application/json; charset=utf-8') 70 | }) 71 | 72 | it('should send resolved GET GraphQL query when Accept is */*', async () => { 73 | const fetch = makeFetch(app) 74 | 75 | const res = await fetch('/?query={hello}', { 76 | headers: { Accept: '*/*' }, 77 | }) 78 | res.expectBody({ data: { hello: 'Hello World!' } }) 79 | res.expectStatus(200) 80 | res.expectHeader('Content-Type', 'application/json; charset=utf-8') 81 | }) 82 | 83 | it('should send 406 not acceptable when Accept is other (text/html)', async () => { 84 | const fetch = makeFetch(app) 85 | 86 | const res = await fetch('/?query={hello}', { 87 | headers: { Accept: 'text/html' }, 88 | }) 89 | res.expectStatus(406) 90 | res.expectBody('Not Acceptable') 91 | }) 92 | 93 | it('should send 406 not acceptable when Accept is other (text/css)', async () => { 94 | const fetch = makeFetch(app) 95 | 96 | const res = await fetch('/?query={hello}', { 97 | headers: { Accept: 'text/css' }, 98 | }) 99 | res.expectStatus(406) 100 | res.expectBody('Not Acceptable') 101 | }) 102 | 103 | describe('graphiql', () => { 104 | it('should allow query GET requests when set to false', async () => { 105 | const app = GraphQLHTTP({ graphiql: false, schema, rootValue }) 106 | 107 | const fetch = makeFetch(app) 108 | 109 | const res = await fetch('/?query={hello}') 110 | res.expectBody({ data: { hello: 'Hello World!' } }) 111 | res.expectStatus(200) 112 | }) 113 | 114 | it('should allow query GET requests when set to true', async () => { 115 | const app = GraphQLHTTP({ graphiql: true, schema, rootValue }) 116 | 117 | const fetch = makeFetch(app) 118 | 119 | const res = await fetch('/?query={hello}') 120 | res.expectBody({ data: { hello: 'Hello World!' } }) 121 | res.expectStatus(200) 122 | }) 123 | 124 | it('should send 406 when Accept is only text/html when set to false', async () => { 125 | const app = GraphQLHTTP({ graphiql: false, schema, rootValue }) 126 | 127 | const fetch = makeFetch(app) 128 | 129 | const res = await fetch('/', { 130 | headers: { Accept: 'text/html' }, 131 | }) 132 | res.expectStatus(406) 133 | res.expectBody('Not Acceptable') 134 | }) 135 | 136 | it('should render a playground when Accept does include text/html when set to true', async () => { 137 | const app = GraphQLHTTP({ graphiql: true, schema, rootValue }) 138 | 139 | const fetch = makeFetch(app) 140 | 141 | const res = await fetch('/?query={hello}', { 142 | headers: { Accept: 'text/html;*/*' }, 143 | }) 144 | res.expectStatus(200) 145 | res.expectHeader('Content-Type', 'text/html') 146 | }) 147 | 148 | it('should render a playground if graphiql is set to true', async () => { 149 | const app = GraphQLHTTP({ graphiql: true, schema, rootValue }) 150 | 151 | const fetch = makeFetch(app) 152 | 153 | const res = await fetch('/', { 154 | headers: { Accept: 'text/html' }, 155 | }) 156 | res.expectStatus(200) 157 | res.expectHeader('Content-Type', 'text/html') 158 | }) 159 | 160 | describe('playgroundOptions', () => { 161 | it('supports custom favicon', async () => { 162 | const app = GraphQLHTTP({ 163 | graphiql: true, 164 | schema, 165 | rootValue, 166 | playgroundOptions: { 167 | faviconUrl: 'https://github.com/favicon.ico', 168 | }, 169 | }) 170 | 171 | const fetch = makeFetch(app) 172 | 173 | const res = await fetch('/', { 174 | headers: { Accept: 'text/html' }, 175 | }) 176 | res.expectStatus(200) 177 | res.expectHeader('Content-Type', 'text/html') 178 | res.expectBody( 179 | new RegExp( 180 | '', 181 | ), 182 | ) 183 | }) 184 | 185 | it('supports custom title', async () => { 186 | const app = GraphQLHTTP({ 187 | graphiql: true, 188 | schema, 189 | rootValue, 190 | playgroundOptions: { 191 | title: 'Hello gql!', 192 | }, 193 | }) 194 | 195 | const fetch = makeFetch(app) 196 | 197 | const res = await fetch('/', { 198 | headers: { Accept: 'text/html' }, 199 | }) 200 | res.expectStatus(200) 201 | res.expectHeader('Content-Type', 'text/html') 202 | res.expectBody(new RegExp('Hello gql!')) 203 | }) 204 | 205 | it('adds React CDN links if env is React', async () => { 206 | const app = GraphQLHTTP({ 207 | graphiql: true, 208 | schema, 209 | rootValue, 210 | playgroundOptions: { 211 | cdnUrl: 'https://unpkg.com', 212 | }, 213 | }) 214 | 215 | const fetch = makeFetch(app) 216 | 217 | const res = await fetch('/', { 218 | headers: { Accept: 'text/html' }, 219 | }) 220 | res.expectStatus(200) 221 | res.expectHeader('Content-Type', 'text/html') 222 | res.expectBody(new RegExp('unpkg.com')) 223 | }) 224 | }) 225 | }) 226 | 227 | describe('headers', () => { 228 | it('should pass custom headers to response', async () => { 229 | const app = GraphQLHTTP({ schema, rootValue, headers: { Key: 'Value' } }) 230 | 231 | const fetch = makeFetch(app) 232 | 233 | const res = await fetch('/', { 234 | method: 'POST', 235 | headers: { 'Content-Type': 'application/json' }, 236 | body: JSON.stringify({ 237 | query: '\n{\n hello\n}', 238 | variables: {}, 239 | operationName: null, 240 | }), 241 | }) 242 | res.expectBody({ data: { hello: 'Hello World!' } }) 243 | res.expectStatus(200) 244 | res.expectHeader('Key', 'Value') 245 | }) 246 | 247 | it('does not error with empty header object', async () => { 248 | const app = GraphQLHTTP({ schema, rootValue, headers: {} }) 249 | 250 | const fetch = makeFetch(app) 251 | 252 | const res = await fetch('/', { 253 | method: 'POST', 254 | headers: { 'Content-Type': 'application/json' }, 255 | body: JSON.stringify({ 256 | query: '\n{\n hello\n}', 257 | variables: {}, 258 | operationName: null, 259 | }), 260 | }) 261 | res.expectBody({ data: { hello: 'Hello World!' } }) 262 | res.expectStatus(200) 263 | }) 264 | }) 265 | }) 266 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | import type { HandlerOptions, OperationContext } from './deps.ts' 2 | import type { RenderPageOptions } from './graphiql/render.ts' 3 | 4 | /** 5 | * gql options 6 | */ 7 | export interface GQLOptions< 8 | Req = Request, 9 | ReqCtx = unknown, 10 | Context extends OperationContext = OperationContext, 11 | > extends HandlerOptions { 12 | /** 13 | * GraphQL playground 14 | */ 15 | graphiql?: boolean 16 | /** 17 | * Custom headers for responses 18 | */ 19 | headers?: HeadersInit 20 | /** 21 | * Custom options for GraphQL Playground 22 | */ 23 | playgroundOptions?: Omit 24 | } 25 | 26 | interface Params { 27 | variables?: Record 28 | operationName?: string 29 | } 30 | 31 | interface QueryParams extends Params { 32 | query: string 33 | mutation?: never 34 | } 35 | 36 | interface MutationParams extends Params { 37 | mutation: string 38 | query?: never 39 | } 40 | 41 | export type GraphQLParams = QueryParams | MutationParams 42 | --------------------------------------------------------------------------------