generated from Dicken/dickendock
Commit 4: Kategorien-API (CRUD + Reorder) und Favoriten-Toggle im Frontend
This commit is contained in:
101
apps/backend/src/db/repositories/categories.ts
Normal file
101
apps/backend/src/db/repositories/categories.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { eq } from "drizzle-orm";
|
||||
import type {
|
||||
Category,
|
||||
CategoryCreateInput,
|
||||
CategoryReorderInput,
|
||||
CategoryUpdateInput,
|
||||
} from "@launchpad/shared";
|
||||
import { db } from "../client.js";
|
||||
import { categories, services } from "../schema.js";
|
||||
|
||||
function nowIso(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function mapRow(row: typeof categories.$inferSelect): Category {
|
||||
return { id: row.id, name: row.name, order: row.order };
|
||||
}
|
||||
|
||||
export function listCategories(): Category[] {
|
||||
return db
|
||||
.select()
|
||||
.from(categories)
|
||||
.all()
|
||||
.map(mapRow)
|
||||
.sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
export function getCategory(id: string): Category | null {
|
||||
const row = db.select().from(categories).where(eq(categories.id, id)).get();
|
||||
return row ? mapRow(row) : null;
|
||||
}
|
||||
|
||||
export function createCategory(input: CategoryCreateInput): Category {
|
||||
const id = randomUUID();
|
||||
const timestamp = nowIso();
|
||||
const existing = listCategories();
|
||||
const nextOrder = existing.length > 0 ? Math.max(...existing.map((c) => c.order)) + 1 : 0;
|
||||
|
||||
db.insert(categories)
|
||||
.values({
|
||||
id,
|
||||
name: input.name,
|
||||
order: nextOrder,
|
||||
createdAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
})
|
||||
.run();
|
||||
|
||||
return getCategory(id)!;
|
||||
}
|
||||
|
||||
export function updateCategory(id: string, input: CategoryUpdateInput): Category | null {
|
||||
const existing = getCategory(id);
|
||||
if (!existing) return null;
|
||||
|
||||
db.update(categories)
|
||||
.set({
|
||||
...(input.name !== undefined && { name: input.name }),
|
||||
updatedAt: nowIso(),
|
||||
})
|
||||
.where(eq(categories.id, id))
|
||||
.run();
|
||||
|
||||
return getCategory(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt die Reihenfolge mehrerer Kategorien in einer Transaktion neu,
|
||||
* z. B. nach Drag & Drop in der Admin-UI (siehe Commit 6).
|
||||
*/
|
||||
export function reorderCategories(input: CategoryReorderInput): Category[] {
|
||||
db.transaction((tx) => {
|
||||
for (const entry of input) {
|
||||
tx.update(categories)
|
||||
.set({ order: entry.order, updatedAt: nowIso() })
|
||||
.where(eq(categories.id, entry.id))
|
||||
.run();
|
||||
}
|
||||
});
|
||||
|
||||
return listCategories();
|
||||
}
|
||||
|
||||
/**
|
||||
* Löscht eine Kategorie. Dienste, die dieser Kategorie zugeordnet waren,
|
||||
* verlieren ihre Kategoriezuordnung (category wird auf null gesetzt),
|
||||
* werden aber nicht selbst gelöscht.
|
||||
*/
|
||||
export function deleteCategory(id: string): boolean {
|
||||
const category = getCategory(id);
|
||||
if (!category) return false;
|
||||
|
||||
db.update(services)
|
||||
.set({ category: null, updatedAt: nowIso() })
|
||||
.where(eq(services.category, category.name))
|
||||
.run();
|
||||
|
||||
const result = db.delete(categories).where(eq(categories.id, id)).run();
|
||||
return result.changes > 0;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { ensureSchema } from "./db/client.js";
|
||||
import { healthRoutes } from "./routes/health.js";
|
||||
import { deviceRoutes } from "./routes/devices.js";
|
||||
import { serviceRoutes } from "./routes/services.js";
|
||||
import { categoryRoutes } from "./routes/categories.js";
|
||||
|
||||
const PORT = Number(process.env.PORT ?? 3001);
|
||||
const HOST = process.env.HOST ?? "0.0.0.0";
|
||||
@@ -28,6 +29,7 @@ async function main() {
|
||||
await app.register(healthRoutes);
|
||||
await app.register(deviceRoutes);
|
||||
await app.register(serviceRoutes);
|
||||
await app.register(categoryRoutes);
|
||||
|
||||
app.get("/", async () => {
|
||||
return { name: "LaunchPad API", status: "running" };
|
||||
|
||||
53
apps/backend/src/routes/categories.ts
Normal file
53
apps/backend/src/routes/categories.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { FastifyInstance } from "fastify";
|
||||
import {
|
||||
CategoryCreateSchema,
|
||||
CategoryReorderSchema,
|
||||
CategoryUpdateSchema,
|
||||
} from "@launchpad/shared";
|
||||
import * as categoryRepo from "../db/repositories/categories.js";
|
||||
|
||||
export async function categoryRoutes(app: FastifyInstance): Promise<void> {
|
||||
app.get("/api/categories", async () => {
|
||||
return categoryRepo.listCategories();
|
||||
});
|
||||
|
||||
app.post("/api/categories", async (request, reply) => {
|
||||
const parsed = CategoryCreateSchema.safeParse(request.body);
|
||||
if (!parsed.success) {
|
||||
return reply.code(400).send({ error: "Ungültige Eingabe", issues: parsed.error.issues });
|
||||
}
|
||||
const category = categoryRepo.createCategory(parsed.data);
|
||||
return reply.code(201).send(category);
|
||||
});
|
||||
|
||||
// Muss vor der /:id-Route registriert sein, damit "reorder" nicht als ID interpretiert wird.
|
||||
app.patch("/api/categories/reorder", async (request, reply) => {
|
||||
const parsed = CategoryReorderSchema.safeParse(request.body);
|
||||
if (!parsed.success) {
|
||||
return reply.code(400).send({ error: "Ungültige Eingabe", issues: parsed.error.issues });
|
||||
}
|
||||
return categoryRepo.reorderCategories(parsed.data);
|
||||
});
|
||||
|
||||
app.patch("/api/categories/:id", async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const parsed = CategoryUpdateSchema.safeParse(request.body);
|
||||
if (!parsed.success) {
|
||||
return reply.code(400).send({ error: "Ungültige Eingabe", issues: parsed.error.issues });
|
||||
}
|
||||
const category = categoryRepo.updateCategory(id, parsed.data);
|
||||
if (!category) {
|
||||
return reply.code(404).send({ error: "Kategorie nicht gefunden" });
|
||||
}
|
||||
return category;
|
||||
});
|
||||
|
||||
app.delete("/api/categories/:id", async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const deleted = categoryRepo.deleteCategory(id);
|
||||
if (!deleted) {
|
||||
return reply.code(404).send({ error: "Kategorie nicht gefunden" });
|
||||
}
|
||||
return reply.code(204).send();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user