// 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.4.0?dts' import type { GraphQLSchema, DocumentNode, ValidationRule, FormattedExecutionResult, GraphQLFormattedError, } from 'https://cdn.skypack.dev/graphql@16.4.0?dts' import type { Context } from '../../context.ts' import type { Next } from '../../hono.ts' import { parseBody } from './parse-body.ts' type Options = { schema: GraphQLSchema rootValue?: unknown pretty?: boolean validationRules?: ReadonlyArray // 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 => { 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 = () => {}