Skip to content

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 prueft
access: { roles: [roles.Admni] } // ❌ TypeScript Error

2. 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); // ✅ Typsicher
await ctx.write(handlers.craete, payload); // ❌ TypeScript Error
// Cross-Feature:
import { handlers as orderHandlers } from "@features/orders";
await ctx.write(orderHandlers.create, payload); // ✅ Typsicher

3. 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 Context
type 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.db
ctx.user
ctx.entityName

7. 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 noetig

9. 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

WasAktuellNeu
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
Contextctx["db"]ctx.db (typed Property)
Routes"/write"Routes.write
Auditmanueller StringAus Handler abgeleitet
CRUD5 separate Stringscrud.handlers.* / crud.events.*
Message Kinds"write"MessageKind.write

Keine Enums. Alles as const Objekte mit inferierten Union Types.