From 1e930baccbdb4f544d27c7357c70c427a939bbcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Wed, 4 Mar 2026 20:31:18 +0100 Subject: [PATCH] fix: resolve GraphQL request hang in Fastify integration - Pass FastifyRequest/FastifyReply directly to yoga.handleNodeRequestAndResponse per the official graphql-yoga Fastify integration docs. Yoga v5 uses req.body (already parsed by Fastify) when available, avoiding the dead raw stream issue. - Add proper TypeScript generics for server context including db and redis - Wrap sendVerification/sendPasswordReset in try/catch so missing SMTP does not crash register/requestPasswordReset mutations - Fix migrate.ts path resolution to work with both tsx (src/) and compiled (dist/) - Expose postgres:5432 and redis:6379 ports in compose.yml for local dev Co-Authored-By: Claude Sonnet 4.6 --- compose.yml | 4 +++ packages/backend/src/graphql/context.ts | 10 ++++++- .../backend/src/graphql/resolvers/auth.ts | 12 +++++++-- packages/backend/src/index.ts | 27 ++++++------------- packages/backend/src/scripts/migrate.ts | 6 ++++- 5 files changed, 36 insertions(+), 23 deletions(-) diff --git a/compose.yml b/compose.yml index 29c64af..4951774 100644 --- a/compose.yml +++ b/compose.yml @@ -4,6 +4,8 @@ services: image: postgres:16-alpine container_name: sexy_postgres restart: unless-stopped + ports: + - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data environment: @@ -19,6 +21,8 @@ services: image: redis:7-alpine container_name: sexy_redis restart: unless-stopped + ports: + - "6379:6379" volumes: - redis_data:/data command: redis-server --appendonly yes diff --git a/packages/backend/src/graphql/context.ts b/packages/backend/src/graphql/context.ts index c9f2f8d..0d5f1b0 100644 --- a/packages/backend/src/graphql/context.ts +++ b/packages/backend/src/graphql/context.ts @@ -1,10 +1,18 @@ import type { YogaInitialContext } from "graphql-yoga"; +import type { FastifyRequest, FastifyReply } from "fastify"; import type { Context } from "./builder"; import { getSession } from "../lib/auth"; import { db } from "../db/connection"; import { redis } from "../lib/auth"; -export async function buildContext(ctx: YogaInitialContext & { request: Request; reply: unknown; db: typeof db; redis: typeof redis }): Promise { +type ServerContext = { + req: FastifyRequest; + reply: FastifyReply; + db: typeof db; + redis: typeof redis; +}; + +export async function buildContext(ctx: YogaInitialContext & ServerContext): Promise { const request = ctx.request; const cookieHeader = request.headers.get("cookie") || ""; diff --git a/packages/backend/src/graphql/resolvers/auth.ts b/packages/backend/src/graphql/resolvers/auth.ts index 6f0344f..0cc2d12 100644 --- a/packages/backend/src/graphql/resolvers/auth.ts +++ b/packages/backend/src/graphql/resolvers/auth.ts @@ -129,7 +129,11 @@ builder.mutationField("register", (t) => email_verified: false, }); - await sendVerification(args.email, verifyToken); + try { + await sendVerification(args.email, verifyToken); + } catch (e) { + console.warn("Failed to send verification email:", (e as Error).message); + } return true; }, }), @@ -184,7 +188,11 @@ builder.mutationField("requestPasswordReset", (t) => .set({ password_reset_token: token, password_reset_expiry: expiry }) .where(eq(users.id, user[0].id)); - await sendPasswordReset(args.email, token); + try { + await sendPasswordReset(args.email, token); + } catch (e) { + console.warn("Failed to send password reset email:", (e as Error).message); + } return true; }, }), diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 1390743..7e01144 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -1,10 +1,9 @@ -import Fastify from "fastify"; +import Fastify, { FastifyRequest, FastifyReply } from "fastify"; import fastifyCookie from "@fastify/cookie"; import fastifyCors from "@fastify/cors"; import fastifyMultipart from "@fastify/multipart"; import fastifyStatic from "@fastify/static"; import { createYoga } from "graphql-yoga"; -import { Readable } from "stream"; import path from "path"; import { schema } from "./graphql/index"; import { buildContext } from "./graphql/context"; @@ -44,34 +43,24 @@ async function main() { decorateReply: false, }); - const yoga = createYoga({ + const yoga = createYoga<{ req: FastifyRequest; reply: FastifyReply; db: typeof db; redis: typeof redis }>({ schema, context: buildContext, graphqlEndpoint: "/graphql", healthCheckEndpoint: "/health", logging: { - debug: (msg: string) => fastify.log.debug(msg), - info: (msg: string) => fastify.log.info(msg), - warn: (msg: string) => fastify.log.warn(msg), - error: (msg: string) => fastify.log.error(msg), + debug: (...args) => args.forEach((arg) => fastify.log.debug(arg)), + info: (...args) => args.forEach((arg) => fastify.log.info(arg)), + warn: (...args) => args.forEach((arg) => fastify.log.warn(arg)), + error: (...args) => args.forEach((arg) => fastify.log.error(arg)), }, }); fastify.route({ url: "/graphql", method: ["GET", "POST", "OPTIONS"], - handler: async (request, reply) => { - const response = await yoga.handleNodeRequestAndResponse( - request.raw, - reply.raw, - { reply, db, redis }, - ); - reply.status(response.status); - for (const [key, value] of response.headers.entries()) { - reply.header(key, value); - } - return reply.send(Readable.from(response.body)); - }, + handler: (req, reply) => + yoga.handleNodeRequestAndResponse(req, reply, { req, reply, db, redis }), }); fastify.get("/health", async (_request, reply) => { diff --git a/packages/backend/src/scripts/migrate.ts b/packages/backend/src/scripts/migrate.ts index 6d666d5..8083c30 100644 --- a/packages/backend/src/scripts/migrate.ts +++ b/packages/backend/src/scripts/migrate.ts @@ -11,7 +11,11 @@ const db = drizzle(pool); async function main() { console.log("Running schema migrations..."); - const migrationsFolder = path.join(__dirname, "../../migrations"); + // In dev (tsx): __dirname = src/scripts → migrations are at src/migrations + // In prod (node dist): __dirname = dist/scripts → migrations are at ../../migrations (package root) + const migrationsFolder = __dirname.includes("/src/") + ? path.join(__dirname, "../migrations") + : path.join(__dirname, "../../migrations"); await migrate(db, { migrationsFolder }); console.log("Schema migrations complete."); await pool.end();