honojs-middleware/packages/graphql-server/src/index.ts

235 lines
6.2 KiB
TypeScript
Raw Normal View History

2022-07-21 08:43:43 +08:00
import {
Source,
parse,
execute,
validateSchema,
validate,
specifiedRules,
getOperationAST,
GraphQLError,
2022-07-30 21:36:01 +08:00
} from 'graphql'
2022-07-21 08:43:43 +08:00
import type {
GraphQLSchema,
DocumentNode,
ValidationRule,
FormattedExecutionResult,
GraphQLFormattedError,
2022-07-30 21:36:01 +08:00
} from 'graphql'
2022-07-21 08:43:43 +08:00
2022-07-30 21:36:01 +08:00
import type { Context } from 'hono'
import { parseBody } from './parse-body'
2022-07-21 08:43:43 +08:00
export type RootResolver = (ctx?: Context) => Promise<unknown> | unknown
2022-07-21 08:43:43 +08:00
type Options = {
2022-07-30 21:36:01 +08:00
schema: GraphQLSchema
2023-02-04 10:53:27 +08:00
rootResolver?: RootResolver
2022-07-30 21:36:01 +08:00
pretty?: boolean
validationRules?: ReadonlyArray<ValidationRule>
2022-07-21 08:43:43 +08:00
// graphiql?: boolean
2022-07-30 21:36:01 +08:00
}
2022-07-21 08:43:43 +08:00
export const graphqlServer = (options: Options) => {
2022-07-30 21:36:01 +08:00
const schema = options.schema
const pretty = options.pretty ?? false
const validationRules = options.validationRules ?? []
2022-07-21 08:43:43 +08:00
// const showGraphiQL = options.graphiql ?? false
return async (c: Context) => {
2022-07-21 08:43:43 +08:00
// GraphQL HTTP only supports GET and POST methods.
2022-07-30 21:36:01 +08:00
if (c.req.method !== 'GET' && c.req.method !== 'POST') {
return c.json(errorMessages(['GraphQL only supports GET and POST requests.']), 405, {
Allow: 'GET, POST',
})
2022-07-21 08:43:43 +08:00
}
2022-07-30 21:36:01 +08:00
let params: GraphQLParams
2022-07-21 08:43:43 +08:00
try {
2022-07-30 21:36:01 +08:00
params = await getGraphQLParams(c.req)
2022-07-21 08:43:43 +08:00
} catch (e) {
if (e instanceof Error) {
2022-07-30 21:36:01 +08:00
console.error(`${e.stack || e.message}`)
return c.json(errorMessages([e.message], [e]), 400)
2022-07-21 08:43:43 +08:00
}
2022-07-30 21:36:01 +08:00
throw e
2022-07-21 08:43:43 +08:00
}
2022-07-30 21:36:01 +08:00
const { query, variables, operationName } = params
2022-07-21 08:43:43 +08:00
if (query == null) {
2022-07-30 21:36:01 +08:00
return c.json(errorMessages(['Must provide query string.']), 400)
2022-07-21 08:43:43 +08:00
}
2022-07-30 21:36:01 +08:00
const schemaValidationErrors = validateSchema(schema)
2022-07-21 08:43:43 +08:00
if (schemaValidationErrors.length > 0) {
// Return 500: Internal Server Error if invalid schema.
return c.json(
2022-07-30 21:36:01 +08:00
errorMessages(['GraphQL schema validation error.'], schemaValidationErrors),
2022-07-21 08:43:43 +08:00
500
2022-07-30 21:36:01 +08:00
)
2022-07-21 08:43:43 +08:00
}
2022-07-30 21:36:01 +08:00
let documentAST: DocumentNode
2022-07-21 08:43:43 +08:00
try {
2022-07-30 21:36:01 +08:00
documentAST = parse(new Source(query, 'GraphQL request'))
2022-07-21 08:43:43 +08:00
} catch (syntaxError: unknown) {
// Return 400: Bad Request if any syntax errors errors exist.
if (syntaxError instanceof Error) {
2022-07-30 21:36:01 +08:00
console.error(`${syntaxError.stack || syntaxError.message}`)
2022-07-21 08:43:43 +08:00
const e = new GraphQLError(syntaxError.message, {
originalError: syntaxError,
2022-07-30 21:36:01 +08:00
})
return c.json(errorMessages(['GraphQL syntax error.'], [e]), 400)
2022-07-21 08:43:43 +08:00
}
2022-07-30 21:36:01 +08:00
throw syntaxError
2022-07-21 08:43:43 +08:00
}
// Validate AST, reporting any errors.
2022-07-30 21:36:01 +08:00
const validationErrors = validate(schema, documentAST, [...specifiedRules, ...validationRules])
2022-07-21 08:43:43 +08:00
if (validationErrors.length > 0) {
// Return 400: Bad Request if any validation errors exist.
2022-07-30 21:36:01 +08:00
return c.json(errorMessages(['GraphQL validation error.'], validationErrors), 400)
2022-07-21 08:43:43 +08:00
}
2022-07-30 21:36:01 +08:00
if (c.req.method === 'GET') {
2022-07-21 08:43:43 +08:00
// Determine if this GET request will perform a non-query.
2022-07-30 21:36:01 +08:00
const operationAST = getOperationAST(documentAST, operationName)
if (operationAST && operationAST.operation !== 'query') {
2022-07-21 08:43:43 +08:00
/*
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,
2022-07-30 21:36:01 +08:00
{ Allow: 'POST' }
)
2022-07-21 08:43:43 +08:00
}
}
2022-07-30 21:36:01 +08:00
let result: FormattedExecutionResult
const { rootResolver } = options
2022-07-21 08:43:43 +08:00
try {
result = await execute({
schema,
document: documentAST,
rootValue: rootResolver ? await rootResolver(c) : null,
2022-07-21 08:43:43 +08:00
variableValues: variables,
operationName: operationName,
2022-07-30 21:36:01 +08:00
})
2022-07-21 08:43:43 +08:00
} catch (contextError: unknown) {
if (contextError instanceof Error) {
2022-07-30 21:36:01 +08:00
console.error(`${contextError.stack || contextError.message}`)
2022-07-21 08:43:43 +08:00
const e = new GraphQLError(contextError.message, {
originalError: contextError,
nodes: documentAST,
2022-07-30 21:36:01 +08:00
})
2022-07-21 08:43:43 +08:00
// Return 400: Bad Request if any execution context errors exist.
2022-07-30 21:36:01 +08:00
return c.json(errorMessages(['GraphQL execution context error.'], [e]), 400)
2022-07-21 08:43:43 +08:00
}
2022-07-30 21:36:01 +08:00
throw contextError
2022-07-21 08:43:43 +08:00
}
if (!result.data) {
if (result.errors) {
2022-07-30 21:36:01 +08:00
return c.json(errorMessages([result.errors.toString()], result.errors), 500)
2022-07-21 08:43:43 +08:00
}
}
/*
Now, does not support GraphiQL
if (showGraphiQL) {
}
*/
if (pretty) {
2022-07-30 21:36:01 +08:00
const payload = JSON.stringify(result, null, pretty ? 2 : 0)
2022-07-21 08:43:43 +08:00
return c.text(payload, 200, {
2022-07-30 21:36:01 +08:00
'Content-Type': 'application/json',
})
2022-07-21 08:43:43 +08:00
} else {
2022-07-30 21:36:01 +08:00
return c.json(result)
2022-07-21 08:43:43 +08:00
}
2022-07-30 21:36:01 +08:00
}
}
2022-07-21 08:43:43 +08:00
export interface GraphQLParams {
2022-07-30 21:36:01 +08:00
query: string | null
variables: { readonly [name: string]: unknown } | null
operationName: string | null
raw: boolean
2022-07-21 08:43:43 +08:00
}
2022-07-30 21:36:01 +08:00
export const getGraphQLParams = async (request: Request): Promise<GraphQLParams> => {
const urlData = new URLSearchParams(request.url.split('?')[1])
const bodyData = await parseBody(request)
2022-07-21 08:43:43 +08:00
// GraphQL Query string.
2022-07-30 21:36:01 +08:00
let query = urlData.get('query') ?? (bodyData.query as string | null)
2022-07-21 08:43:43 +08:00
2022-07-30 21:36:01 +08:00
if (typeof query !== 'string') {
query = null
2022-07-21 08:43:43 +08:00
}
// Parse the variables if needed.
2022-07-30 21:36:01 +08:00
let variables = (urlData.get('variables') ?? bodyData.variables) as {
readonly [name: string]: unknown
} | null
if (typeof variables === 'string') {
2022-07-21 08:43:43 +08:00
try {
2022-07-30 21:36:01 +08:00
variables = JSON.parse(variables)
2022-07-21 08:43:43 +08:00
} catch {
2022-07-30 21:36:01 +08:00
throw Error('Variables are invalid JSON.')
2022-07-21 08:43:43 +08:00
}
2022-07-30 21:36:01 +08:00
} else if (typeof variables !== 'object') {
variables = null
2022-07-21 08:43:43 +08:00
}
// Name of GraphQL operation to execute.
2022-07-30 21:36:01 +08:00
let operationName = urlData.get('operationName') ?? (bodyData.operationName as string | null)
if (typeof operationName !== 'string') {
operationName = null
2022-07-21 08:43:43 +08:00
}
2022-07-30 21:36:01 +08:00
const raw = urlData.get('raw') != null || bodyData.raw !== undefined
2022-07-21 08:43:43 +08:00
const params: GraphQLParams = {
query: query,
variables: variables,
operationName: operationName,
raw: raw,
2022-07-30 21:36:01 +08:00
}
2022-07-21 08:43:43 +08:00
2022-07-30 21:36:01 +08:00
return params
}
2022-07-21 08:43:43 +08:00
export const errorMessages = (
messages: string[],
graphqlErrors?: readonly GraphQLError[] | readonly GraphQLFormattedError[]
) => {
if (graphqlErrors) {
return {
errors: graphqlErrors,
2022-07-30 21:36:01 +08:00
}
2022-07-21 08:43:43 +08:00
}
return {
errors: messages.map((message) => {
return {
message: message,
2022-07-30 21:36:01 +08:00
}
2022-07-21 08:43:43 +08:00
}),
2022-07-30 21:36:01 +08:00
}
}
2022-07-21 08:43:43 +08:00
// export const graphiQLResponse = () => {}