Skip to content

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
  • ParsingparseQn splittet nur an den ersten zwei :; Rest ist der Name

Built-in Types

Das Framework nutzt diese Types:

TypeVerwendung
writeWrite-Handler (create, update, delete)
queryQuery-Handler (list, detail)
hookSystem Hooks (audit, search, sse)
jobBackground Jobs
notifyNotifications
eventSSE Events, Domain Events
channelDelivery Channels (in-app, email)
configConfig Keys

Custom Types

Features koennen eigene Types definieren. Der Type wird nur auf Format geprueft, nicht auf Mitgliedschaft:

// Feature definiert eigene QNs
qn("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

WasFeature schreibtFramework 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
QueryHandlerr.queryHandler("task:detail", ...)tasks:query:task:detail
Jobr.job("sendInvite", ...)admin:job:send-invite
Notificationr.notification("ticketAssigned", ...)tickets:notify:ticket-assigned
Configr.config({ keys: { defaultVat } })invoicing:config:default-vat
Eventr.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: channelInAppchannel-in-app
  • dot.separated → kebab-case: task.createtask-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:

  1. Formatqn() wirft bei ungueltigem Scope, Type oder Name (muss [a-z][a-z0-9-]* sein)
  2. Duplikate — Registry wirft bei doppelten QNs
  3. 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-Handler
  • r.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 Feld
const userEntity = createEntity({
fields: {
passwordHash: createTextField({ access: { read: ["system"] } }),
// ...
},
});
r.writeHandler("user:create", ...) // OK — mapped auf user-Entity, Field-Access greift
r.writeHandler("create", ...) // Boot-Fehler: kein Entity-Mapping, Regeln unwirksam

Was KEIN QN ist

WasFormatWarum
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 EntityIntra-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 Monitor
PUBLISH kumiko:events {"type":"system:event:task-created","payload":{...}}
// Filtern
logs.filter(l => l.action.startsWith("billing:")) // alles aus billing
logs.filter(l => l.action.includes(":notify:")) // alle notifications
logs.filter(l => l.action.includes(":workflow:")) // custom type eines features

Utility 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 gueltig
qn("billing", "workflow", "invoice-approval") // → "billing:workflow:invoice-approval"
// Parsing — splittet nur an den ersten 2 Colons
parseQn("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") // → true
isValidQn("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"