fix(ci): enable deno and fix the pipeline
commit
9da7fcd238
|
@ -13,7 +13,7 @@ jobs:
|
|||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 16.x
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn
|
||||
- run: yarn build
|
||||
- run: yarn test
|
||||
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
# GraphQL Server Middleware
|
||||
|
||||
## Requirements
|
||||
|
||||
This middleware depends on [GraphQL.js](https://www.npmjs.com/package/graphql).
|
||||
|
||||
```plain
|
||||
npm i graphql
|
||||
```
|
||||
|
||||
or
|
||||
|
||||
```plain
|
||||
yarn add graphql
|
||||
```
|
||||
|
||||
## 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 rootValue = {
|
||||
hello: () => 'Hello Hono!',
|
||||
}
|
||||
|
||||
app.use(
|
||||
'/graphql',
|
||||
graphqlServer({
|
||||
schema,
|
||||
rootValue,
|
||||
})
|
||||
)
|
||||
|
||||
app.fire()
|
||||
```
|
|
@ -0,0 +1,573 @@
|
|||
import {
|
||||
buildSchema,
|
||||
GraphQLSchema,
|
||||
GraphQLString,
|
||||
GraphQLObjectType,
|
||||
GraphQLNonNull,
|
||||
} from 'https://cdn.skypack.dev/graphql@16.5.0?dts'
|
||||
import { Hono } from 'https://raw.githubusercontent.com/honojs/hono/v2.0.2/deno_dist/mod.ts'
|
||||
import { errorMessages, graphqlServer } from './index.ts'
|
||||
|
||||
// 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', () => {
|
||||
// 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,
|
||||
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),
|
||||
})
|
||||
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
|
||||
})
|
||||
})
|
||||
|
||||
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,260 @@
|
|||
// Based on the code in the `express-graphql` package.
|
||||
// https://github.com/graphql/express-graphql/blob/main/src/index.ts
|
||||
|
||||
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://raw.githubusercontent.com/honojs/hono/v2.0.2/deno_dist/mod.ts";
|
||||
import type { Next } from "https://raw.githubusercontent.com/honojs/hono/v2.0.2/deno_dist/mod.ts";
|
||||
import { parseBody } from "./parse-body.ts";
|
||||
|
||||
type Options = {
|
||||
schema: GraphQLSchema;
|
||||
rootValue?: unknown;
|
||||
pretty?: boolean;
|
||||
validationRules?: ReadonlyArray<ValidationRule>;
|
||||
// graphiql?: boolean
|
||||
};
|
||||
|
||||
export const graphqlServer = (options: Options) => {
|
||||
const schema = options.schema;
|
||||
const rootValue = options.rootValue;
|
||||
const pretty = options.pretty ?? false;
|
||||
const validationRules = options.validationRules ?? [];
|
||||
// const showGraphiQL = options.graphiql ?? false
|
||||
|
||||
return async (c: Context, next: Next) => {
|
||||
// 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;
|
||||
|
||||
try {
|
||||
result = await execute({
|
||||
schema,
|
||||
document: documentAST,
|
||||
rootValue,
|
||||
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);
|
||||
}
|
||||
|
||||
await next(); // XXX
|
||||
};
|
||||
};
|
||||
|
||||
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,61 @@
|
|||
import { parseBody } from './parse-body.ts'
|
||||
|
||||
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,49 @@
|
|||
import { buildSchema } from "https://cdn.skypack.dev/graphql@16.5.0?dts";
|
||||
import { Hono } from "https://deno.land/x/hono@v2.0.3/mod.ts";
|
||||
import { errorMessages, 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,
|
||||
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!"}}')
|
||||
});
|
Loading…
Reference in New Issue