Qualified Names (QN)
Einheitliches Naming-Pattern fuer alle Framework-Identifikatoren.
Pattern
scope:type:name- Scope — Feature-Name in kebab-case (
tasks,channel-in-app,system) - Type — Was es ist (offen, nicht auf Framework-Types beschraenkt)
- Name — Identifier, kann
:fuer Entity:Action-Trennung enthalten - Segmentformat — jedes Segment:
/^[a-z][a-z0-9-]*$/ - Separator — Immer
:— auch innerhalb des Namens fuer Entity:Action - Parsing —
parseQnsplittet nur an den ersten zwei:; Rest ist der Name
Built-in Types
Das Framework nutzt diese Types:
| Type | Verwendung |
|---|---|
write | Write-Handler (create, update, delete) |
query | Query-Handler (list, detail) |
hook | System Hooks (audit, search, sse) |
job | Background Jobs |
notify | Notifications |
event | SSE Events, Domain Events |
channel | Delivery Channels (in-app, email) |
config | Config Keys |
Custom Types
Features koennen eigene Types definieren. Der Type wird nur auf Format geprueft, nicht auf Mitgliedschaft:
// Feature definiert eigene QNsqn("billing", "workflow", "invoice-approval") // → "billing:workflow:invoice-approval"qn("auth", "rule", "must-be-admin") // → "auth:rule:must-be-admin"qn("crm", "pipeline", "lead-qualification") // → "crm:pipeline:lead-qualification"Damit kann jedes Feature eigene Konventionen einfuehren und trotzdem das gleiche QN-System nutzen — validierbar, filterbar, inspizierbar.
QN-Tabelle
| Was | Feature schreibt | Framework qualifiziert zu |
|---|---|---|
| WriteHandler (CRUD) | r.crud("task") | tasks:write:task:create |
| WriteHandler (manual) | r.writeHandler("task:create", ...) | tasks:write:task:create |
| WriteHandler (standalone) | r.writeHandler("reset", ...) | admin:write:reset |
| QueryHandler | r.queryHandler("task:detail", ...) | tasks:query:task:detail |
| Job | r.job("sendInvite", ...) | admin:job:send-invite |
| Notification | r.notification("ticketAssigned", ...) | tickets:notify:ticket-assigned |
| Config | r.config({ keys: { defaultVat } }) | invoicing:config:default-vat |
| Event | r.defineEvent("orderCreated", ...) | orders:event:order-created |
| System Hook | (intern) | system:hook:audit-trail |
| SSE Event | (intern) | system:event:task:created |
| InApp Event | (intern) | channel-in-app:event:delivered |
Wann Colon im Namen, wann Dash?
- Entity:Action → Colon:
"task:create","invoice:send","user:detail" - Standalone (kein Entity) → Dash fuer Woerter:
"assign-order","send-invite","audit-trail" - Faustregel: Colon trennt Entity von Action. Dash trennt Woerter innerhalb eines Segments.
Automatische Konvertierung
qualify(featureName, type, name) konvertiert automatisch:
- camelCase → kebab-case:
channelInApp→channel-in-app - dot.separated → kebab-case:
task.create→task-create - Feature-Name: wird als Scope in kebab-case eingesetzt
Der User schreibt nur den kurzen Namen. Das Framework baut den QN.
CRUD-Konvention
r.crud("task") erzeugt automatisch 5 Handler mit Entity:Action-Notation:
r.crud(taskEntity)// → Write: "task:create", "task:update", "task:delete"// → Query: "task:list", "task:detail"// Qualifiziert: "tasks:write:task:create", "tasks:query:task:list", etc.Cross-Feature-Referenzen
Innerhalb des eigenen Features: kurzer Name (wird automatisch qualifiziert).
defineFeature("invoicing", (r) => { r.notification("invoiceSent", { trigger: { on: "invoice-create" }, // → invoicing:write:invoice-create });});Cross-Feature: voller QN.
defineFeature("analytics", (r) => { r.job("trackOrder", { trigger: { on: "orders:write:order-create" }, // anderes Feature → voller QN });});Validierung
QNs werden bei der Feature-Registrierung validiert:
- Format —
qn()wirft bei ungueltigem Scope, Type oder Name (muss[a-z][a-z0-9-]*sein) - Duplikate — Registry wirft bei doppelten QNs
- Referenzen — Hook-Targets, Job-Triggers, Notification-Triggers muessen auf existierende Handler zeigen
// Fehler bei Boot:// "Invalid QN scope "MyFeature": must match /^[a-z][a-z0-9-]*$/"// "Duplicate write handler: tasks:write:task-create"// "Notification tickets:notify:assigned triggers on ghost-handler but no handler exists"Entity-Mapping
Handler werden ueber die Colon-Konvention auf Entities gemappt:
r.crud()setzt das Mapping automatisch fuer alle 5 CRUD-Handlerr.writeHandler("task:create")→ Entity ist das Segment vor dem ersten Colon ("task")r.writeHandler("reset")→ kein Colon, kein Entity-Mapping (standalone Handler)- Kein Raten, kein O(n²) Matching — einfach
name.split(":")[0]
Wann ist das Entity-Mapping Pflicht?
Field-level Access-Regeln (access: { read: [...], write: [...] } auf einem Feld) werden nur auf Handler angewendet, die einer Entity zugeordnet sind. Darum:
- Entity hat Field-Access-Regeln → alle Write-Handler der Entity brauchen
"entity:action"-Namen, sonst wirft der Boot-Validator. - Entity hat keine Field-Access-Regeln → Colon-Format ist optional.
Beispiel:
// Entity mit geschuetztem Feldconst userEntity = createEntity({ fields: { passwordHash: createTextField({ access: { read: ["system"] } }), // ... },});
r.writeHandler("user:create", ...) // OK — mapped auf user-Entity, Field-Access greiftr.writeHandler("create", ...) // Boot-Fehler: kein Entity-Mapping, Regeln unwirksamWas KEIN QN ist
| Was | Format | Warum |
|---|---|---|
| Entity-Name | "task", "billingPeriod" | Global unique, kein Feature-Prefix noetig |
| Translation-Key | "feature:nav.title" | i18next Namespace-Konvention — bewusst 2-Segment. Punkte im Key sind i18next-Hierarchie, kein QN. isValidQn() gibt false. |
| Redis Key | "kumiko:idempotency:..." | Infrastruktur-Namespace, eigenes Schema |
| SSE Channel | "tenant:42" | Transport-Channel, keine Feature-Identitaet |
| Relation-Name | "tasks" auf Entity | Intra-Entity-Referenz |
Debugging & Audit
QNs sind ueberall lesbar — vom Feature-Code bis zum Redis Monitor:
// Audit Log{ action: "tasks:write:task-update", entityType: "task", entityId: 42 }
// SSE Event{ type: "system:event:task-updated", data: { id: 42, changes: { status: "done" } } }
// InApp Delivery{ type: "channel-in-app:event:delivered", data: { userId: 7, title: "..." } }
// Redis MonitorPUBLISH kumiko:events {"type":"system:event:task-created","payload":{...}}
// Filternlogs.filter(l => l.action.startsWith("billing:")) // alles aus billinglogs.filter(l => l.action.includes(":notify:")) // alle notificationslogs.filter(l => l.action.includes(":workflow:")) // custom type eines featuresUtility API
import { qn, parseQn, isValidQn, toKebab, QnTypes } from "@kumiko/framework/engine";
// Entity:Action Handler (Colon im Namen)qn("tasks", QnTypes.write, "task:create") // → "tasks:write:task:create"
// Standalone Handler (kein Entity)qn("admin", QnTypes.write, "reset") // → "admin:write:reset"qn("system", QnTypes.hook, "audit-trail") // → "system:hook:audit-trail"
// Custom Types — genauso gueltigqn("billing", "workflow", "invoice-approval") // → "billing:workflow:invoice-approval"
// Parsing — splittet nur an den ersten 2 ColonsparseQn("tasks:write:task:create") // → { scope: "tasks", type: "write", name: "task:create" }parseQn("system:hook:audit-trail") // → { scope: "system", type: "hook", name: "audit-trail" }
isValidQn("tasks:write:task:create") // → trueisValidQn("billing:workflow:invoice-approval") // → true (custom type)isValidQn("tasks.task.create") // → false
// Konvertierung (Colons bleiben erhalten)toKebab("ticketAssigned") // → "ticket-assigned"toKebab("task:create") // → "task:create"toKebab("SSEBroadcast") // → "sse-broadcast"