Add 'packages/graphql-server/' from commit 'c7e949b0f93954d44c3c7f15564dd9e3ce8ad709'
git-subtree-dir: packages/graphql-server git-subtree-mainline:pull/29/headfc30bf65cb
git-subtree-split:c7e949b0f9
commit
3f6f302864
|
@ -0,0 +1 @@
|
|||
dist
|
|
@ -0,0 +1,60 @@
|
|||
const { defineConfig } = require('eslint-define-config')
|
||||
|
||||
module.exports = defineConfig({
|
||||
root: true,
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:node/recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'prettier',
|
||||
],
|
||||
parser: '@typescript-eslint/parser',
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
ecmaVersion: 2021,
|
||||
},
|
||||
plugins: ['@typescript-eslint', 'import'],
|
||||
globals: {
|
||||
fetch: false,
|
||||
Response: false,
|
||||
Request: false,
|
||||
addEventListener: false,
|
||||
},
|
||||
rules: {
|
||||
quotes: ['error', 'single'],
|
||||
semi: ['error', 'never'],
|
||||
'no-debugger': ['error'],
|
||||
'no-empty': ['warn', { allowEmptyCatch: true }],
|
||||
'no-process-exit': 'off',
|
||||
'no-useless-escape': 'off',
|
||||
'prefer-const': [
|
||||
'warn',
|
||||
{
|
||||
destructuring: 'all',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/ban-types': [
|
||||
'error',
|
||||
{
|
||||
types: {
|
||||
Function: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
'sort-imports': 0,
|
||||
'import/order': [2, { alphabetize: { order: 'asc' } }],
|
||||
|
||||
'node/no-missing-import': 'off',
|
||||
'node/no-missing-require': 'off',
|
||||
'node/no-deprecated-api': 'off',
|
||||
'node/no-unpublished-import': 'off',
|
||||
'node/no-unpublished-require': 'off',
|
||||
'node/no-unsupported-features/es-syntax': 'off',
|
||||
|
||||
'@typescript-eslint/no-empty-function': ['error', { allow: ['arrowFunctions'] }],
|
||||
'@typescript-eslint/no-empty-interface': 'off',
|
||||
'@typescript-eslint/no-inferrable-types': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/consistent-type-imports': ['error', { prefer: 'type-imports' }],
|
||||
},
|
||||
})
|
|
@ -0,0 +1,37 @@
|
|||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: ['*']
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.x
|
||||
- run: yarn
|
||||
- run: yarn lint
|
||||
- run: yarn build
|
||||
- run: yarn test
|
||||
|
||||
deno:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: v1.x
|
||||
- run: deno test deno_test
|
||||
|
||||
bun:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- run: curl -fsSL https://bun.sh/install | bash
|
||||
- run: echo "${HOME}/.bun/bin" >> $GITHUB_PATH
|
||||
- run: bun install
|
||||
- run: bun wiptest bun_test/index.test.ts
|
|
@ -0,0 +1,21 @@
|
|||
name: publish
|
||||
on:
|
||||
push:
|
||||
branches: [production]
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
- run: yarn
|
||||
- run: yarn build
|
||||
- run: yarn test
|
||||
- name: publish
|
||||
uses: mikeal/merge-release@master
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
dist
|
||||
yarn-error.log
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"printWidth": 100,
|
||||
"trailingComma": "es5",
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"jsxSingleQuote": true,
|
||||
"endOfLine": "lf"
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
# GraphQL Server Middleware
|
||||
|
||||
## Information
|
||||
|
||||
GraphQL Server Middleware `@honojs/graphql-server` is renamed to `@hono/graphql-server`.
|
||||
`@honojs/graphql-server` is not maintained, please use `@hono/graphql-server`.
|
||||
Also, for Deno, you can use import with `npm:` prefix like `npm:@hono/graphql-server`.
|
||||
|
||||
## Requirements
|
||||
|
||||
This middleware depends on [GraphQL.js](https://www.npmjs.com/package/graphql).
|
||||
|
||||
```sh
|
||||
npm i @hono/graphql-server
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```plain
|
||||
yarn add @hono/graphql-server
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
index.js:
|
||||
|
||||
```js
|
||||
import { Hono } from 'hono'
|
||||
import { graphqlServer } from '@hono/graphql-server'
|
||||
import { buildSchema } from 'graphql'
|
||||
|
||||
export const app = new Hono()
|
||||
|
||||
const schema = buildSchema(`
|
||||
type Query {
|
||||
hello: String
|
||||
}
|
||||
`)
|
||||
|
||||
const rootResolver = (ctx) => {
|
||||
return {
|
||||
hello: () => 'Hello Hono!',
|
||||
}
|
||||
}
|
||||
|
||||
app.use(
|
||||
'/graphql',
|
||||
graphqlServer({
|
||||
schema,
|
||||
rootResolver,
|
||||
})
|
||||
)
|
||||
|
||||
app.fire()
|
||||
```
|
|
@ -0,0 +1,569 @@
|
|||
import { describe, expect, it } from 'bun:test'
|
||||
import {
|
||||
buildSchema,
|
||||
GraphQLSchema,
|
||||
GraphQLString,
|
||||
GraphQLObjectType,
|
||||
GraphQLNonNull,
|
||||
} from 'graphql'
|
||||
import { Hono } from 'hono'
|
||||
import { graphqlServer } from '../src/index'
|
||||
|
||||
// Test just only minimal patterns.
|
||||
// Because others are tested well in Cloudflare Workers environment already.
|
||||
|
||||
describe('graphql-server', () => {
|
||||
// Construct a schema, using GraphQL schema language
|
||||
const schema = buildSchema(`
|
||||
type Query {
|
||||
hello: String
|
||||
}
|
||||
`)
|
||||
|
||||
// The root provides a resolver function for each API endpoint
|
||||
const rootValue = {
|
||||
hello: () => 'Hello world!',
|
||||
}
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.use(
|
||||
'/graphql',
|
||||
graphqlServer({
|
||||
schema,
|
||||
rootResolver: () => rootValue,
|
||||
})
|
||||
)
|
||||
|
||||
app.all('*', (c) => {
|
||||
c.header('foo', 'bar')
|
||||
return c.text('fallback')
|
||||
})
|
||||
|
||||
it('Should return GraphQL response', async () => {
|
||||
const query = 'query { hello }'
|
||||
const body = {
|
||||
query: query,
|
||||
}
|
||||
|
||||
const res = await app.request('http://localhost/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
// TODO enable this
|
||||
// expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('{"data":{"hello":"Hello world!"}}')
|
||||
// TODO enable this
|
||||
// expect(res.headers.get('foo')).toBeNull() // GraphQL Server middleware should be Handler
|
||||
})
|
||||
})
|
||||
|
||||
const QueryRootType = new GraphQLObjectType({
|
||||
name: 'QueryRoot',
|
||||
fields: {
|
||||
test: {
|
||||
type: GraphQLString,
|
||||
args: {
|
||||
who: { type: GraphQLString },
|
||||
},
|
||||
resolve: (_root, args: { who?: string }) => 'Hello ' + (args.who ?? 'World'),
|
||||
},
|
||||
thrower: {
|
||||
type: GraphQLString,
|
||||
resolve() {
|
||||
throw new Error('Throws!')
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const TestSchema = new GraphQLSchema({
|
||||
query: QueryRootType,
|
||||
mutation: new GraphQLObjectType({
|
||||
name: 'MutationRoot',
|
||||
fields: {
|
||||
writeTest: {
|
||||
type: QueryRootType,
|
||||
resolve: () => ({}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
const urlString = (query?: Record<string, string>): string => {
|
||||
const base = 'http://localhost/graphql'
|
||||
if (!query) return base
|
||||
const queryString = new URLSearchParams(query).toString()
|
||||
return `${base}?${queryString}`
|
||||
}
|
||||
|
||||
describe('GraphQL Middleware - GET functionality', () => {
|
||||
const app = new Hono()
|
||||
app.use(
|
||||
'/graphql',
|
||||
graphqlServer({
|
||||
schema: TestSchema,
|
||||
})
|
||||
)
|
||||
|
||||
it('Allows GET with variable values', async () => {
|
||||
const query = {
|
||||
query: 'query helloWho($who: String){ test(who: $who) }',
|
||||
variables: JSON.stringify({ who: 'Dolly' }),
|
||||
}
|
||||
|
||||
const res = await app.request(urlString(query), {
|
||||
method: 'GET',
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('{"data":{"test":"Hello Dolly"}}')
|
||||
})
|
||||
|
||||
it('Allows GET with operation name', async () => {
|
||||
const query = {
|
||||
query: `
|
||||
query helloYou { test(who: "You"), ...shared }
|
||||
query helloWorld { test(who: "World"), ...shared }
|
||||
query helloDolly { test(who: "Dolly"), ...shared }
|
||||
fragment shared on QueryRoot {
|
||||
shared: test(who: "Everyone")
|
||||
}
|
||||
`,
|
||||
operationName: 'helloWorld',
|
||||
}
|
||||
|
||||
const res = await app.request(urlString(query), {
|
||||
method: 'GET',
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
// TODO enable this
|
||||
// expect(await res.json()).toEqual({
|
||||
// data: {
|
||||
// test: 'Hello World',
|
||||
// shared: 'Hello Everyone',
|
||||
// },
|
||||
// })
|
||||
})
|
||||
|
||||
it('Reports validation errors', async () => {
|
||||
const query = { query: '{ test, unknownOne, unknownTwo }' }
|
||||
const res = await app.request(urlString(query), {
|
||||
method: 'GET',
|
||||
})
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
it('Errors when missing operation name', async () => {
|
||||
const query = {
|
||||
query: `
|
||||
query TestQuery { test }
|
||||
mutation TestMutation { writeTest { test } }
|
||||
`,
|
||||
}
|
||||
const res = await app.request(urlString(query), {
|
||||
method: 'GET',
|
||||
})
|
||||
expect(res.status).toBe(500)
|
||||
})
|
||||
|
||||
it('Errors when sending a mutation via GET', async () => {
|
||||
const query = {
|
||||
query: 'mutation TestMutation { writeTest { test } }',
|
||||
}
|
||||
const res = await app.request(urlString(query), {
|
||||
method: 'GET',
|
||||
})
|
||||
expect(res.status).toBe(405)
|
||||
})
|
||||
|
||||
it('Errors when selecting a mutation within a GET', async () => {
|
||||
const query = {
|
||||
operationName: 'TestMutation',
|
||||
query: `
|
||||
query TestQuery { test }
|
||||
mutation TestMutation { writeTest { test } }
|
||||
`,
|
||||
}
|
||||
const res = await app.request(urlString(query), {
|
||||
method: 'GET',
|
||||
})
|
||||
expect(res.status).toBe(405)
|
||||
})
|
||||
|
||||
it('Allows a mutation to exist within a GET', async () => {
|
||||
const query = {
|
||||
operationName: 'TestQuery',
|
||||
query: `
|
||||
mutation TestMutation { writeTest { test } }
|
||||
query TestQuery { test }
|
||||
`,
|
||||
}
|
||||
const res = await app.request(urlString(query), {
|
||||
method: 'GET',
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
// TODO enable this
|
||||
// expect(await res.json()).toEqual({
|
||||
// data: {
|
||||
// test: 'Hello World',
|
||||
// },
|
||||
// })
|
||||
})
|
||||
})
|
||||
|
||||
describe('GraphQL Middleware - POST functionality', () => {
|
||||
const app = new Hono()
|
||||
app.use(
|
||||
'/graphql',
|
||||
graphqlServer({
|
||||
schema: TestSchema,
|
||||
})
|
||||
)
|
||||
|
||||
it('Allows POST with JSON encoding', async () => {
|
||||
const query = { query: '{test}' }
|
||||
|
||||
const res = await app.request(urlString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(query),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('{"data":{"test":"Hello World"}}')
|
||||
})
|
||||
|
||||
it('Allows sending a mutation via POST', async () => {
|
||||
const query = { query: 'mutation TestMutation { writeTest { test } }' }
|
||||
const res = await app.request(urlString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(query),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('{"data":{"writeTest":{"test":"Hello World"}}}')
|
||||
})
|
||||
|
||||
it('Allows POST with url encoding', async () => {
|
||||
const query = {
|
||||
query: '{test}',
|
||||
}
|
||||
const res = await app.request(urlString(query), {
|
||||
method: 'POST',
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('{"data":{"test":"Hello World"}}')
|
||||
})
|
||||
|
||||
it('Supports POST JSON query with string variables', async () => {
|
||||
const query = {
|
||||
query: 'query helloWho($who: String){ test(who: $who) }',
|
||||
variables: JSON.stringify({ who: 'Dolly' }),
|
||||
}
|
||||
const res = await app.request(urlString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(query),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('{"data":{"test":"Hello Dolly"}}')
|
||||
})
|
||||
|
||||
it('Supports POST url encoded query with string variables', async () => {
|
||||
const searchParams = new URLSearchParams({
|
||||
query: 'query helloWho($who: String){ test(who: $who) }',
|
||||
variables: JSON.stringify({ who: 'Dolly' }),
|
||||
})
|
||||
const res = await app.request(urlString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: searchParams.toString(),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('{"data":{"test":"Hello Dolly"}}')
|
||||
})
|
||||
|
||||
it('Supports POST JSON query with GET variable values', async () => {
|
||||
const variables = {
|
||||
variables: JSON.stringify({ who: 'Dolly' }),
|
||||
}
|
||||
const query = { query: 'query helloWho($who: String){ test(who: $who) }' }
|
||||
const res = await app.request(urlString(variables), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(query),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('{"data":{"test":"Hello Dolly"}}')
|
||||
})
|
||||
|
||||
it('Supports POST url encoded query with GET variable values', async () => {
|
||||
const searchParams = new URLSearchParams({
|
||||
query: 'query helloWho($who: String){ test(who: $who) }',
|
||||
})
|
||||
const variables = {
|
||||
variables: JSON.stringify({ who: 'Dolly' }),
|
||||
}
|
||||
const res = await app.request(urlString(variables), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: searchParams.toString(),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('{"data":{"test":"Hello Dolly"}}')
|
||||
})
|
||||
|
||||
it('Supports POST raw text query with GET variable values', async () => {
|
||||
const variables = {
|
||||
variables: JSON.stringify({ who: 'Dolly' }),
|
||||
}
|
||||
const res = await app.request(urlString(variables), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/graphql',
|
||||
},
|
||||
body: 'query helloWho($who: String){ test(who: $who) }',
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('{"data":{"test":"Hello Dolly"}}')
|
||||
})
|
||||
|
||||
it('Allows POST with operation name', async () => {
|
||||
const query = {
|
||||
query: `
|
||||
query helloYou { test(who: "You"), ...shared }
|
||||
query helloWorld { test(who: "World"), ...shared }
|
||||
query helloDolly { test(who: "Dolly"), ...shared }
|
||||
fragment shared on QueryRoot {
|
||||
shared: test(who: "Everyone")
|
||||
}
|
||||
`,
|
||||
operationName: 'helloWorld',
|
||||
}
|
||||
const res = await app.request(urlString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(query),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
// TODO enable this
|
||||
// expect(await res.json()).toEqual({
|
||||
// data: {
|
||||
// test: 'Hello World',
|
||||
// shared: 'Hello Everyone',
|
||||
// },
|
||||
// })
|
||||
})
|
||||
|
||||
it('Allows POST with GET operation name', async () => {
|
||||
const res = await app.request(
|
||||
urlString({
|
||||
operationName: 'helloWorld',
|
||||
}),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/graphql',
|
||||
},
|
||||
body: `
|
||||
query helloYou { test(who: "You"), ...shared }
|
||||
query helloWorld { test(who: "World"), ...shared }
|
||||
query helloDolly { test(who: "Dolly"), ...shared }
|
||||
fragment shared on QueryRoot {
|
||||
shared: test(who: "Everyone")
|
||||
}
|
||||
`,
|
||||
}
|
||||
)
|
||||
expect(res.status).toBe(200)
|
||||
// TODO enable this
|
||||
// expect(await res.json()).toEqual({
|
||||
// data: {
|
||||
// test: 'Hello World',
|
||||
// shared: 'Hello Everyone',
|
||||
// },
|
||||
// })
|
||||
})
|
||||
})
|
||||
|
||||
describe('Pretty printing', () => {
|
||||
it('Supports pretty printing', async () => {
|
||||
const app = new Hono()
|
||||
|
||||
app.use(
|
||||
'/graphql',
|
||||
graphqlServer({
|
||||
schema: TestSchema,
|
||||
pretty: true,
|
||||
})
|
||||
)
|
||||
|
||||
const res = await app.request(urlString({ query: '{test}' }))
|
||||
|
||||
// TODO enable this
|
||||
// expect(await res.text()).toEqual(
|
||||
// [
|
||||
// // Pretty printed JSON
|
||||
// '{',
|
||||
// ' "data": {',
|
||||
// ' "test": "Hello World"',
|
||||
// ' }',
|
||||
// '}',
|
||||
// ].join('\n')
|
||||
// )
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error handling functionality', () => {
|
||||
const app = new Hono()
|
||||
app.use(
|
||||
'/graphql',
|
||||
graphqlServer({
|
||||
schema: TestSchema,
|
||||
})
|
||||
)
|
||||
|
||||
it('Handles query errors from non-null top field errors', async () => {
|
||||
const schema = new GraphQLSchema({
|
||||
query: new GraphQLObjectType({
|
||||
name: 'Query',
|
||||
fields: {
|
||||
test: {
|
||||
type: new GraphQLNonNull(GraphQLString),
|
||||
resolve() {
|
||||
throw new Error('Throws!')
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
const app = new Hono()
|
||||
|
||||
app.use('/graphql', graphqlServer({ schema }))
|
||||
|
||||
const res = await app.request(
|
||||
urlString({
|
||||
query: '{ test }',
|
||||
})
|
||||
)
|
||||
|
||||
expect(res.status).toBe(500)
|
||||
})
|
||||
|
||||
it('Handles syntax errors caught by GraphQL', async () => {
|
||||
const res = await app.request(
|
||||
urlString({
|
||||
query: 'syntax_error',
|
||||
}),
|
||||
{
|
||||
method: 'GET',
|
||||
}
|
||||
)
|
||||
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
it('Handles errors caused by a lack of query', async () => {
|
||||
const res = await app.request(urlString(), {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
it('Handles invalid JSON bodies', async () => {
|
||||
const res = await app.request(urlString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify([]),
|
||||
})
|
||||
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
it('Handles incomplete JSON bodies', async () => {
|
||||
const res = await app.request(urlString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: '{"query":',
|
||||
})
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
it('Handles plain POST text', async () => {
|
||||
const res = await app.request(
|
||||
urlString({
|
||||
variables: JSON.stringify({ who: 'Dolly' }),
|
||||
}),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
body: 'query helloWho($who: String){ test(who: $who) }',
|
||||
}
|
||||
)
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
it('Handles poorly formed variables', async () => {
|
||||
const res = await app.request(
|
||||
urlString({
|
||||
variables: 'who:You',
|
||||
query: 'query helloWho($who: String){ test(who: $who) }',
|
||||
}),
|
||||
{
|
||||
method: 'GET',
|
||||
}
|
||||
)
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
it('Handles invalid variables', async () => {
|
||||
const res = await app.request(urlString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: 'query helloWho($who: String){ test(who: $who) }',
|
||||
variables: { who: ['John', 'Jane'] },
|
||||
}),
|
||||
})
|
||||
expect(res.status).toBe(500)
|
||||
})
|
||||
|
||||
it('Handles unsupported HTTP methods', async () => {
|
||||
const res = await app.request(urlString({ query: '{test}' }), {
|
||||
method: 'PUT',
|
||||
})
|
||||
|
||||
expect(res.status).toBe(405)
|
||||
expect(res.headers.get('allow')).toBe('GET, POST')
|
||||
// TODO enable this
|
||||
// expect(await res.json()).toEqual({
|
||||
// errors: [{ message: 'GraphQL only supports GET and POST requests.' }],
|
||||
// })
|
||||
})
|
||||
})
|
|
@ -0,0 +1,55 @@
|
|||
# GraphQL Server Middleware
|
||||
|
||||
## Information
|
||||
|
||||
GraphQL Server Middleware `@honojs/graphql-server` is renamed to `@hono/graphql-server`.
|
||||
`@honojs/graphql-server` is not maintained, please use `@hono/graphql-server`.
|
||||
Also, for Deno, you can use import with `npm:` prefix like `npm:@hono/graphql-server`.
|
||||
|
||||
## Requirements
|
||||
|
||||
This middleware depends on [GraphQL.js](https://www.npmjs.com/package/graphql).
|
||||
|
||||
```sh
|
||||
npm i @hono/graphql-server
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```plain
|
||||
yarn add @hono/graphql-server
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
index.js:
|
||||
|
||||
```js
|
||||
import { Hono } from 'hono'
|
||||
import { graphqlServer } from '@hono/graphql-server'
|
||||
import { buildSchema } from 'graphql'
|
||||
|
||||
export const app = new Hono()
|
||||
|
||||
const schema = buildSchema(`
|
||||
type Query {
|
||||
hello: String
|
||||
}
|
||||
`)
|
||||
|
||||
const rootResolver = (ctx) => {
|
||||
return {
|
||||
hello: () => 'Hello Hono!',
|
||||
}
|
||||
}
|
||||
|
||||
app.use(
|
||||
'/graphql',
|
||||
graphqlServer({
|
||||
schema,
|
||||
rootResolver,
|
||||
})
|
||||
)
|
||||
|
||||
app.fire()
|
||||
```
|
|
@ -0,0 +1,234 @@
|
|||
import {
|
||||
Source,
|
||||
parse,
|
||||
execute,
|
||||
validateSchema,
|
||||
validate,
|
||||
specifiedRules,
|
||||
getOperationAST,
|
||||
GraphQLError,
|
||||
} from 'https://cdn.skypack.dev/graphql@16.5.0?dts'
|
||||
|
||||
import type {
|
||||
GraphQLSchema,
|
||||
DocumentNode,
|
||||
ValidationRule,
|
||||
FormattedExecutionResult,
|
||||
GraphQLFormattedError,
|
||||
} from 'https://cdn.skypack.dev/graphql@16.5.0?dts'
|
||||
|
||||
import type { Context } from 'https://deno.land/x/hono/mod.ts'
|
||||
import { parseBody } from './parse-body.ts'
|
||||
|
||||
export type RootResolver = (ctx?: Context) => Promise<unknown> | unknown
|
||||
|
||||
type Options = {
|
||||
schema: GraphQLSchema
|
||||
rootResolver?: RootResolver,
|
||||
pretty?: boolean
|
||||
validationRules?: ReadonlyArray<ValidationRule>
|
||||
// graphiql?: boolean
|
||||
}
|
||||
|
||||
export const graphqlServer = (options: Options) => {
|
||||
const schema = options.schema
|
||||
const pretty = options.pretty ?? false
|
||||
const validationRules = options.validationRules ?? []
|
||||
// const showGraphiQL = options.graphiql ?? false
|
||||
|
||||
return async (c: Context) => {
|
||||
// GraphQL HTTP only supports GET and POST methods.
|
||||
if (c.req.method !== 'GET' && c.req.method !== 'POST') {
|
||||
return c.json(errorMessages(['GraphQL only supports GET and POST requests.']), 405, {
|
||||
Allow: 'GET, POST',
|
||||
})
|
||||
}
|
||||
|
||||
let params: GraphQLParams
|
||||
try {
|
||||
params = await getGraphQLParams(c.req)
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
console.error(`${e.stack || e.message}`)
|
||||
return c.json(errorMessages([e.message], [e]), 400)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
||||
const { query, variables, operationName } = params
|
||||
|
||||
if (query == null) {
|
||||
return c.json(errorMessages(['Must provide query string.']), 400)
|
||||
}
|
||||
|
||||
const schemaValidationErrors = validateSchema(schema)
|
||||
if (schemaValidationErrors.length > 0) {
|
||||
// Return 500: Internal Server Error if invalid schema.
|
||||
return c.json(
|
||||
errorMessages(['GraphQL schema validation error.'], schemaValidationErrors),
|
||||
500
|
||||
)
|
||||
}
|
||||
|
||||
let documentAST: DocumentNode
|
||||
try {
|
||||
documentAST = parse(new Source(query, 'GraphQL request'))
|
||||
} catch (syntaxError: unknown) {
|
||||
// Return 400: Bad Request if any syntax errors errors exist.
|
||||
if (syntaxError instanceof Error) {
|
||||
console.error(`${syntaxError.stack || syntaxError.message}`)
|
||||
const e = new GraphQLError(syntaxError.message, {
|
||||
originalError: syntaxError,
|
||||
})
|
||||
return c.json(errorMessages(['GraphQL syntax error.'], [e]), 400)
|
||||
}
|
||||
throw syntaxError
|
||||
}
|
||||
|
||||
// Validate AST, reporting any errors.
|
||||
const validationErrors = validate(schema, documentAST, [...specifiedRules, ...validationRules])
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
// Return 400: Bad Request if any validation errors exist.
|
||||
return c.json(errorMessages(['GraphQL validation error.'], validationErrors), 400)
|
||||
}
|
||||
|
||||
if (c.req.method === 'GET') {
|
||||
// Determine if this GET request will perform a non-query.
|
||||
const operationAST = getOperationAST(documentAST, operationName)
|
||||
if (operationAST && operationAST.operation !== 'query') {
|
||||
/*
|
||||
Now , does not support GraphiQL
|
||||
if (showGraphiQL) {
|
||||
//return respondWithGraphiQL(response, graphiqlOptions, params)
|
||||
}
|
||||
*/
|
||||
|
||||
// Otherwise, report a 405: Method Not Allowed error.
|
||||
return c.json(
|
||||
errorMessages([
|
||||
`Can only perform a ${operationAST.operation} operation from a POST request.`,
|
||||
]),
|
||||
405,
|
||||
{ Allow: 'POST' }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let result: FormattedExecutionResult
|
||||
const { rootResolver } = options
|
||||
|
||||
try {
|
||||
result = await execute({
|
||||
schema,
|
||||
document: documentAST,
|
||||
rootValue: rootResolver ? await rootResolver(c) : null,
|
||||
variableValues: variables,
|
||||
operationName: operationName,
|
||||
})
|
||||
} catch (contextError: unknown) {
|
||||
if (contextError instanceof Error) {
|
||||
console.error(`${contextError.stack || contextError.message}`)
|
||||
const e = new GraphQLError(contextError.message, {
|
||||
originalError: contextError,
|
||||
nodes: documentAST,
|
||||
})
|
||||
// Return 400: Bad Request if any execution context errors exist.
|
||||
return c.json(errorMessages(['GraphQL execution context error.'], [e]), 400)
|
||||
}
|
||||
throw contextError
|
||||
}
|
||||
|
||||
if (!result.data) {
|
||||
if (result.errors) {
|
||||
return c.json(errorMessages([result.errors.toString()], result.errors), 500)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Now, does not support GraphiQL
|
||||
if (showGraphiQL) {
|
||||
}
|
||||
*/
|
||||
|
||||
if (pretty) {
|
||||
const payload = JSON.stringify(result, null, pretty ? 2 : 0)
|
||||
return c.text(payload, 200, {
|
||||
'Content-Type': 'application/json',
|
||||
})
|
||||
} else {
|
||||
return c.json(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface GraphQLParams {
|
||||
query: string | null
|
||||
variables: { readonly [name: string]: unknown } | null
|
||||
operationName: string | null
|
||||
raw: boolean
|
||||
}
|
||||
|
||||
export const getGraphQLParams = async (request: Request): Promise<GraphQLParams> => {
|
||||
const urlData = new URLSearchParams(request.url.split('?')[1])
|
||||
const bodyData = await parseBody(request)
|
||||
|
||||
// GraphQL Query string.
|
||||
let query = urlData.get('query') ?? (bodyData.query as string | null)
|
||||
|
||||
if (typeof query !== 'string') {
|
||||
query = null
|
||||
}
|
||||
|
||||
// Parse the variables if needed.
|
||||
let variables = (urlData.get('variables') ?? bodyData.variables) as {
|
||||
readonly [name: string]: unknown
|
||||
} | null
|
||||
if (typeof variables === 'string') {
|
||||
try {
|
||||
variables = JSON.parse(variables)
|
||||
} catch {
|
||||
throw Error('Variables are invalid JSON.')
|
||||
}
|
||||
} else if (typeof variables !== 'object') {
|
||||
variables = null
|
||||
}
|
||||
|
||||
// Name of GraphQL operation to execute.
|
||||
let operationName = urlData.get('operationName') ?? (bodyData.operationName as string | null)
|
||||
if (typeof operationName !== 'string') {
|
||||
operationName = null
|
||||
}
|
||||
|
||||
const raw = urlData.get('raw') != null || bodyData.raw !== undefined
|
||||
|
||||
const params: GraphQLParams = {
|
||||
query: query,
|
||||
variables: variables,
|
||||
operationName: operationName,
|
||||
raw: raw,
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
export const errorMessages = (
|
||||
messages: string[],
|
||||
graphqlErrors?: readonly GraphQLError[] | readonly GraphQLFormattedError[]
|
||||
) => {
|
||||
if (graphqlErrors) {
|
||||
return {
|
||||
errors: graphqlErrors,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
errors: messages.map((message) => {
|
||||
return {
|
||||
message: message,
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// export const graphiQLResponse = () => {}
|
|
@ -0,0 +1 @@
|
|||
export * from "./index.ts";
|
|
@ -0,0 +1,29 @@
|
|||
export async function parseBody(req: Request): Promise<Record<string, unknown>> {
|
||||
const contentType = req.headers.get('content-type')
|
||||
|
||||
switch (contentType) {
|
||||
case 'application/graphql':
|
||||
return { query: await req.text() }
|
||||
case 'application/json':
|
||||
try {
|
||||
return await req.json()
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
console.error(`${e.stack || e.message}`)
|
||||
}
|
||||
throw Error(`POST body sent invalid JSON: ${e}`)
|
||||
}
|
||||
case 'application/x-www-form-urlencoded':
|
||||
return parseFormURL(req)
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
const parseFormURL = async (req: Request) => {
|
||||
const text = await req.text()
|
||||
const searchParams = new URLSearchParams(text)
|
||||
const res: { [params: string]: string } = {}
|
||||
searchParams.forEach((v, k) => (res[k] = v))
|
||||
return res
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import { buildSchema } from 'https://cdn.skypack.dev/graphql@16.5.0?dts'
|
||||
import { Hono } from 'https://deno.land/x/hono@v2.6.1/mod.ts'
|
||||
import { graphqlServer } from '../deno_dist/index.ts'
|
||||
import { assertEquals } from 'https://deno.land/std@0.149.0/testing/asserts.ts'
|
||||
|
||||
Deno.test('graphql-server', async (t) => {
|
||||
// Construct a schema, using GraphQL schema language
|
||||
const schema = buildSchema(`
|
||||
type Query {
|
||||
hello: String
|
||||
}
|
||||
`)
|
||||
|
||||
// The root provides a resolver function for each API endpoint
|
||||
const rootValue = {
|
||||
hello: () => 'Hello world!',
|
||||
}
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.use(
|
||||
'/graphql',
|
||||
graphqlServer({
|
||||
schema,
|
||||
rootResolver: () => rootValue,
|
||||
})
|
||||
)
|
||||
|
||||
app.all('*', (c) => {
|
||||
c.header('foo', 'bar')
|
||||
return c.text('fallback')
|
||||
})
|
||||
|
||||
const query = 'query { hello }'
|
||||
const body = {
|
||||
query: query,
|
||||
}
|
||||
|
||||
const res = await app.request('http://localhost/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
assertEquals(res.status, 200)
|
||||
assertEquals(await res.text(), '{"data":{"hello":"Hello world!"}}')
|
||||
})
|
|
@ -0,0 +1,7 @@
|
|||
module.exports = {
|
||||
testMatch: ['**/test/**/*.+(ts|tsx|js)', '**/src/**/(*.)+(spec|test).+(ts|tsx|js)'],
|
||||
transform: {
|
||||
'^.+\\.(ts|tsx)$': 'ts-jest',
|
||||
},
|
||||
testEnvironment: 'miniflare',
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"name": "@honojs/graphql-server",
|
||||
"version": "0.1.2",
|
||||
"repository": "git@github.com:honojs/grahql-server.git",
|
||||
"author": "Minghe Huang <h.minghe@gmail.com>",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"license": "MIT",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"registry": "https://registry.npmjs.org/"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "jest",
|
||||
"test:deno": "deno test deno_test",
|
||||
"test:bun": "bun wiptest bun_test/index.test.ts",
|
||||
"test:all": "yarn test && yarn test:deno && yarn test:bun",
|
||||
"denoify": "rimraf deno_dist && denoify && rimraf 'deno_dist/**/*.test.ts'",
|
||||
"build": "rimraf dist && tsc --project tsconfig.build.json",
|
||||
"lint": "eslint --ext js,ts src .eslintrc.js",
|
||||
"lint:fix": "eslint --ext js,ts src .eslintrc.js --fix",
|
||||
"prerelease": "yarn build && yarn denoify && yarn test:all",
|
||||
"release": "np"
|
||||
},
|
||||
"denoify": {
|
||||
"replacer": "dist/replacer.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"hono": "^2.6.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"graphql": "^16.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^3.14.0",
|
||||
"@types/jest": "^28.1.4",
|
||||
"@typescript-eslint/eslint-plugin": "^5.21.0",
|
||||
"@typescript-eslint/parser": "^5.21.0",
|
||||
"denoify": "^1.4.5",
|
||||
"eslint": "^8.14.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-define-config": "^1.4.0",
|
||||
"eslint-import-resolver-typescript": "^2.7.1",
|
||||
"eslint-plugin-eslint-comments": "^3.2.0",
|
||||
"eslint-plugin-flowtype": "^8.0.3",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"hono": "^2.6.1",
|
||||
"jest": "^28.1.2",
|
||||
"jest-environment-miniflare": "^2.6.0",
|
||||
"np": "^7.6.2",
|
||||
"prettier": "^2.7.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^28.0.5",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=11.0.0"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,626 @@
|
|||
import {
|
||||
buildSchema,
|
||||
GraphQLSchema,
|
||||
GraphQLString,
|
||||
GraphQLObjectType,
|
||||
GraphQLNonNull,
|
||||
} from 'graphql'
|
||||
import { Hono } from 'hono'
|
||||
import type { Context, Next } from 'hono'
|
||||
import { errorMessages, graphqlServer } from '.'
|
||||
import type { RootResolver } from './index'
|
||||
|
||||
// Do not show `console.error` messages
|
||||
jest.spyOn(console, 'error').mockImplementation()
|
||||
|
||||
describe('errorMessages', () => {
|
||||
const messages = errorMessages(['message a', 'message b'])
|
||||
expect(messages).toEqual({
|
||||
errors: [
|
||||
{
|
||||
message: 'message a',
|
||||
},
|
||||
{
|
||||
message: 'message b',
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
describe('GraphQL Middleware - Simple way', () => {
|
||||
it('Should return GraphQL response', async () => {
|
||||
const schema = buildSchema(`
|
||||
type Query {
|
||||
hello: String
|
||||
}
|
||||
`)
|
||||
|
||||
const rootValue = {
|
||||
hello: () => 'Hello world!',
|
||||
}
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.use(
|
||||
'/graphql',
|
||||
graphqlServer({
|
||||
schema,
|
||||
rootResolver: () => rootValue,
|
||||
})
|
||||
)
|
||||
|
||||
app.all('*', (c) => {
|
||||
c.header('foo', 'bar')
|
||||
return c.text('fallback')
|
||||
})
|
||||
|
||||
const query = 'query { hello }'
|
||||
const body = {
|
||||
query: query,
|
||||
}
|
||||
|
||||
const res = await app.request('http://localhost/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('{"data":{"hello":"Hello world!"}}')
|
||||
expect(res.headers.get('foo')).toBeNull() // GraphQL Server middleware should be Handler
|
||||
})
|
||||
|
||||
it('Should access the ctx', async () => {
|
||||
const schema = buildSchema(`
|
||||
type Query {
|
||||
hi: String
|
||||
}
|
||||
`)
|
||||
|
||||
const rootResolver: RootResolver = (ctx?: Context) => {
|
||||
const name = ctx?.get('name')
|
||||
return {
|
||||
hi: `hi ${name}`
|
||||
}
|
||||
}
|
||||
|
||||
const app = new Hono()
|
||||
|
||||
app.use('*', async (ctx: Context, next: Next) => {
|
||||
ctx.set('name', 'Jason')
|
||||
await next()
|
||||
})
|
||||
|
||||
|
||||
app.use(
|
||||
'/graphql',
|
||||
graphqlServer({
|
||||
schema,
|
||||
rootResolver,
|
||||
})
|
||||
)
|
||||
|
||||
app.all('*', (c) => {
|
||||
c.header('foo', 'bar')
|
||||
return c.text('fallback')
|
||||
})
|
||||
|
||||
const query = 'query { hi }'
|
||||
const body = {
|
||||
query: query,
|
||||
}
|
||||
|
||||
const res = await app.request('http://localhost/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
})
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('{"data":{"hi":"hi Jason"}}')
|
||||
expect(res.headers.get('foo')).toBeNull() // GraphQL Server middleware should be Handler
|
||||
})
|
||||
})
|
||||
|
||||
const QueryRootType = new GraphQLObjectType({
|
||||
name: 'QueryRoot',
|
||||
fields: {
|
||||
test: {
|
||||
type: GraphQLString,
|
||||
args: {
|
||||
who: { type: GraphQLString },
|
||||
},
|
||||
resolve: (_root, args: { who?: string }) => 'Hello ' + (args.who ?? 'World'),
|
||||
},
|
||||
thrower: {
|
||||
type: GraphQLString,
|
||||
resolve() {
|
||||
throw new Error('Throws!')
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const TestSchema = new GraphQLSchema({
|
||||
query: QueryRootType,
|
||||
mutation: new GraphQLObjectType({
|
||||
name: 'MutationRoot',
|
||||
fields: {
|
||||
writeTest: {
|
||||
type: QueryRootType,
|
||||
resolve: () => ({}),
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
const urlString = (query?: Record<string, string>): string => {
|
||||
const base = 'http://localhost/graphql'
|
||||
if (!query) return base
|
||||
const queryString = new URLSearchParams(query).toString()
|
||||
return `${base}?${queryString}`
|
||||
}
|
||||
|
||||
describe('GraphQL Middleware - GET functionality', () => {
|
||||
const app = new Hono()
|
||||
app.use(
|
||||
'/graphql',
|
||||
graphqlServer({
|
||||
schema: TestSchema,
|
||||
})
|
||||
)
|
||||
|
||||
it('Allows GET with variable values', async () => {
|
||||
const query = {
|
||||
query: 'query helloWho($who: String){ test(who: $who) }',
|
||||
variables: JSON.stringify({ who: 'Dolly' }),
|
||||
}
|
||||
|
||||
const res = await app.request(urlString(query), {
|
||||
method: 'GET',
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('{"data":{"test":"Hello Dolly"}}')
|
||||
})
|
||||
|
||||
it('Allows GET with operation name', async () => {
|
||||
const query = {
|
||||
query: `
|
||||
query helloYou { test(who: "You"), ...shared }
|
||||
query helloWorld { test(who: "World"), ...shared }
|
||||
query helloDolly { test(who: "Dolly"), ...shared }
|
||||
fragment shared on QueryRoot {
|
||||
shared: test(who: "Everyone")
|
||||
}
|
||||
`,
|
||||
operationName: 'helloWorld',
|
||||
}
|
||||
|
||||
const res = await app.request(urlString(query), {
|
||||
method: 'GET',
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual({
|
||||
data: {
|
||||
test: 'Hello World',
|
||||
shared: 'Hello Everyone',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('Reports validation errors', async () => {
|
||||
const query = { query: '{ test, unknownOne, unknownTwo }' }
|
||||
const res = await app.request(urlString(query), {
|
||||
method: 'GET',
|
||||
})
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
it('Errors when missing operation name', async () => {
|
||||
const query = {
|
||||
query: `
|
||||
query TestQuery { test }
|
||||
mutation TestMutation { writeTest { test } }
|
||||
`,
|
||||
}
|
||||
const res = await app.request(urlString(query), {
|
||||
method: 'GET',
|
||||
})
|
||||
expect(res.status).toBe(500)
|
||||
})
|
||||
|
||||
it('Errors when sending a mutation via GET', async () => {
|
||||
const query = {
|
||||
query: 'mutation TestMutation { writeTest { test } }',
|
||||
}
|
||||
const res = await app.request(urlString(query), {
|
||||
method: 'GET',
|
||||
})
|
||||
expect(res.status).toBe(405)
|
||||
})
|
||||
|
||||
it('Errors when selecting a mutation within a GET', async () => {
|
||||
const query = {
|
||||
operationName: 'TestMutation',
|
||||
query: `
|
||||
query TestQuery { test }
|
||||
mutation TestMutation { writeTest { test } }
|
||||
`,
|
||||
}
|
||||
const res = await app.request(urlString(query), {
|
||||
method: 'GET',
|
||||
})
|
||||
expect(res.status).toBe(405)
|
||||
})
|
||||
|
||||
it('Allows a mutation to exist within a GET', async () => {
|
||||
const query = {
|
||||
operationName: 'TestQuery',
|
||||
query: `
|
||||
mutation TestMutation { writeTest { test } }
|
||||
query TestQuery { test }
|
||||
`,
|
||||
}
|
||||
const res = await app.request(urlString(query), {
|
||||
method: 'GET',
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual({
|
||||
data: {
|
||||
test: 'Hello World',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('GraphQL Middleware - POST functionality', () => {
|
||||
const app = new Hono()
|
||||
app.use(
|
||||
'/graphql',
|
||||
graphqlServer({
|
||||
schema: TestSchema,
|
||||
})
|
||||
)
|
||||
|
||||
it('Allows POST with JSON encoding', async () => {
|
||||
const query = { query: '{test}' }
|
||||
|
||||
const res = await app.request(urlString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(query),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('{"data":{"test":"Hello World"}}')
|
||||
})
|
||||
|
||||
it('Allows sending a mutation via POST', async () => {
|
||||
const query = { query: 'mutation TestMutation { writeTest { test } }' }
|
||||
const res = await app.request(urlString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(query),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('{"data":{"writeTest":{"test":"Hello World"}}}')
|
||||
})
|
||||
|
||||
it('Allows POST with url encoding', async () => {
|
||||
const query = {
|
||||
query: '{test}',
|
||||
}
|
||||
const res = await app.request(urlString(query), {
|
||||
method: 'POST',
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('{"data":{"test":"Hello World"}}')
|
||||
})
|
||||
|
||||
it('Supports POST JSON query with string variables', async () => {
|
||||
const query = {
|
||||
query: 'query helloWho($who: String){ test(who: $who) }',
|
||||
variables: JSON.stringify({ who: 'Dolly' }),
|
||||
}
|
||||
const res = await app.request(urlString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(query),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('{"data":{"test":"Hello Dolly"}}')
|
||||
})
|
||||
|
||||
it('Supports POST url encoded query with string variables', async () => {
|
||||
const searchParams = new URLSearchParams({
|
||||
query: 'query helloWho($who: String){ test(who: $who) }',
|
||||
variables: JSON.stringify({ who: 'Dolly' }),
|
||||
})
|
||||
const res = await app.request(urlString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: searchParams.toString(),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('{"data":{"test":"Hello Dolly"}}')
|
||||
})
|
||||
|
||||
it('Supports POST JSON query with GET variable values', async () => {
|
||||
const variables = {
|
||||
variables: JSON.stringify({ who: 'Dolly' }),
|
||||
}
|
||||
const query = { query: 'query helloWho($who: String){ test(who: $who) }' }
|
||||
const res = await app.request(urlString(variables), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(query),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('{"data":{"test":"Hello Dolly"}}')
|
||||
})
|
||||
|
||||
it('Supports POST url encoded query with GET variable values', async () => {
|
||||
const searchParams = new URLSearchParams({
|
||||
query: 'query helloWho($who: String){ test(who: $who) }',
|
||||
})
|
||||
const variables = {
|
||||
variables: JSON.stringify({ who: 'Dolly' }),
|
||||
}
|
||||
const res = await app.request(urlString(variables), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: searchParams.toString(),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('{"data":{"test":"Hello Dolly"}}')
|
||||
})
|
||||
|
||||
it('Supports POST raw text query with GET variable values', async () => {
|
||||
const variables = {
|
||||
variables: JSON.stringify({ who: 'Dolly' }),
|
||||
}
|
||||
const res = await app.request(urlString(variables), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/graphql',
|
||||
},
|
||||
body: 'query helloWho($who: String){ test(who: $who) }',
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.text()).toBe('{"data":{"test":"Hello Dolly"}}')
|
||||
})
|
||||
|
||||
it('Allows POST with operation name', async () => {
|
||||
const query = {
|
||||
query: `
|
||||
query helloYou { test(who: "You"), ...shared }
|
||||
query helloWorld { test(who: "World"), ...shared }
|
||||
query helloDolly { test(who: "Dolly"), ...shared }
|
||||
fragment shared on QueryRoot {
|
||||
shared: test(who: "Everyone")
|
||||
}
|
||||
`,
|
||||
operationName: 'helloWorld',
|
||||
}
|
||||
const res = await app.request(urlString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(query),
|
||||
})
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual({
|
||||
data: {
|
||||
test: 'Hello World',
|
||||
shared: 'Hello Everyone',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('Allows POST with GET operation name', async () => {
|
||||
const res = await app.request(
|
||||
urlString({
|
||||
operationName: 'helloWorld',
|
||||
}),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/graphql',
|
||||
},
|
||||
body: `
|
||||
query helloYou { test(who: "You"), ...shared }
|
||||
query helloWorld { test(who: "World"), ...shared }
|
||||
query helloDolly { test(who: "Dolly"), ...shared }
|
||||
fragment shared on QueryRoot {
|
||||
shared: test(who: "Everyone")
|
||||
}
|
||||
`,
|
||||
}
|
||||
)
|
||||
expect(res.status).toBe(200)
|
||||
expect(await res.json()).toEqual({
|
||||
data: {
|
||||
test: 'Hello World',
|
||||
shared: 'Hello Everyone',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Pretty printing', () => {
|
||||
it('Supports pretty printing', async () => {
|
||||
const app = new Hono()
|
||||
|
||||
app.use(
|
||||
'/graphql',
|
||||
graphqlServer({
|
||||
schema: TestSchema,
|
||||
pretty: true,
|
||||
})
|
||||
)
|
||||
|
||||
const res = await app.request(urlString({ query: '{test}' }))
|
||||
expect(await res.text()).toEqual(
|
||||
[
|
||||
// Pretty printed JSON
|
||||
'{',
|
||||
' "data": {',
|
||||
' "test": "Hello World"',
|
||||
' }',
|
||||
'}',
|
||||
].join('\n')
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Error handling functionality', () => {
|
||||
const app = new Hono()
|
||||
app.use(
|
||||
'/graphql',
|
||||
graphqlServer({
|
||||
schema: TestSchema,
|
||||
})
|
||||
)
|
||||
|
||||
it('Handles query errors from non-null top field errors', async () => {
|
||||
const schema = new GraphQLSchema({
|
||||
query: new GraphQLObjectType({
|
||||
name: 'Query',
|
||||
fields: {
|
||||
test: {
|
||||
type: new GraphQLNonNull(GraphQLString),
|
||||
resolve() {
|
||||
throw new Error('Throws!')
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
})
|
||||
const app = new Hono()
|
||||
|
||||
app.use('/graphql', graphqlServer({ schema }))
|
||||
|
||||
const res = await app.request(
|
||||
urlString({
|
||||
query: '{ test }',
|
||||
})
|
||||
)
|
||||
|
||||
expect(res.status).toBe(500)
|
||||
})
|
||||
|
||||
it('Handles syntax errors caught by GraphQL', async () => {
|
||||
const res = await app.request(
|
||||
urlString({
|
||||
query: 'syntax_error',
|
||||
}),
|
||||
{
|
||||
method: 'GET',
|
||||
}
|
||||
)
|
||||
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
it('Handles errors caused by a lack of query', async () => {
|
||||
const res = await app.request(urlString(), {
|
||||
method: 'GET',
|
||||
})
|
||||
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
it('Handles invalid JSON bodies', async () => {
|
||||
const res = await app.request(urlString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify([]),
|
||||
})
|
||||
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
it('Handles incomplete JSON bodies', async () => {
|
||||
const res = await app.request(urlString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: '{"query":',
|
||||
})
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
it('Handles plain POST text', async () => {
|
||||
const res = await app.request(
|
||||
urlString({
|
||||
variables: JSON.stringify({ who: 'Dolly' }),
|
||||
}),
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
body: 'query helloWho($who: String){ test(who: $who) }',
|
||||
}
|
||||
)
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
it('Handles poorly formed variables', async () => {
|
||||
const res = await app.request(
|
||||
urlString({
|
||||
variables: 'who:You',
|
||||
query: 'query helloWho($who: String){ test(who: $who) }',
|
||||
}),
|
||||
{
|
||||
method: 'GET',
|
||||
}
|
||||
)
|
||||
expect(res.status).toBe(400)
|
||||
})
|
||||
|
||||
it('Handles invalid variables', async () => {
|
||||
const res = await app.request(urlString(), {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: 'query helloWho($who: String){ test(who: $who) }',
|
||||
variables: { who: ['John', 'Jane'] },
|
||||
}),
|
||||
})
|
||||
expect(res.status).toBe(500)
|
||||
})
|
||||
|
||||
it('Handles unsupported HTTP methods', async () => {
|
||||
const res = await app.request(urlString({ query: '{test}' }), {
|
||||
method: 'PUT',
|
||||
})
|
||||
|
||||
expect(res.status).toBe(405)
|
||||
expect(res.headers.get('allow')).toBe('GET, POST')
|
||||
expect(await res.json()).toEqual({
|
||||
errors: [{ message: 'GraphQL only supports GET and POST requests.' }],
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,234 @@
|
|||
import {
|
||||
Source,
|
||||
parse,
|
||||
execute,
|
||||
validateSchema,
|
||||
validate,
|
||||
specifiedRules,
|
||||
getOperationAST,
|
||||
GraphQLError,
|
||||
} from 'graphql'
|
||||
|
||||
import type {
|
||||
GraphQLSchema,
|
||||
DocumentNode,
|
||||
ValidationRule,
|
||||
FormattedExecutionResult,
|
||||
GraphQLFormattedError,
|
||||
} from 'graphql'
|
||||
|
||||
import type { Context } from 'hono'
|
||||
import { parseBody } from './parse-body'
|
||||
|
||||
export type RootResolver = (ctx?: Context) => Promise<unknown> | unknown
|
||||
|
||||
type Options = {
|
||||
schema: GraphQLSchema
|
||||
rootResolver?: RootResolver,
|
||||
pretty?: boolean
|
||||
validationRules?: ReadonlyArray<ValidationRule>
|
||||
// graphiql?: boolean
|
||||
}
|
||||
|
||||
export const graphqlServer = (options: Options) => {
|
||||
const schema = options.schema
|
||||
const pretty = options.pretty ?? false
|
||||
const validationRules = options.validationRules ?? []
|
||||
// const showGraphiQL = options.graphiql ?? false
|
||||
|
||||
return async (c: Context) => {
|
||||
// GraphQL HTTP only supports GET and POST methods.
|
||||
if (c.req.method !== 'GET' && c.req.method !== 'POST') {
|
||||
return c.json(errorMessages(['GraphQL only supports GET and POST requests.']), 405, {
|
||||
Allow: 'GET, POST',
|
||||
})
|
||||
}
|
||||
|
||||
let params: GraphQLParams
|
||||
try {
|
||||
params = await getGraphQLParams(c.req)
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
console.error(`${e.stack || e.message}`)
|
||||
return c.json(errorMessages([e.message], [e]), 400)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
||||
const { query, variables, operationName } = params
|
||||
|
||||
if (query == null) {
|
||||
return c.json(errorMessages(['Must provide query string.']), 400)
|
||||
}
|
||||
|
||||
const schemaValidationErrors = validateSchema(schema)
|
||||
if (schemaValidationErrors.length > 0) {
|
||||
// Return 500: Internal Server Error if invalid schema.
|
||||
return c.json(
|
||||
errorMessages(['GraphQL schema validation error.'], schemaValidationErrors),
|
||||
500
|
||||
)
|
||||
}
|
||||
|
||||
let documentAST: DocumentNode
|
||||
try {
|
||||
documentAST = parse(new Source(query, 'GraphQL request'))
|
||||
} catch (syntaxError: unknown) {
|
||||
// Return 400: Bad Request if any syntax errors errors exist.
|
||||
if (syntaxError instanceof Error) {
|
||||
console.error(`${syntaxError.stack || syntaxError.message}`)
|
||||
const e = new GraphQLError(syntaxError.message, {
|
||||
originalError: syntaxError,
|
||||
})
|
||||
return c.json(errorMessages(['GraphQL syntax error.'], [e]), 400)
|
||||
}
|
||||
throw syntaxError
|
||||
}
|
||||
|
||||
// Validate AST, reporting any errors.
|
||||
const validationErrors = validate(schema, documentAST, [...specifiedRules, ...validationRules])
|
||||
|
||||
if (validationErrors.length > 0) {
|
||||
// Return 400: Bad Request if any validation errors exist.
|
||||
return c.json(errorMessages(['GraphQL validation error.'], validationErrors), 400)
|
||||
}
|
||||
|
||||
if (c.req.method === 'GET') {
|
||||
// Determine if this GET request will perform a non-query.
|
||||
const operationAST = getOperationAST(documentAST, operationName)
|
||||
if (operationAST && operationAST.operation !== 'query') {
|
||||
/*
|
||||
Now , does not support GraphiQL
|
||||
if (showGraphiQL) {
|
||||
//return respondWithGraphiQL(response, graphiqlOptions, params)
|
||||
}
|
||||
*/
|
||||
|
||||
// Otherwise, report a 405: Method Not Allowed error.
|
||||
return c.json(
|
||||
errorMessages([
|
||||
`Can only perform a ${operationAST.operation} operation from a POST request.`,
|
||||
]),
|
||||
405,
|
||||
{ Allow: 'POST' }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let result: FormattedExecutionResult
|
||||
const { rootResolver } = options
|
||||
|
||||
try {
|
||||
result = await execute({
|
||||
schema,
|
||||
document: documentAST,
|
||||
rootValue: rootResolver ? await rootResolver(c) : null,
|
||||
variableValues: variables,
|
||||
operationName: operationName,
|
||||
})
|
||||
} catch (contextError: unknown) {
|
||||
if (contextError instanceof Error) {
|
||||
console.error(`${contextError.stack || contextError.message}`)
|
||||
const e = new GraphQLError(contextError.message, {
|
||||
originalError: contextError,
|
||||
nodes: documentAST,
|
||||
})
|
||||
// Return 400: Bad Request if any execution context errors exist.
|
||||
return c.json(errorMessages(['GraphQL execution context error.'], [e]), 400)
|
||||
}
|
||||
throw contextError
|
||||
}
|
||||
|
||||
if (!result.data) {
|
||||
if (result.errors) {
|
||||
return c.json(errorMessages([result.errors.toString()], result.errors), 500)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Now, does not support GraphiQL
|
||||
if (showGraphiQL) {
|
||||
}
|
||||
*/
|
||||
|
||||
if (pretty) {
|
||||
const payload = JSON.stringify(result, null, pretty ? 2 : 0)
|
||||
return c.text(payload, 200, {
|
||||
'Content-Type': 'application/json',
|
||||
})
|
||||
} else {
|
||||
return c.json(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface GraphQLParams {
|
||||
query: string | null
|
||||
variables: { readonly [name: string]: unknown } | null
|
||||
operationName: string | null
|
||||
raw: boolean
|
||||
}
|
||||
|
||||
export const getGraphQLParams = async (request: Request): Promise<GraphQLParams> => {
|
||||
const urlData = new URLSearchParams(request.url.split('?')[1])
|
||||
const bodyData = await parseBody(request)
|
||||
|
||||
// GraphQL Query string.
|
||||
let query = urlData.get('query') ?? (bodyData.query as string | null)
|
||||
|
||||
if (typeof query !== 'string') {
|
||||
query = null
|
||||
}
|
||||
|
||||
// Parse the variables if needed.
|
||||
let variables = (urlData.get('variables') ?? bodyData.variables) as {
|
||||
readonly [name: string]: unknown
|
||||
} | null
|
||||
if (typeof variables === 'string') {
|
||||
try {
|
||||
variables = JSON.parse(variables)
|
||||
} catch {
|
||||
throw Error('Variables are invalid JSON.')
|
||||
}
|
||||
} else if (typeof variables !== 'object') {
|
||||
variables = null
|
||||
}
|
||||
|
||||
// Name of GraphQL operation to execute.
|
||||
let operationName = urlData.get('operationName') ?? (bodyData.operationName as string | null)
|
||||
if (typeof operationName !== 'string') {
|
||||
operationName = null
|
||||
}
|
||||
|
||||
const raw = urlData.get('raw') != null || bodyData.raw !== undefined
|
||||
|
||||
const params: GraphQLParams = {
|
||||
query: query,
|
||||
variables: variables,
|
||||
operationName: operationName,
|
||||
raw: raw,
|
||||
}
|
||||
|
||||
return params
|
||||
}
|
||||
|
||||
export const errorMessages = (
|
||||
messages: string[],
|
||||
graphqlErrors?: readonly GraphQLError[] | readonly GraphQLFormattedError[]
|
||||
) => {
|
||||
if (graphqlErrors) {
|
||||
return {
|
||||
errors: graphqlErrors,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
errors: messages.map((message) => {
|
||||
return {
|
||||
message: message,
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// export const graphiQLResponse = () => {}
|
|
@ -0,0 +1,61 @@
|
|||
import { parseBody } from './parse-body'
|
||||
|
||||
describe('parseBody', () => {
|
||||
it('Should return a blank JSON object', async () => {
|
||||
const req = new Request('http://localhost/graphql')
|
||||
const res = await parseBody(req)
|
||||
expect(res).toEqual({})
|
||||
})
|
||||
|
||||
it('With Content-Type: application/graphql', async () => {
|
||||
const req = new Request('http://localhost/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/graphql',
|
||||
},
|
||||
body: 'query { hello }',
|
||||
})
|
||||
const res = await parseBody(req)
|
||||
expect(res).toEqual({ query: 'query { hello }' })
|
||||
})
|
||||
|
||||
it('With Content-Type: application/json', async () => {
|
||||
const variables = JSON.stringify({ who: 'Dolly' })
|
||||
const req = new Request('http://localhost/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
query: 'query { hello }',
|
||||
variables: variables,
|
||||
}),
|
||||
})
|
||||
const res = await parseBody(req)
|
||||
expect(res).toEqual({
|
||||
query: 'query { hello }',
|
||||
variables: variables,
|
||||
})
|
||||
})
|
||||
|
||||
it('With Content-Type: application/x-www-form-urlencoded', async () => {
|
||||
const variables = JSON.stringify({ who: 'Dolly' })
|
||||
|
||||
const searchParams = new URLSearchParams()
|
||||
searchParams.append('query', 'query { hello }')
|
||||
searchParams.append('variables', variables)
|
||||
const req = new Request('http://localhost/graphql', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: searchParams.toString(),
|
||||
})
|
||||
const res = await parseBody(req)
|
||||
|
||||
expect(res).toEqual({
|
||||
query: 'query { hello }',
|
||||
variables: variables,
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,29 @@
|
|||
export async function parseBody(req: Request): Promise<Record<string, unknown>> {
|
||||
const contentType = req.headers.get('content-type')
|
||||
|
||||
switch (contentType) {
|
||||
case 'application/graphql':
|
||||
return { query: await req.text() }
|
||||
case 'application/json':
|
||||
try {
|
||||
return await req.json()
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
console.error(`${e.stack || e.message}`)
|
||||
}
|
||||
throw Error(`POST body sent invalid JSON: ${e}`)
|
||||
}
|
||||
case 'application/x-www-form-urlencoded':
|
||||
return parseFormURL(req)
|
||||
}
|
||||
|
||||
return {}
|
||||
}
|
||||
|
||||
const parseFormURL = async (req: Request) => {
|
||||
const text = await req.text()
|
||||
const searchParams = new URLSearchParams(text)
|
||||
const res: { [params: string]: string } = {}
|
||||
searchParams.forEach((v, k) => (res[k] = v))
|
||||
return res
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
// @denoify-ignore
|
||||
import { makeThisModuleAnExecutableReplacer, ParsedImportExportStatement } from 'denoify'
|
||||
|
||||
makeThisModuleAnExecutableReplacer(async ({ parsedImportExportStatement }) => {
|
||||
if (parsedImportExportStatement.parsedArgument.nodeModuleName === 'hono') {
|
||||
return ParsedImportExportStatement.stringify({
|
||||
...parsedImportExportStatement,
|
||||
parsedArgument: {
|
||||
type: 'URL',
|
||||
url: 'https://deno.land/x/hono/mod.ts',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return undefined
|
||||
})
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/deno/**/*.ts",
|
||||
"src/**/*.test.ts"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2020",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"moduleResolution": "Node",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"strictNullChecks": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"types": ["jest", "node", "@cloudflare/workers-types"],
|
||||
"rootDir": "./src",
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue