Add 'packages/graphql-server/' from commit 'c7e949b0f93954d44c3c7f15564dd9e3ce8ad709'

git-subtree-dir: packages/graphql-server
git-subtree-mainline: fc30bf65cb
git-subtree-split: c7e949b0f9
pull/29/head
Yusuke Wada 2023-02-04 11:26:47 +09:00
commit 3f6f302864
23 changed files with 7691 additions and 0 deletions

View File

@ -0,0 +1 @@
dist

View File

@ -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' }],
},
})

View File

@ -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

View File

@ -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 }}

View File

@ -0,0 +1,3 @@
node_modules
dist
yarn-error.log

View File

@ -0,0 +1,9 @@
{
"printWidth": 100,
"trailingComma": "es5",
"tabWidth": 2,
"semi": false,
"singleQuote": true,
"jsxSingleQuote": true,
"endOfLine": "lf"
}

View File

@ -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()
```

View File

@ -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.' }],
// })
})
})

View File

@ -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()
```

View File

@ -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 = () => {}

View File

@ -0,0 +1 @@
export * from "./index.ts";

View File

@ -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
}

View File

@ -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!"}}')
})

View File

@ -0,0 +1,7 @@
module.exports = {
testMatch: ['**/test/**/*.+(ts|tsx|js)', '**/src/**/(*.)+(spec|test).+(ts|tsx|js)'],
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
testEnvironment: 'miniflare',
}

View File

@ -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"
}
}

View File

@ -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.' }],
})
})
})

View File

@ -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 = () => {}

View File

@ -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,
})
})
})

View File

@ -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
}

View File

@ -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
})

View File

@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"include": [
"src/**/*.ts"
],
"exclude": [
"src/deno/**/*.ts",
"src/**/*.test.ts"
]
}

View File

@ -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