Feature-Dateistruktur
Problem
Alles inline in defineFeature() wird schnell unleserlich. Handler, Schemas, Access — alles in einer Datei, 500+ Zeilen.
Loesung: Ein Handler pro Datei
features/jobs/ jobs.feature.ts ← Schlank: Imports + Registrierung (Uebersicht) handlers/ detail.query.ts ← Ein Handler, komplett self-contained list.query.ts start.write.ts stop.write.ts retry.write.ts schemas/ job-run.ts ← Shared Zod Schemas (wiederverwendbar) job-definition.ts tables/ index.ts ← Drizzle Table DefinitionenFeature-Datei: Nur Registrierung
// jobs.feature.ts — 20 Zeilen statt 500import { defineFeature } from "@kumiko/framework";import { detailQuery } from "./handlers/detail.query";import { listQuery } from "./handlers/list.query";import { startWrite } from "./handlers/start.write";import { stopWrite } from "./handlers/stop.write";
export default defineFeature("jobs", (r) => { // Entities r.entity("jobDefinition", { ... }); r.entity("jobRun", { ... });
// Queries const queries = { detail: r.queryHandler(detailQuery), list: r.queryHandler(listQuery), };
// Commands const handlers = { start: r.writeHandler(startWrite), stop: r.writeHandler(stopWrite), };
// Nav + Screens r.nav({ ... }); r.screen({ ... });
return { queries, handlers };});Man sieht sofort: 2 Entities, 2 Queries, 2 Commands, Nav, Screen.
Handler-Datei: Self-Contained
Jeder Handler hat alles was er braucht — Schema, Access, Logic:
import { defineQueryHandler } from "@kumiko/framework";import { z } from "zod";import { jobRunsTable, jobRunLogsTable } from "../tables";import { eq } from "drizzle-orm";
export const detailQuery = defineQueryHandler({ name: "detail", // Kurz — Feature-Prefix automatisch schema: z.object({ runId: z.number() }), access: { roles: [roles.SystemAdmin] }, // Typisierte Rolle handler: async (query, ctx) => { const [row] = await ctx.db // ctx.db statt ctx["db"] .select() .from(jobRunsTable) .where(eq(jobRunsTable.id, query.payload.runId));
if (!row) return null;
const logs = await ctx.db .select() .from(jobRunLogsTable) .where(eq(jobRunLogsTable.runId, query.payload.runId)) .orderBy(jobRunLogsTable.id);
return { ...row, logs }; },});import { defineWriteHandler } from "@kumiko/framework";import { z } from "zod";
export const startWrite = defineWriteHandler({ name: "start", schema: z.object({ jobName: z.string(), payload: z.record(z.unknown()).optional(), }), access: { roles: [roles.SystemAdmin] }, handler: async (event, ctx) => { const jobDef = ctx.jobRegistry.get(event.payload.jobName); if (!jobDef) return { isSuccess: false, error: "job_not_found" };
const runId = await ctx.jobRunner.enqueue( event.payload.jobName, event.payload.payload ?? {}, { triggeredBy: event.user.id }, );
return { isSuccess: true, data: { runId } }; },});Helper: defineWriteHandler / defineQueryHandler
Identity Functions fuer TypeScript-Inferenz. Geben das Objekt typisiert zurueck:
// Framework exportiert:export function defineWriteHandler<TSchema extends ZodType>( def: WriteHandlerDefinition<TSchema>,): WriteHandlerDefinition<TSchema> { return def;}
export function defineQueryHandler<TSchema extends ZodType>( def: QueryHandlerDefinition<TSchema>,): QueryHandlerDefinition<TSchema> { return def;}Kein Magic — nur TypeScript Inference. Autocomplete fuer alle Felder.
Registrar akzeptiert Objekte
r.writeHandler() und r.queryHandler() akzeptieren beide Formen:
// Objekt (neu — aus Handler-Datei):r.writeHandler(startWrite);
// Inline (alt — fuer kleine Handler die keine eigene Datei brauchen):r.writeHandler("toggle", z.object({ id: z.number() }), handler, { access: ... });Inline bleibt fuer triviale Handler (5 Zeilen). Eigene Datei fuer alles andere.
Schemas: Shared und wiederverwendbar
import { z } from "zod";
export const jobRunSchema = z.object({ jobName: z.string(), status: z.string(), payload: z.string().optional(), error: z.string().optional(),});
export type JobRun = z.infer<typeof jobRunSchema>;Schemas koennen von mehreren Handlern importiert werden. Und von Tests.
Zusammenspiel mit Type Safety
Greift perfekt ineinander:
| Aspekt | Wie |
|---|---|
| Feature-Prefix | name: "start" → Framework macht "jobs.start" |
| Rollen | roles.SystemAdmin statt "SystemAdmin" |
| Context | ctx.db statt ctx["db"] (typisiert) |
| Cross-Feature | import { handlers } from "@features/jobs" |
| Return | r.writeHandler(x) gibt typisierte Referenz zurueck |
Vorteile
- Feature-Datei ist eine Uebersicht (20 Zeilen)
- Handler sind self-contained (Schema + Access + Logic)
- Handler sind einzeln testbar (importieren, ausfuehren)
- Handler sind einzeln reviewbar (ein File = ein Handler)
- Schemas sind wiederverwendbar (Handler + Tests + andere Features)
- Inline bleibt moeglich fuer triviale Handler