added api logic, vitest, minimal testing ui

This commit is contained in:
2025-11-15 23:26:57 -06:00
parent f4160b91db
commit 4eae966f96
95 changed files with 14155 additions and 469 deletions

View 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 });
});
}

View 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;

View 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` },
});
});
});