Type Safety: Keine Magic Strings
Prinzip
Keine hardcoded Strings als Identifier. Alles typisiert ueber as const und Union Types. Keine Enums — nur as const Objekte und inferierte Types.
1. Rollen
App definiert einmal ihre Rollen. Alle Access-Regeln nutzen diese Referenz.
// App-Level (einmalig):const roles = defineRoles(["Admin", "SystemAdmin", "Driver", "Disponent", "Accounting"] as const);
// Typ wird inferiert:// type AppRole = "Admin" | "SystemAdmin" | "Driver" | "Disponent" | "Accounting"
// Nutzung:access: { roles: [roles.Admin, roles.SystemAdmin] } // ✅ TypeScript prueftaccess: { roles: [roles.Admni] } // ❌ TypeScript Error2. Handler-Namen
r.writeHandler() und r.queryHandler() geben typisierte Referenzen zurueck.
defineFeature("orders", (r) => { const handlers = { create: r.writeHandler("order.create", schema, handler), update: r.writeHandler("order.update", schema, handler), accept: r.writeHandler("order.accept", schema, handler), };
const queries = { list: r.queryHandler("order.list", schema, handler), detail: r.queryHandler("order.detail", schema, handler), };
return { handlers, queries };});
// Im eigenen Feature:await ctx.write(handlers.create, payload); // ✅ Typsicherawait ctx.write(handlers.craete, payload); // ❌ TypeScript Error
// Cross-Feature:import { handlers as orderHandlers } from "@features/orders";await ctx.write(orderHandlers.create, payload); // ✅ Typsicher3. Event-Typen
SharedEvents geben typisierte Referenzen zurueck.
defineFeature("orders", (r) => { const events = { created: r.defineEvent("order.created", z.object({ orderId: z.number() })), updated: r.defineEvent("order.updated", z.object({ orderId: z.number() })), stateChanged: r.defineEvent("order.stateChanged", z.object({ orderId: z.number(), from: z.string(), to: z.string() })), };
// Im Handler: r.writeHandler("order.create", schema, async (event, ctx) => { // ... await ctx.emit(events.created, { orderId: order.id }); // ✅ Payload typisiert });
return { events };});
// Cross-Feature:import { events as orderEvents } from "@features/orders";
r.job("sendNotification", { trigger: { on: orderEvents.stateChanged }, // ✅ Typsicher});4. System Hook Namen
Constants statt Magic Strings.
// Framework exportiert:export const SystemHooks = { searchIndex: "system:search-index", searchRemove: "system:search-remove", sseBroadcast: "system:sse-broadcast", sseDeleteBroadcast: "system:sse-delete-broadcast", auditTrail: "system:audit-trail", auditTrailDelete: "system:audit-trail-delete",} as const;
// Intern:registerSystemHook(SystemHooks.searchIndex, 1000, handler);5. Error Codes
Typisierte Error-Klassen statt Strings (siehe infrastructure.md).
// Framework exportiert Error-Klassen:throw new NotFoundError("order", orderId); // code: "not_found"throw new AccessDeniedError("order.create"); // code: "access_denied"throw new ValidationError([...]); // code: "validation_failed"throw new ConflictError("version_conflict"); // code: "version_conflict"throw new HandlerNotFoundError("order.foo"); // code: "handler_not_found"
// Error codes als const:export const ErrorCodes = { notFound: "not_found", accessDenied: "access_denied", validationFailed: "validation_failed", versionConflict: "version_conflict", handlerNotFound: "handler_not_found", fieldAccessDenied: "field_access_denied",} as const;6. Context Keys
Typisierter PipelineContext statt Magic String Keys.
// Aktuell (schlecht):ctx["_entityName"]ctx["db"]
// Besser: typisierter Contexttype PipelineContext = { readonly db: TenantDb; readonly user: PipelineUser; readonly log: Logger; readonly requestId: string; readonly config: ConfigAccessor; readonly delivery: DeliveryAccessor; readonly entityName?: string;};
// Zugriff ueber Properties, nicht Strings:ctx.dbctx.userctx.entityName7. Route Pfade
Constants statt inline Strings.
export const Routes = { write: "/write", query: "/query", command: "/command", sse: "/sse", files: "/files", auth: "/auth", health: "/health", log: "/log",} as const;
// Server:app.post(Routes.write, writeHandler);app.post(Routes.query, queryHandler);8. Audit Actions
Aus Handler-Referenz ableiten statt doppelt definieren.
// Aktuell (doppelt):// Handler: "user.create"// Audit: "user.create" ← gleicher String, manuell
// Besser: Audit leitet automatisch aus dem Handler ab// WriteHandler "order.create" → Audit Action "orders.order.create" (mit Feature-Prefix)// Kein eigener String noetig9. Standard-Handler Conventions
Standard-Handler werden einzeln registriert. Schema und Body kommen aus
defineEntityWriteHandler / defineEntityQueryHandler, die jeweils
einen Handler generieren — keine Auto-Generierung eines kompletten CRUD-Sets.
r.writeHandler(defineEntityWriteHandler("order:create", orderEntity, { access }));r.writeHandler(defineEntityWriteHandler("order:update", orderEntity, { access }));r.writeHandler(defineEntityWriteHandler("order:delete", orderEntity, { access }));r.queryHandler(defineEntityQueryHandler("order:list", orderEntity, { access }));r.queryHandler(defineEntityQueryHandler("order:detail", orderEntity, { access }));
// Auto-Events vom Executor: order.created / order.updated / order.deleted / order.restored// Domain-Events deklarierst du explizit über r.defineEvent + ctx.appendEvent.10. Message Kinds
export const MessageKind = { write: "write", query: "query", command: "command", shared: "shared", broadcast: "broadcast",} as const;
type MessageKind = typeof MessageKind[keyof typeof MessageKind];Zusammenfassung
| Was | Aktuell | Neu |
|---|---|---|
| Rollen | "Admin" (frei) | roles.Admin (geprueft) |
| Handler | "user.create" (String) | handlers.create (typisiert) |
| Events | "user.created" (String) | events.created (typisiert + Payload) |
| System Hooks | "system:search-index" | SystemHooks.searchIndex |
| Errors | "handler_not_found" | ErrorCodes.handlerNotFound / Error-Klassen |
| Context | ctx["db"] | ctx.db (typed Property) |
| Routes | "/write" | Routes.write |
| Audit | manueller String | Aus Handler abgeleitet |
| CRUD | 5 separate Strings | crud.handlers.* / crud.events.* |
| Message Kinds | "write" | MessageKind.write |
Keine Enums. Alles as const Objekte mit inferierten Union Types.