added api logic, vitest, minimal testing ui
This commit is contained in:
48
api/src/plugins/error-handler.ts
Normal file
48
api/src/plugins/error-handler.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// api/src/plugins/error-handler.ts
|
||||
import type { FastifyError, FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
|
||||
import { ZodError } from "zod";
|
||||
|
||||
function isPrismaError(e: any, code: string) {
|
||||
return e && typeof e === "object" && e.code === code;
|
||||
}
|
||||
|
||||
export function installErrorHandler(app: FastifyInstance) {
|
||||
app.setErrorHandler((err: FastifyError & { code?: string; statusCode?: number }, req: FastifyRequest, reply: FastifyReply) => {
|
||||
const requestId = (req as any).id as string | undefined;
|
||||
|
||||
// Respect explicit statusCode + code (e.g., OVERDRAFT_CATEGORY/PLAN)
|
||||
if (err.statusCode && err.code) {
|
||||
return reply.code(err.statusCode).send({ ok: false, code: err.code, message: err.message, requestId });
|
||||
}
|
||||
|
||||
// Zod validation
|
||||
if (err instanceof ZodError) {
|
||||
return reply.code(400).send({
|
||||
ok: false,
|
||||
code: "INVALID_INPUT",
|
||||
message: err.errors.map(e => e.message).join("; "),
|
||||
requestId,
|
||||
});
|
||||
}
|
||||
|
||||
// Prisma common cases
|
||||
if (isPrismaError(err, "P2002")) {
|
||||
return reply.code(409).send({ ok: false, code: "UNIQUE_VIOLATION", message: "Duplicate value violates unique constraint", requestId });
|
||||
}
|
||||
if (isPrismaError(err, "P2003")) {
|
||||
return reply.code(400).send({ ok: false, code: "FK_CONSTRAINT", message: "Foreign key constraint violated", requestId });
|
||||
}
|
||||
|
||||
// 404 produced by handlers
|
||||
if (err.statusCode === 404) {
|
||||
return reply.code(404).send({ ok: false, code: "NOT_FOUND", message: err.message || "Not found", requestId });
|
||||
}
|
||||
|
||||
// Default
|
||||
const status = err.statusCode && err.statusCode >= 400 ? err.statusCode : 500;
|
||||
const code = status >= 500 ? "INTERNAL" : "BAD_REQUEST";
|
||||
// Log full error with request id for correlation
|
||||
app.log.error({ err, requestId }, "Request error");
|
||||
return reply.code(status).send({ ok: false, code, message: err.message || "Unexpected error", requestId });
|
||||
});
|
||||
}
|
||||
15
api/src/plugins/request-id.ts
Normal file
15
api/src/plugins/request-id.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
// api/src/plugins/request-id.ts
|
||||
import type { FastifyPluginCallback } from "fastify";
|
||||
import { randomUUID } from "crypto";
|
||||
|
||||
const requestIdPlugin: FastifyPluginCallback = (app, _opts, done) => {
|
||||
app.addHook("onRequest", async (req, reply) => {
|
||||
const incoming = (req.headers["x-request-id"] as string | undefined)?.trim();
|
||||
const id = incoming && incoming.length > 0 ? incoming : randomUUID();
|
||||
(req as any).id = id; // attach to request
|
||||
reply.header("x-request-id", id); // echo on response
|
||||
});
|
||||
done();
|
||||
};
|
||||
|
||||
export default requestIdPlugin;
|
||||
23
api/src/plugins/user-stub.ts
Normal file
23
api/src/plugins/user-stub.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import fp from "fastify-plugin";
|
||||
import { prisma } from "../prisma.js";
|
||||
|
||||
declare module "fastify" {
|
||||
interface FastifyRequest {
|
||||
userId: string;
|
||||
}
|
||||
}
|
||||
|
||||
export default fp(async (app) => {
|
||||
app.addHook("onRequest", async (req) => {
|
||||
// Dev-only stub: use header if provided, else default
|
||||
const hdr = req.headers["x-user-id"];
|
||||
req.userId = typeof hdr === "string" && hdr.trim() ? hdr.trim() : "demo-user-1";
|
||||
|
||||
// Ensure the user exists (avoids FK P2003 on first write)
|
||||
await prisma.user.upsert({
|
||||
where: { id: req.userId },
|
||||
update: {},
|
||||
create: { id: req.userId, email: `${req.userId}@demo.local` },
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user