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