@cosmicdrift/kumiko-framework 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +79 -0
- package/package.json +2 -2
- package/src/es-ops/README.md +64 -6
- package/src/es-ops/context.ts +12 -3
- package/src/es-ops/runner.ts +55 -1
- package/src/es-ops/types.ts +22 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,84 @@
|
|
|
1
1
|
# @cosmicdrift/kumiko-framework
|
|
2
2
|
|
|
3
|
+
## 0.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 8489d18: feat(es-ops): Phase 1.5 — tenantIdOverride + dry-run-validator + E2E-Test + Doku
|
|
8
|
+
|
|
9
|
+
Phase 1.5 schließt die Lücken aus Phase 1 die den ersten Driver-Use-Case
|
|
10
|
+
(publicstatus admin-roles) blockten. Siehe Retro:
|
|
11
|
+
`kumiko-platform/docs/plans/features/es-ops-phase1-retro.md` (PR #9).
|
|
12
|
+
|
|
13
|
+
**A1 — tenantIdOverride:**
|
|
14
|
+
`SeedMigrationContext.systemWriteAs(qn, payload, tenantIdOverride?)`.
|
|
15
|
+
Default SYSTEM_TENANT_ID (unverändert für System-scope-Aggregates wie
|
|
16
|
+
config-values). Mit override: `createSystemUser(tenantIdOverride)` als
|
|
17
|
+
Executor, damit der Event-Store-Executor den Aggregate-Stream im
|
|
18
|
+
richtigen Tenant findet. Fix für die `version_conflict`-Klasse-Bug
|
|
19
|
+
(Memory `feedback_event_store_tenant_consistency.md`).
|
|
20
|
+
|
|
21
|
+
**A2 — dry-run-validator:**
|
|
22
|
+
Runner parsed seed-files vor `migration.run()` per regex
|
|
23
|
+
`systemWriteAs\(["']([^"']+)["']`, sammelt handler-QNs, validiert
|
|
24
|
+
gegen `registry.getWriteHandler(qn)`. Fail-fast mit klarer Message
|
|
25
|
+
|
|
26
|
+
- Datei + QN statt zur Runtime "handler not found". Catched camelCase-
|
|
27
|
+
typos (kebab-case-vs-camelCase Drift) + andere QN-Drift zur Boot-Zeit.
|
|
28
|
+
runProdApp reicht den richtigen Registry rein (`registry` neu in
|
|
29
|
+
RunPendingSeedMigrationsArgs).
|
|
30
|
+
|
|
31
|
+
**A3 — E2E-Test:**
|
|
32
|
+
`packages/bundled-features/src/__tests__/es-ops-e2e.integration.ts`
|
|
33
|
+
mit `setupTestStack`-Pattern: tenant+config Features echt geladen,
|
|
34
|
+
echtes Membership-Aggregate via TenantHandlers.addMember im Demo-Tenant,
|
|
35
|
+
seed-migration ruft update-member-roles mit tenantIdOverride → write
|
|
36
|
+
geht durch, Marker landed, Event in Store, Read-Model aktualisiert.
|
|
37
|
+
Plus typo-Test: seed mit camelCase fail-t Dry-Run mit
|
|
38
|
+
`/dry-run found.*unknown handler-QN/`. **TDD-First**: ohne A1+A2 wäre
|
|
39
|
+
der test rot.
|
|
40
|
+
|
|
41
|
+
**A4 — Doku:**
|
|
42
|
+
`framework/src/es-ops/README.md` erweitert um „Wann brauche ich
|
|
43
|
+
tenantIdOverride?" + „Deployment-Anforderungen" (Docker COPY, Idempotenz,
|
|
44
|
+
Multi-Replica) + „Lokaler Smoke vor Push". Recipe-README + seed-files
|
|
45
|
+
auf neue API aktualisiert.
|
|
46
|
+
|
|
47
|
+
**A5 — Smoke-Skript-Template:**
|
|
48
|
+
`samples/recipes/seed-migration/scripts/smoke.ts` als copy-paste-Template
|
|
49
|
+
für App-Authors: Bun-runnable, offline (read-only, kein DB-Write),
|
|
50
|
+
validiert Module-Load + QN-Resolution + System-User-Access. Recipe-
|
|
51
|
+
README dokumentiert Pflicht-Pattern.
|
|
52
|
+
|
|
53
|
+
**Bonus-Fix:**
|
|
54
|
+
`tenant:write:create`-access auf `["system", "SystemAdmin"]` erweitert
|
|
55
|
+
(symmetrisch zu update-member-roles). Aufgedeckt durch Recipe-Smoke +
|
|
56
|
+
initial-tenants-Seed. Pinning-Test in `tenant.integration.ts` updated.
|
|
57
|
+
|
|
58
|
+
**Test-State:** 45/45 grün (Pre-Push). Typecheck clean. Biome clean.
|
|
59
|
+
as-cast-Audit clean. Guard-silent-skip clean. Recipe-Smoke clean.
|
|
60
|
+
|
|
61
|
+
**Folge-Step (separater PR):** publicstatus driver-sample reaktivieren
|
|
62
|
+
mit lokalem Pre-Push-Smoke gegen publicstatus' echtes Feature-Set.
|
|
63
|
+
|
|
64
|
+
## 0.5.2
|
|
65
|
+
|
|
66
|
+
### Patch Changes
|
|
67
|
+
|
|
68
|
+
- 4f0d781: fix(tenant): updateMemberRoles erlaubt "system"-Rolle (symmetrisch zu create)
|
|
69
|
+
|
|
70
|
+
Drift innerhalb des tenant-Features: `tenant:write:create` akzeptierte
|
|
71
|
+
`["system", "SystemAdmin"]`, `tenant:write:update-member-roles` aber
|
|
72
|
+
nur `["SystemAdmin"]`. Konsequenz: ops-tooling und seed-migrations
|
|
73
|
+
(`createSystemUser` mit `roles: ["system"]`) konnten den Handler nicht
|
|
74
|
+
aufrufen — `access_denied`.
|
|
75
|
+
|
|
76
|
+
Live entdeckt beim ersten Driver-Sample der es-ops Phase 1: publicstatus
|
|
77
|
+
seed `2026-05-20-fix-admin-roles.ts` rief `update-member-roles` via
|
|
78
|
+
`systemWriteAs` → access_denied → Pod CrashLoopBackOff.
|
|
79
|
+
|
|
80
|
+
Plus access-rule-Pinning-Test in `tenant.integration.ts`-scenario-7.
|
|
81
|
+
|
|
3
82
|
## 0.5.1
|
|
4
83
|
|
|
5
84
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Framework core — engine, pipeline, API, DB, and every other bit that makes Kumiko go.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -163,7 +163,7 @@
|
|
|
163
163
|
"zod": "^4.4.3"
|
|
164
164
|
},
|
|
165
165
|
"devDependencies": {
|
|
166
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
166
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.6.0",
|
|
167
167
|
"@types/uuid": "^11.0.0",
|
|
168
168
|
"bun-types": "^1.3.13",
|
|
169
169
|
"drizzle-kit": "^0.31.10",
|
package/src/es-ops/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# es-ops
|
|
2
2
|
|
|
3
|
-
ES-Operations für Kumiko-Apps. Phase 1 liefert `seed-migrations` als file-basiertes Diff-and-Apply für Aggregate-State-Updates, die idempotent-Seeder nicht erfassen können.
|
|
3
|
+
ES-Operations für Kumiko-Apps. Phase 1+1.5 liefert `seed-migrations` als file-basiertes Diff-and-Apply für Aggregate-State-Updates, die idempotent-Seeder nicht erfassen können.
|
|
4
|
+
|
|
5
|
+
> **Phase 1 vs 1.5:** Phase 1 hatte den Foundation-Code, Phase 1.5 hat den ersten realen Driver-Use-Case durch (publicstatus admin-roles) und brachte: `tenantIdOverride` für Tenant-scope-Aggregates, Dry-Run-Validator für Handler-QNs, Deploy-Doku, lokales Smoke-Pattern. Pflicht-Lesen: [Retro](../../../../kumiko-platform/docs/plans/features/es-ops-phase1-retro.md).
|
|
4
6
|
|
|
5
7
|
## Quick API
|
|
6
8
|
|
|
@@ -27,16 +29,72 @@ export default {
|
|
|
27
29
|
if (!admin) return;
|
|
28
30
|
for (const m of await ctx.findMembershipsOfUser(admin.id)) {
|
|
29
31
|
if (m.roles.includes("TenantAdmin")) continue;
|
|
30
|
-
await ctx.systemWriteAs(
|
|
31
|
-
|
|
32
|
-
tenantId: m.tenantId,
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
await ctx.systemWriteAs(
|
|
33
|
+
"tenant:write:update-member-roles",
|
|
34
|
+
{ userId: admin.id, tenantId: m.tenantId, roles: [...m.roles, "TenantAdmin"] },
|
|
35
|
+
m.tenantId, // ← tenantIdOverride: Aggregate lebt im Tenant-Stream, NICHT SYSTEM
|
|
36
|
+
);
|
|
35
37
|
}
|
|
36
38
|
},
|
|
37
39
|
} satisfies SeedMigration;
|
|
38
40
|
```
|
|
39
41
|
|
|
42
|
+
### Wann brauche ich `tenantIdOverride`?
|
|
43
|
+
|
|
44
|
+
Faustregel: **wenn das Ziel-Aggregate via Tenant-User erstellt wurde, brauchst Du den Override.**
|
|
45
|
+
|
|
46
|
+
| Aggregate-Typ | Stream-Tenant | `tenantIdOverride` |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| config-values (system-scope) | SYSTEM_TENANT | weglassen |
|
|
49
|
+
| system text-content | SYSTEM_TENANT | weglassen |
|
|
50
|
+
| tenant-membership | jeweiliger Tenant-Stream | ✅ `m.tenantId` |
|
|
51
|
+
| App-Entity (orders, tasks, …) | Tenant-Stream | ✅ Tenant-Id aus dem Lookup |
|
|
52
|
+
|
|
53
|
+
Ohne `tenantIdOverride` sucht der Executor den Stream gegen SYSTEM_TENANT → `version_conflict`. Memory: `feedback_event_store_tenant_consistency.md`.
|
|
54
|
+
|
|
55
|
+
## Deployment-Anforderungen
|
|
56
|
+
|
|
57
|
+
Wichtig — wird gerne übersehen:
|
|
58
|
+
|
|
59
|
+
### Docker / Bun-Bundle
|
|
60
|
+
|
|
61
|
+
Seeds werden zur Runtime via `await import(absolutePath)` geladen. Bun's Bundler strippt dynamic-import-Targets → seeds/-Tree muss **als raw-TS-Tree** ins Image kopiert werden:
|
|
62
|
+
|
|
63
|
+
```dockerfile
|
|
64
|
+
# Nach dem dist-server/-COPY:
|
|
65
|
+
COPY --from=build --chown=app:app /app/seeds ./seeds
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Plus: in der `bun build` Stage NICHT mit `--minify` durch die seed-Files laufen (sie sind keine Eingabe — der Bundler bundlet `bin/main.ts`, nicht das seeds-Verzeichnis).
|
|
69
|
+
|
|
70
|
+
### Idempotenz-Pflicht
|
|
71
|
+
|
|
72
|
+
Seed-Body läuft **NICHT** atomic mit dem Marker (siehe „Was NICHT garantiert ist" unten). Wenn ein Seed mid-way thrown wirft, sind die schon committed Events drin, der Marker aber nicht → Retry beim nächsten Boot. **Seeds müssen idempotent sein.**
|
|
73
|
+
|
|
74
|
+
Standard-Pattern:
|
|
75
|
+
```ts
|
|
76
|
+
const memberships = await ctx.findMembershipsOfUser(adminId);
|
|
77
|
+
for (const m of memberships) {
|
|
78
|
+
if (m.roles.includes("TenantAdmin")) continue; // ← check-then-write
|
|
79
|
+
await ctx.systemWriteAs(...);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Anti-Pattern (NICHT idempotent):
|
|
84
|
+
```ts
|
|
85
|
+
for (let i = 0; i < 5; i++) {
|
|
86
|
+
await ctx.systemWriteAs("create-something", { ... }); // ← Re-Run produziert Duplikate
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Multi-Replica-Boot
|
|
91
|
+
|
|
92
|
+
`pg_advisory_xact_lock` sequentialisiert parallele Pod-Boots. Lock-Key ist global (`0x65736f70` / „esop"), nicht migration-spezifisch → bei N pending Migrationen läuft N-mal sequentiell, nicht parallel. Für die typische seed-Migration-Workload ist das schnell genug; bei sehr langen Migrationen (>30s) auf einem Multi-Replica-Stack: erst manuell als CLI-Step laufen lassen (`bunx kumiko ops seed:apply`), dann Pod-Rollout.
|
|
93
|
+
|
|
94
|
+
### Lokaler Smoke vor Push
|
|
95
|
+
|
|
96
|
+
Pflicht-Pattern: bevor Du seeds in main pushst, einmal lokal gegen Dev-DB den Boot-Loop laufen lassen. Siehe `samples/recipes/seed-migration/scripts/smoke.ts` als copy-paste-Template.
|
|
97
|
+
|
|
40
98
|
## CLI
|
|
41
99
|
|
|
42
100
|
```bash
|
package/src/es-ops/context.ts
CHANGED
|
@@ -29,11 +29,20 @@ export type CreateSeedMigrationContextArgs = {
|
|
|
29
29
|
export function createSeedMigrationContext(
|
|
30
30
|
args: CreateSeedMigrationContextArgs,
|
|
31
31
|
): SeedMigrationContext {
|
|
32
|
-
|
|
32
|
+
// Default-Executor für System-scope-Aggregates (config-values, system
|
|
33
|
+
// text-content, etc.). Bei Tenant-scope-Aggregates muss der Caller
|
|
34
|
+
// explizit `tenantIdOverride` übergeben — siehe types.ts Doku.
|
|
35
|
+
const defaultSystemUser = createSystemUser(SYSTEM_TENANT_ID);
|
|
33
36
|
|
|
34
37
|
return {
|
|
35
|
-
systemWriteAs: async (handlerQualifiedName, payload) => {
|
|
36
|
-
|
|
38
|
+
systemWriteAs: async (handlerQualifiedName, payload, tenantIdOverride) => {
|
|
39
|
+
// tenantIdOverride: baut einen System-User mit der Stream-tenantId
|
|
40
|
+
// damit der Event-Store-Executor das Aggregate im richtigen Stream
|
|
41
|
+
// findet. Verhindert die version_conflict-Falle (siehe Memory
|
|
42
|
+
// feedback_event_store_tenant_consistency.md).
|
|
43
|
+
const executor =
|
|
44
|
+
tenantIdOverride !== undefined ? createSystemUser(tenantIdOverride) : defaultSystemUser;
|
|
45
|
+
const result = await args.dispatcher.write(handlerQualifiedName, payload, executor);
|
|
37
46
|
// Critical: WriteResult{isSuccess: false} würde sonst silent durchlaufen
|
|
38
47
|
// → Marker landet trotz failed-Write → Migration falsch als "applied"
|
|
39
48
|
// markiert. Hier throw damit der Runner's outer-tx rollback macht und
|
package/src/es-ops/runner.ts
CHANGED
|
@@ -20,10 +20,11 @@
|
|
|
20
20
|
// überspringen ohne ihr Code touchen zu müssen. NICHT als
|
|
21
21
|
// Standard-Workflow — wirklich Notfall.
|
|
22
22
|
|
|
23
|
-
import { readdir } from "node:fs/promises";
|
|
23
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
24
24
|
import path from "node:path";
|
|
25
25
|
import { eq, sql } from "drizzle-orm";
|
|
26
26
|
import type { DbConnection, DbRunner } from "../db";
|
|
27
|
+
import type { Registry } from "../engine";
|
|
27
28
|
import { esOperationsTable } from "./operations-schema";
|
|
28
29
|
import type { EsOperationAppliedBy, SeedMigration, SeedMigrationContext } from "./types";
|
|
29
30
|
|
|
@@ -45,6 +46,14 @@ export type RunPendingSeedMigrationsArgs = {
|
|
|
45
46
|
readonly createContext: (dbRunner: DbRunner) => SeedMigrationContext;
|
|
46
47
|
/** Trace-marker: boot | cli | ci-pipeline. Landet in applied_by. */
|
|
47
48
|
readonly appliedBy: EsOperationAppliedBy;
|
|
49
|
+
/** Optional registry für Dry-Run-Validation: parsed jeden seed-file und
|
|
50
|
+
* checkt dass alle referenzierten handler-QNs in der Registry existieren
|
|
51
|
+
* BEVOR die Migration läuft. Catched camelCase-typos + andere QN-Drift
|
|
52
|
+
* zur Boot-Zeit statt mitten im write-cycle (Phase 1.5 / A2).
|
|
53
|
+
*
|
|
54
|
+
* Wenn weggelassen → kein Dry-Run (backward-compat für tests die ohne
|
|
55
|
+
* Registry arbeiten). runProdApp reicht den richtigen Registry rein. */
|
|
56
|
+
readonly registry?: Registry;
|
|
48
57
|
/** Optional log-prefix override, default "[es-ops/seed-migration]". */
|
|
49
58
|
readonly logger?: (line: string) => void;
|
|
50
59
|
};
|
|
@@ -84,6 +93,31 @@ export async function runPendingSeedMigrations(
|
|
|
84
93
|
const appliedIds: string[] = [];
|
|
85
94
|
const skippedIds: string[] = [];
|
|
86
95
|
|
|
96
|
+
// Dry-Run-Pass (Phase 1.5 / A2): vor JEDER migration alle handler-QNs aus
|
|
97
|
+
// den seed-files parsen + gegen registry checken. Fail-fast vor erstem
|
|
98
|
+
// write — gibt klare error-message mit Datei + qn statt zur runtime
|
|
99
|
+
// "handler not found" mitten im migration-flow.
|
|
100
|
+
if (args.registry !== undefined) {
|
|
101
|
+
const unknownQns: Array<{ id: string; qn: string }> = [];
|
|
102
|
+
for (const entry of pending) {
|
|
103
|
+
const source = await readFile(entry.filePath, "utf-8");
|
|
104
|
+
for (const qn of extractWriteHandlerQns(source)) {
|
|
105
|
+
if (!args.registry.getWriteHandler(qn)) {
|
|
106
|
+
unknownQns.push({ id: entry.id, qn });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (unknownQns.length > 0) {
|
|
111
|
+
const lines = unknownQns.map((u) => ` - ${u.id}: "${u.qn}" not registered`);
|
|
112
|
+
throw new Error(
|
|
113
|
+
`[es-ops/seed-migration] dry-run found ${unknownQns.length} unknown handler-QN(s):\n${lines.join(
|
|
114
|
+
"\n",
|
|
115
|
+
)}\n Check spelling against your TenantHandlers/AuthHandlers constants (kebab-case after the colon).`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
log(`${LOG_PREFIX} dry-run ok — all referenced handler-QNs registered`);
|
|
119
|
+
}
|
|
120
|
+
|
|
87
121
|
for (const entry of pending) {
|
|
88
122
|
const migration = await loadSeedModule(entry.filePath);
|
|
89
123
|
|
|
@@ -203,6 +237,26 @@ function sanitizeForEnv(id: string): string {
|
|
|
203
237
|
return id.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
|
|
204
238
|
}
|
|
205
239
|
|
|
240
|
+
// Parse seed-file source + extract handler-QNs aus `systemWriteAs(...)`-
|
|
241
|
+
// Calls. Reine regex (kein AST) — fängt die häufigen Inline-String-Cases:
|
|
242
|
+
// ctx.systemWriteAs("foo:write:bar", payload)
|
|
243
|
+
// systemWriteAs("foo:write:bar", ...) (destructured)
|
|
244
|
+
//
|
|
245
|
+
// Edge-Cases die NICHT geguckt werden:
|
|
246
|
+
// - QN aus Variable: `const qn = "..."; ctx.systemWriteAs(qn, ...)`
|
|
247
|
+
// - String-Concat / Template-Literals mit dynamic vars
|
|
248
|
+
// Diese Pattern sind selten in real seed-migrations + bleibt als known-
|
|
249
|
+
// limitation dokumentiert. Wer dynamic-QN braucht, weiß was er tut.
|
|
250
|
+
function extractWriteHandlerQns(source: string): readonly string[] {
|
|
251
|
+
const pattern = /systemWriteAs\s*\(\s*["']([^"']+)["']/g;
|
|
252
|
+
const out = new Set<string>();
|
|
253
|
+
for (const match of source.matchAll(pattern)) {
|
|
254
|
+
const qn = match[1];
|
|
255
|
+
if (qn) out.add(qn);
|
|
256
|
+
}
|
|
257
|
+
return [...out];
|
|
258
|
+
}
|
|
259
|
+
|
|
206
260
|
function stringifyError(err: unknown): string {
|
|
207
261
|
if (err instanceof Error) return `${err.name}: ${err.message}`;
|
|
208
262
|
try {
|
package/src/es-ops/types.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// (Source-of-Truth + Projection läuft automatisch).
|
|
14
14
|
|
|
15
15
|
import type { DbRunner } from "../db";
|
|
16
|
-
import type { WriteResult } from "../engine";
|
|
16
|
+
import type { TenantId, WriteResult } from "../engine";
|
|
17
17
|
|
|
18
18
|
export type EsOperationAppliedBy = "boot" | "cli" | "ci-pipeline";
|
|
19
19
|
|
|
@@ -67,8 +67,27 @@ export type SeedMigrationContext = {
|
|
|
67
67
|
*
|
|
68
68
|
* Typ-Signatur folgt existing ctx.writeAs (payload als unknown) — Type-
|
|
69
69
|
* Safety kommt über handler-spezifische Wrapper im Aufrufer ("ich weiß
|
|
70
|
-
* was updateMemberRoles braucht"). Versucht NICHT Generic-Magic.
|
|
71
|
-
|
|
70
|
+
* was updateMemberRoles braucht"). Versucht NICHT Generic-Magic.
|
|
71
|
+
*
|
|
72
|
+
* **tenantIdOverride (Phase 1.5):** wenn das Ziel-Aggregate in einem
|
|
73
|
+
* spezifischen Tenant-Stream lebt (nicht SYSTEM_TENANT_ID, was Default
|
|
74
|
+
* ist), MUSS der Caller die Stream-tenantId mitgeben — sonst sucht der
|
|
75
|
+
* Event-Store-Executor den Aggregate-Stream gegen `SYSTEM_TENANT_ID`
|
|
76
|
+
* und liefert `version_conflict` (siehe Memory
|
|
77
|
+
* `feedback_event_store_tenant_consistency.md` + Driver-Use-Case
|
|
78
|
+
* publicstatus-admin-roles in `project_es_ops_phase1_retro.md`).
|
|
79
|
+
*
|
|
80
|
+
* Typische Pattern:
|
|
81
|
+
* - System-scope-Aggregate (config-values, system text-content) →
|
|
82
|
+
* tenantIdOverride weglassen (Default SYSTEM_TENANT_ID).
|
|
83
|
+
* - Tenant-scope-Aggregate (memberships, tenant-config, app-data) →
|
|
84
|
+
* `tenantIdOverride: m.tenantId` (oder den Stream-Tenant aus
|
|
85
|
+
* einem find*-Helper). */
|
|
86
|
+
readonly systemWriteAs: (
|
|
87
|
+
handlerQualifiedName: string,
|
|
88
|
+
payload: unknown,
|
|
89
|
+
tenantIdOverride?: TenantId,
|
|
90
|
+
) => Promise<WriteResult>;
|
|
72
91
|
|
|
73
92
|
// Read-helpers für die häufigsten Lookups. Wachsen on-demand —
|
|
74
93
|
// Phase 1 deckt den admin-roles-Driver-Use-Case ab; weitere Lookups
|