Refactor code structure for improved readability and maintainability

This commit is contained in:
Daniel Volz
2025-12-20 20:48:23 +01:00
parent 4c351aae2d
commit a0e879e8d2
9 changed files with 1982 additions and 15 deletions
+1
View File
@@ -35,6 +35,7 @@ async function main() {
every_json text NOT NULL DEFAULT '[]',
start_json text NOT NULL DEFAULT '[]',
strip_size integer NOT NULL DEFAULT 1,
image_url text,
updated_at integer NOT NULL DEFAULT (strftime('%s','now'))
);
+1
View File
@@ -23,6 +23,7 @@ export const medications = sqliteTable("medications", {
everyJson: text("every_json").notNull().default("[]"),
startJson: text("start_json").notNull().default("[]"),
stripSize: integer("strip_size").notNull().default(1),
imageUrl: text("image_url"),
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull().default(sql`CURRENT_TIMESTAMP`),
});
+16
View File
@@ -5,6 +5,10 @@ import rateLimit from "@fastify/rate-limit";
import sensible from "@fastify/sensible";
import cookie, { CookieSerializeOptions } from "@fastify/cookie";
import jwt from "@fastify/jwt";
import fastifyMultipart from "@fastify/multipart";
import fastifyStatic from "@fastify/static";
import { resolve } from "path";
import { existsSync, mkdirSync } from "fs";
import { env } from "./plugins/env.js";
import { healthRoutes } from "./routes/health.js";
import { authRoutes } from "./routes/auth.js";
@@ -13,6 +17,12 @@ import { settingsRoutes } from "./routes/settings.js";
import { plannerRoutes } from "./routes/planner.js";
import { startReminderScheduler } from "./services/reminder-scheduler.js";
// Ensure images directory exists
const imagesDir = resolve(process.cwd(), "data/images");
if (!existsSync(imagesDir)) {
mkdirSync(imagesDir, { recursive: true });
}
const app = Fastify({
logger: {
level: env.LOG_LEVEL,
@@ -55,6 +65,12 @@ await app.register(rateLimit, {
});
await app.register(cookie, { secret: env.COOKIE_SECRET });
await app.register(jwt, { secret: env.JWT_SECRET, cookie: { cookieName: "access_token", signed: false } });
await app.register(fastifyMultipart, { limits: { fileSize: 2 * 1024 * 1024 } }); // 2MB limit
await app.register(fastifyStatic, {
root: imagesDir,
prefix: "/images/",
decorateReply: false,
});
await app.register(healthRoutes);
await app.register(authRoutes);
+65
View File
@@ -3,6 +3,11 @@ import { z } from "zod";
import { db } from "../db/client.js";
import { medications } from "../db/schema.js";
import { eq } from "drizzle-orm";
import { createWriteStream, existsSync, unlinkSync } from "fs";
import { resolve, extname } from "path";
import { pipeline } from "stream/promises";
const IMAGES_DIR = resolve(process.cwd(), "data/images");
const sliceSchema = z.object({
usage: z.number().nonnegative(),
@@ -54,6 +59,7 @@ export async function medicationRoutes(app: FastifyInstance) {
tabsPerStrip: row.tabsPerStrip ?? row.stripSize ?? 1,
looseTablets: row.looseTablets ?? 0,
slices: parseSlices(row),
imageUrl: row.imageUrl,
updatedAt: row.updatedAt,
}));
});
@@ -97,6 +103,7 @@ export async function medicationRoutes(app: FastifyInstance) {
tabsPerStrip: inserted.tabsPerStrip,
looseTablets: inserted.looseTablets,
slices,
imageUrl: inserted.imageUrl,
updatedAt: inserted.updatedAt,
};
});
@@ -146,6 +153,7 @@ export async function medicationRoutes(app: FastifyInstance) {
tabsPerStrip: result[0].tabsPerStrip,
looseTablets: result[0].looseTablets,
slices,
imageUrl: result[0].imageUrl,
updatedAt: result[0].updatedAt,
};
});
@@ -154,11 +162,68 @@ export async function medicationRoutes(app: FastifyInstance) {
const idNum = Number(req.params.id);
if (Number.isNaN(idNum)) return reply.badRequest("Invalid id");
// Delete associated image if exists
const [existing] = await db.select().from(medications).where(eq(medications.id, idNum));
if (existing?.imageUrl) {
const imagePath = resolve(IMAGES_DIR, existing.imageUrl);
if (existsSync(imagePath)) unlinkSync(imagePath);
}
const deleted = await db.delete(medications).where(eq(medications.id, idNum)).returning();
if (!deleted.length) return reply.notFound();
return reply.status(204).send();
});
// Upload medication image
app.post<{ Params: { id: string } }>("/medications/:id/image", async (req, reply) => {
const idNum = Number(req.params.id);
if (Number.isNaN(idNum)) return reply.badRequest("Invalid id");
const [existing] = await db.select().from(medications).where(eq(medications.id, idNum));
if (!existing) return reply.notFound();
const data = await req.file();
if (!data) return reply.badRequest("No file uploaded");
const allowedTypes = ["image/jpeg", "image/png", "image/webp", "image/gif"];
if (!allowedTypes.includes(data.mimetype)) {
return reply.badRequest("Invalid file type. Allowed: JPEG, PNG, WebP, GIF");
}
const ext = extname(data.filename) || ".jpg";
const filename = `med-${idNum}-${Date.now()}${ext}`;
const filepath = resolve(IMAGES_DIR, filename);
await pipeline(data.file, createWriteStream(filepath));
// Delete old image if exists
if (existing.imageUrl) {
const oldPath = resolve(IMAGES_DIR, existing.imageUrl);
if (existsSync(oldPath)) unlinkSync(oldPath);
}
await db.update(medications).set({ imageUrl: filename, updatedAt: new Date() }).where(eq(medications.id, idNum));
return { success: true, imageUrl: filename };
});
// Delete medication image
app.delete<{ Params: { id: string } }>("/medications/:id/image", async (req, reply) => {
const idNum = Number(req.params.id);
if (Number.isNaN(idNum)) return reply.badRequest("Invalid id");
const [existing] = await db.select().from(medications).where(eq(medications.id, idNum));
if (!existing) return reply.notFound();
if (existing.imageUrl) {
const filepath = resolve(IMAGES_DIR, existing.imageUrl);
if (existsSync(filepath)) unlinkSync(filepath);
}
await db.update(medications).set({ imageUrl: null, updatedAt: new Date() }).where(eq(medications.id, idNum));
return reply.status(204).send();
});
app.post("/medications/usage", async (req, reply) => {
const schema = z.object({ startDate: z.string().datetime(), endDate: z.string().datetime() });
const parsed = schema.safeParse(req.body);