Last active
February 10, 2026 13:50
-
-
Save litewarp/886159c83a43f8f22d3baeba38e1da23 to your computer and use it in GitHub Desktop.
Graphile Hono Middleware Adapter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { transformToRelayResponse } from './transformer.js'; | |
| import type { HonoEnv } from './types.js'; | |
| // oxlint-disable-next-line unicorn/require-module-specifiers | |
| import type {} from '@grafserv/persisted'; | |
| import type { StreamingApi } from 'hono/utils/stream'; | |
| import { mapJwtClaimsToPgSettings } from '@repo/auth'; | |
| import { getPreset } from '@repo/graphile-preset'; | |
| import { schema } from '@repo/graphile-schema'; | |
| import { grafast } from 'grafast'; | |
| import { resolvePreset } from 'graphile-config'; | |
| import { createMiddleware } from 'hono/factory'; | |
| import { HTTPException } from 'hono/http-exception'; | |
| import { stream } from 'hono/streaming'; | |
| import * as z from 'zod/mini'; | |
| export function isAsyncIterable( | |
| maybeAsyncIterable: any | |
| ): maybeAsyncIterable is AsyncIterable<unknown> { | |
| return typeof maybeAsyncIterable?.[Symbol.asyncIterator] === 'function'; | |
| } | |
| const preset = getPreset({ | |
| connectionString: process.env.POSTGRES_URL || '', | |
| pgSettings: (ctx) => { | |
| if (!ctx.honov4) { | |
| throw new Error('Hono v4 context not found'); | |
| } | |
| const jwt = ctx.honov4.ctx.get('jwt'); | |
| return jwt ? mapJwtClaimsToPgSettings(jwt) : {}; | |
| } | |
| }); | |
| const resolvedPreset = resolvePreset(preset); | |
| const operations = resolvedPreset.grafserv?.persistedOperations ?? {}; | |
| const isValidRequest = ( | |
| obj: unknown | |
| ): obj is { documentId: string; variables?: Record<string, unknown> } => { | |
| const schema = z.object({ | |
| documentId: z.string().check(z.minLength(8, 'Invalid Hash')), | |
| variables: z.optional(z.record(z.string(), z.unknown())) | |
| }); | |
| return schema.safeParse(obj).success; | |
| }; | |
| export const handler = createMiddleware<HonoEnv, '/graphql'>(async (ctx) => { | |
| const body = await ctx.req.json(); | |
| if (!isValidRequest(body)) { | |
| throw new HTTPException(400); | |
| } | |
| const { documentId, variables: variableValues } = body; | |
| const source = operations[documentId]; | |
| if (!source) { | |
| throw new HTTPException(400, { message: 'Operation not found' }); | |
| } | |
| const result = await grafast({ | |
| schema, | |
| source, | |
| variableValues, | |
| resolvedPreset, | |
| onError: 'NULL', | |
| requestContext: { honov4: { ctx } } | |
| }); | |
| try { | |
| if (!isAsyncIterable(result)) { | |
| // Return a promise if there's no iterator | |
| return ctx.json(transformToRelayResponse(result)); | |
| } else { | |
| ctx.header('Content-Type', 'multipart/mixed; boundary="-"'); | |
| return stream(ctx, async (stream) => { | |
| const encoder = new TextEncoder(); | |
| let count = 0; | |
| for await (const payload of result) { | |
| // TODO: determine if needed | |
| if (count === 0) { | |
| void stream.write(encoder.encode('\r\n')); | |
| void stream.write(encoder.encode('---')); | |
| } | |
| count++; | |
| const transformed = transformToRelayResponse(payload); | |
| writePartialResult(transformed, encoder, stream); | |
| if (!payload.hasNext) { | |
| void stream.write('--\r\n'); | |
| } | |
| } | |
| }); | |
| } | |
| } catch (error) { | |
| console.error('Error processing GraphQL request:', error); | |
| throw new HTTPException(500, { message: 'Internal Server Error' }); | |
| } | |
| }); | |
| const writePartialResult = ( | |
| result: ReturnType<typeof transformToRelayResponse>, | |
| encoder: TextEncoder, | |
| writer: StreamingApi | |
| ) => { | |
| void writer.write(encoder.encode('\r\n')); | |
| void writer.write(encoder.encode('Content-Type: application/json; charset=utf-8')); | |
| void writer.write(encoder.encode('\r\n')); | |
| const chunk = JSON.stringify(result); | |
| const encodedChunk = encoder.encode(chunk); | |
| void writer.write(encoder.encode(`Content-Length: ${encodedChunk.byteLength}`)); | |
| void writer.write(encoder.encode('\r\n')); | |
| void writer.write(encoder.encode('\r\n')); | |
| void writer.write(encodedChunk); | |
| void writer.write(encoder.encode('\r\n')); | |
| void writer.write(encoder.encode('---')); | |
| }; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment