Cross-Feature Calls
Ueberblick: dieses Dokument behandelt einen der sechs Kommunikations-Pfade zwischen Features (
ctx.query/ctx.write). Fuer den Vergleich aller Mechanismen und die Entscheidungshilfe welcher wann passt, siehe feature-integration.md.
Prinzip
Handler sind der Contract zwischen Features. Feature A darf Feature B nicht direkt importieren — weder Tables noch Entities noch interne Types. Stattdessen ruft A die von B registrierten Handler ueber eine Bruecke im HandlerContext:
ctx.query(qn, payload) // als aktueller Userctx.queryAs(sessionUser, qn, payload)ctx.write(qn, payload) // als aktueller Userctx.writeAs(sessionUser, qn, payload)r.requires("user") macht die Kopplung explizit und Boot-validiert. Ohne requires-Deklaration scheitert der Boot mit klarer Meldung, wenn die Ziel-Handler fehlen.
Warum
- Entkopplung: Tables und interne Helper sind Implementation-Details. Wenn sie nur ueber Handler angefasst werden, kann B umgebaut werden ohne A anzupassen.
- Konsistenz: Field-Access, Access-Checks, Validation-Hooks, Audit — alle Framework-Garantien greifen genauso wie bei externen Calls.
- Transaktionen: Cross-Feature-Calls laufen in derselben Transaktion wie der Aufrufer. Fehler im inneren Write rollt den gesamten Outer-Batch zurueck.
Identity-Wechsel: queryAs / writeAs
query / write laufen als aktueller User. Field-Access-Regeln filtern wie gewohnt. queryAs / writeAs erlauben den expliziten Wechsel zu einer anderen Identitaet — typisch createSystemUser(tenantId) fuer privilegierte Lookups und Updates:
// Im auth-email-password Login-Handler:const systemUser = createSystemUser(0);const found = await ctx.queryAs(systemUser, "user:query:user:find-for-auth", { email: event.payload.email,});// Findet den User inkl. passwordHash, weil system die read-Regel erfuellt.Der explizite Identity-Wechsel macht sichtbar, dass hier privilegiert gelesen wird. Niemand ruft das versehentlich.
Shared Transaction
Alle Writes im selben Batch (auch die per writeAs verschachtelten) laufen in einer Drizzle-Transaktion:
| Szenario | Verhalten |
|---|---|
| Outer write succeeds + inner writeAs succeeds | Transaction commit, alle afterCommit-Hooks feuern exakt einmal |
| Outer write fails nach inner writeAs | Transaction rollback, keine afterCommit-Hooks feuern — auch nicht die vom inneren Save |
Inner writeAs returns isSuccess: false | Rollback wie oben |
| Inner writeAs throws | Rollback wie oben |
Das Test-File packages/framework/src/pipeline/__tests__/ctx-bridge.integration.ts enthaelt Tests fuer beide Faelle inklusive DB-Persistenz und Hook-Log.
Typische Muster
| Use-Case | Muster |
|---|---|
| Lookup in fremdes Feature | ctx.queryAs(systemUser, "feature:query:entity:by-key", {...}) |
| Aenderung triggern die privilegiert ist | ctx.writeAs(systemUser, "feature:write:entity:update", {...}) |
| Normaler Read im Kontext des Users | ctx.query("feature:query:entity:me", {}) |
| Eigener Write-Handler verkettet Logik | ctx.write("feature:write:entity:create", {...}) |
Nicht-Ziele
- Nicht fuer Events: Cross-Feature-Events gehen weiterhin ueber
r.defineEvent()+ postSave-Hooks / Notifications. Die Bruecke ist synchron request/reply, kein Pub/Sub. - Nicht fuer High-Throughput-Jobs: Wenn ein Feature aufwaendige Berechnungen braucht, gehoert das in einen Job (
r.job()), nicht in eine synchrone Query. - Nicht fuer externe Services: HTTP zu externen APIs bleibt Feature-interne Logik, keine Dispatcher-Bruecke.
Zukunft
Die API-Form ist stabil — die Implementation kann zu einem echten Microservice-Layer (Redis-RPC o.ae.) swappen wenn Kumiko-Deployments das brauchen. Features schreiben sich gegen ctx.query/write unabhaengig davon, ob A und B im gleichen Prozess laufen oder nicht.
Testing
Fuer Tests die HandlerContext manuell konstruieren (z.B. Services die einen Handler direkt aufrufen), liefert @kumiko/framework/testing einen bridgeStub() Helper:
import { bridgeStub } from "@kumiko/framework/testing";
const ctx = { db, registry, ...bridgeStub() };await handler.handler(event, ctx);Die Stub-Funktionen werfen beim Aufruf — so merkt man sofort, wenn ein Test-Pfad versehentlich ueber die Bruecke geht (was dann real gebaut werden muesste statt gestubbt).