Skip to content

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 User
ctx.queryAs(sessionUser, qn, payload)
ctx.write(qn, payload) // als aktueller User
ctx.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:

SzenarioVerhalten
Outer write succeeds + inner writeAs succeedsTransaction commit, alle afterCommit-Hooks feuern exakt einmal
Outer write fails nach inner writeAsTransaction rollback, keine afterCommit-Hooks feuern — auch nicht die vom inneren Save
Inner writeAs returns isSuccess: falseRollback wie oben
Inner writeAs throwsRollback 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-CaseMuster
Lookup in fremdes Featurectx.queryAs(systemUser, "feature:query:entity:by-key", {...})
Aenderung triggern die privilegiert istctx.writeAs(systemUser, "feature:write:entity:update", {...})
Normaler Read im Kontext des Usersctx.query("feature:query:entity:me", {})
Eigener Write-Handler verkettet Logikctx.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).