@cosmicdrift/kumiko-bundled-features 0.5.2 → 0.7.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
CHANGED
|
@@ -1,5 +1,88 @@
|
|
|
1
1
|
# @cosmicdrift/kumiko-bundled-features
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- bcf43b6: es-ops: `SeedMembershipRow` exposes `streamTenantId` (stream-tenant aus `kumiko_events.v1`) neben dem payload-`tenantId`. Seed-Authors müssen den `kumiko_events`-JOIN nicht mehr selbst bauen — `m.streamTenantId` ist der korrekte Wert für `systemWriteAs`'s `tenantIdOverride` wenn das Aggregate von einem fremden Executor angelegt wurde (typisches `seedTenantMembership(by=systemAdmin)`-Pattern).
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [bcf43b6]
|
|
12
|
+
- @cosmicdrift/kumiko-framework@0.7.0
|
|
13
|
+
- @cosmicdrift/kumiko-dispatcher-live@0.7.0
|
|
14
|
+
- @cosmicdrift/kumiko-renderer@0.7.0
|
|
15
|
+
- @cosmicdrift/kumiko-renderer-web@0.7.0
|
|
16
|
+
|
|
17
|
+
## 0.6.0
|
|
18
|
+
|
|
19
|
+
### Minor Changes
|
|
20
|
+
|
|
21
|
+
- 8489d18: feat(es-ops): Phase 1.5 — tenantIdOverride + dry-run-validator + E2E-Test + Doku
|
|
22
|
+
|
|
23
|
+
Phase 1.5 schließt die Lücken aus Phase 1 die den ersten Driver-Use-Case
|
|
24
|
+
(publicstatus admin-roles) blockten. Siehe Retro:
|
|
25
|
+
`kumiko-platform/docs/plans/features/es-ops-phase1-retro.md` (PR #9).
|
|
26
|
+
|
|
27
|
+
**A1 — tenantIdOverride:**
|
|
28
|
+
`SeedMigrationContext.systemWriteAs(qn, payload, tenantIdOverride?)`.
|
|
29
|
+
Default SYSTEM_TENANT_ID (unverändert für System-scope-Aggregates wie
|
|
30
|
+
config-values). Mit override: `createSystemUser(tenantIdOverride)` als
|
|
31
|
+
Executor, damit der Event-Store-Executor den Aggregate-Stream im
|
|
32
|
+
richtigen Tenant findet. Fix für die `version_conflict`-Klasse-Bug
|
|
33
|
+
(Memory `feedback_event_store_tenant_consistency.md`).
|
|
34
|
+
|
|
35
|
+
**A2 — dry-run-validator:**
|
|
36
|
+
Runner parsed seed-files vor `migration.run()` per regex
|
|
37
|
+
`systemWriteAs\(["']([^"']+)["']`, sammelt handler-QNs, validiert
|
|
38
|
+
gegen `registry.getWriteHandler(qn)`. Fail-fast mit klarer Message
|
|
39
|
+
|
|
40
|
+
- Datei + QN statt zur Runtime "handler not found". Catched camelCase-
|
|
41
|
+
typos (kebab-case-vs-camelCase Drift) + andere QN-Drift zur Boot-Zeit.
|
|
42
|
+
runProdApp reicht den richtigen Registry rein (`registry` neu in
|
|
43
|
+
RunPendingSeedMigrationsArgs).
|
|
44
|
+
|
|
45
|
+
**A3 — E2E-Test:**
|
|
46
|
+
`packages/bundled-features/src/__tests__/es-ops-e2e.integration.ts`
|
|
47
|
+
mit `setupTestStack`-Pattern: tenant+config Features echt geladen,
|
|
48
|
+
echtes Membership-Aggregate via TenantHandlers.addMember im Demo-Tenant,
|
|
49
|
+
seed-migration ruft update-member-roles mit tenantIdOverride → write
|
|
50
|
+
geht durch, Marker landed, Event in Store, Read-Model aktualisiert.
|
|
51
|
+
Plus typo-Test: seed mit camelCase fail-t Dry-Run mit
|
|
52
|
+
`/dry-run found.*unknown handler-QN/`. **TDD-First**: ohne A1+A2 wäre
|
|
53
|
+
der test rot.
|
|
54
|
+
|
|
55
|
+
**A4 — Doku:**
|
|
56
|
+
`framework/src/es-ops/README.md` erweitert um „Wann brauche ich
|
|
57
|
+
tenantIdOverride?" + „Deployment-Anforderungen" (Docker COPY, Idempotenz,
|
|
58
|
+
Multi-Replica) + „Lokaler Smoke vor Push". Recipe-README + seed-files
|
|
59
|
+
auf neue API aktualisiert.
|
|
60
|
+
|
|
61
|
+
**A5 — Smoke-Skript-Template:**
|
|
62
|
+
`samples/recipes/seed-migration/scripts/smoke.ts` als copy-paste-Template
|
|
63
|
+
für App-Authors: Bun-runnable, offline (read-only, kein DB-Write),
|
|
64
|
+
validiert Module-Load + QN-Resolution + System-User-Access. Recipe-
|
|
65
|
+
README dokumentiert Pflicht-Pattern.
|
|
66
|
+
|
|
67
|
+
**Bonus-Fix:**
|
|
68
|
+
`tenant:write:create`-access auf `["system", "SystemAdmin"]` erweitert
|
|
69
|
+
(symmetrisch zu update-member-roles). Aufgedeckt durch Recipe-Smoke +
|
|
70
|
+
initial-tenants-Seed. Pinning-Test in `tenant.integration.ts` updated.
|
|
71
|
+
|
|
72
|
+
**Test-State:** 45/45 grün (Pre-Push). Typecheck clean. Biome clean.
|
|
73
|
+
as-cast-Audit clean. Guard-silent-skip clean. Recipe-Smoke clean.
|
|
74
|
+
|
|
75
|
+
**Folge-Step (separater PR):** publicstatus driver-sample reaktivieren
|
|
76
|
+
mit lokalem Pre-Push-Smoke gegen publicstatus' echtes Feature-Set.
|
|
77
|
+
|
|
78
|
+
### Patch Changes
|
|
79
|
+
|
|
80
|
+
- Updated dependencies [8489d18]
|
|
81
|
+
- @cosmicdrift/kumiko-framework@0.6.0
|
|
82
|
+
- @cosmicdrift/kumiko-dispatcher-live@0.6.0
|
|
83
|
+
- @cosmicdrift/kumiko-renderer@0.6.0
|
|
84
|
+
- @cosmicdrift/kumiko-renderer-web@0.6.0
|
|
85
|
+
|
|
3
86
|
## 0.5.2
|
|
4
87
|
|
|
5
88
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -74,10 +74,10 @@
|
|
|
74
74
|
"@aws-sdk/client-s3": "^3.1045.0",
|
|
75
75
|
"@aws-sdk/lib-storage": "^3.1045.0",
|
|
76
76
|
"@aws-sdk/s3-request-presigner": "^3.1045.0",
|
|
77
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.
|
|
78
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
79
|
-
"@cosmicdrift/kumiko-renderer": "0.
|
|
80
|
-
"@cosmicdrift/kumiko-renderer-web": "0.
|
|
77
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.7.0",
|
|
78
|
+
"@cosmicdrift/kumiko-framework": "0.7.0",
|
|
79
|
+
"@cosmicdrift/kumiko-renderer": "0.7.0",
|
|
80
|
+
"@cosmicdrift/kumiko-renderer-web": "0.7.0",
|
|
81
81
|
"@mollie/api-client": "^4.5.0",
|
|
82
82
|
"@node-rs/argon2": "^2.0.2",
|
|
83
83
|
"@types/nodemailer": "^8.0.0",
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// @no-server-stack: seed-runner ist boot-time-Code, kein HTTP-route.
|
|
2
|
+
// setupTestStack/buildServer würden eine Hono-app aufziehen die wir nicht
|
|
3
|
+
// brauchen — der seed-runner ruft dispatcher.write direkt vor dem
|
|
4
|
+
// entrypoint.start(). Pattern matched die echte run-prod-app.ts-Integration
|
|
5
|
+
// (siehe run-prod-app.ts:632 — createDispatcher mit identical ctx-shape
|
|
6
|
+
// inline gebaut bevor entrypoint.start()).
|
|
7
|
+
//
|
|
8
|
+
// End-to-End-Integration-Test gegen real-Stack (Phase 1.5 / A3).
|
|
9
|
+
// Catched die Bug-Klassen die runner.integration.ts mit Mock-Dispatcher
|
|
10
|
+
// NICHT abdeckt:
|
|
11
|
+
// - handler-QN-Resolution (Bug 3)
|
|
12
|
+
// - access-rule-realität (Bug 4)
|
|
13
|
+
// - tenantId-stream-matching (Bug 5)
|
|
14
|
+
//
|
|
15
|
+
// Setup: createTestDb + tenant/config-features. Echtes Aggregate im
|
|
16
|
+
// Demo-Tenant via TenantHandlers.addMember. Seed-migration ruft
|
|
17
|
+
// updateMemberRoles auf — MUSS tenantIdOverride nutzen sonst
|
|
18
|
+
// version_conflict.
|
|
19
|
+
|
|
20
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { tmpdir } from "node:os";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { createRegistry, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
24
|
+
import {
|
|
25
|
+
createEsOperationsTable,
|
|
26
|
+
createSeedMigrationContext,
|
|
27
|
+
esOperationsTable,
|
|
28
|
+
runPendingSeedMigrations,
|
|
29
|
+
} from "@cosmicdrift/kumiko-framework/es-ops";
|
|
30
|
+
import { createEventsTable } from "@cosmicdrift/kumiko-framework/event-store";
|
|
31
|
+
import { createDispatcher, type Dispatcher } from "@cosmicdrift/kumiko-framework/pipeline";
|
|
32
|
+
import {
|
|
33
|
+
createTestDb,
|
|
34
|
+
type TestDb,
|
|
35
|
+
TestUsers,
|
|
36
|
+
unsafeCreateEntityTable,
|
|
37
|
+
unsafePushTables,
|
|
38
|
+
} from "@cosmicdrift/kumiko-framework/stack";
|
|
39
|
+
import { sql } from "drizzle-orm";
|
|
40
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
41
|
+
import { createConfigFeature } from "../config/feature";
|
|
42
|
+
import { createConfigResolver } from "../config/resolver";
|
|
43
|
+
import { configValuesTable } from "../config/table";
|
|
44
|
+
import { TenantHandlers } from "../tenant/constants";
|
|
45
|
+
import { createTenantFeature } from "../tenant/feature";
|
|
46
|
+
import { tenantMembershipsTable } from "../tenant/membership-table";
|
|
47
|
+
import { tenantEntity } from "../tenant/schema/tenant";
|
|
48
|
+
|
|
49
|
+
let testDb: TestDb;
|
|
50
|
+
let dispatcher: Dispatcher;
|
|
51
|
+
let registry: ReturnType<typeof createRegistry>;
|
|
52
|
+
|
|
53
|
+
const systemAdmin = TestUsers.systemAdmin;
|
|
54
|
+
// Demo-Tenant — NICHT SYSTEM_TENANT. Echter App-Realität (publicstatus
|
|
55
|
+
// hat seine Memberships in Demo-Tenants `...0001` / `...0002`).
|
|
56
|
+
const demoTenantId = "00000000-0000-4000-8000-000000000001" as TenantId;
|
|
57
|
+
const adminUserId = "01900000-0000-7000-8000-000000000aaa";
|
|
58
|
+
|
|
59
|
+
beforeAll(async () => {
|
|
60
|
+
testDb = await createTestDb();
|
|
61
|
+
await unsafeCreateEntityTable(testDb.db, tenantEntity);
|
|
62
|
+
await unsafePushTables(testDb.db, { tenantMembershipsTable, configValuesTable });
|
|
63
|
+
await createEventsTable(testDb.db);
|
|
64
|
+
await createEsOperationsTable(testDb.db);
|
|
65
|
+
|
|
66
|
+
registry = createRegistry([createConfigFeature(), createTenantFeature()]);
|
|
67
|
+
const resolver = createConfigResolver();
|
|
68
|
+
dispatcher = createDispatcher(registry, {
|
|
69
|
+
db: testDb.db,
|
|
70
|
+
redis: undefined as never,
|
|
71
|
+
entityCache: undefined as never,
|
|
72
|
+
registry,
|
|
73
|
+
configResolver: resolver,
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
afterAll(async () => {
|
|
78
|
+
await testDb.cleanup();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
beforeEach(async () => {
|
|
82
|
+
await testDb.db.execute(sql`
|
|
83
|
+
TRUNCATE kumiko_es_operations, kumiko_events, read_tenants, read_tenant_memberships
|
|
84
|
+
`);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
function makeTempSeedsDir(files: readonly { name: string; content: string }[]): string {
|
|
88
|
+
const dir = mkdtempSync(join(tmpdir(), "es-ops-e2e-"));
|
|
89
|
+
for (const f of files) writeFileSync(join(dir, f.name), f.content);
|
|
90
|
+
return dir;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
describe("es-ops Phase 1.5 — E2E gegen real-Stack", () => {
|
|
94
|
+
test("seed-migration kann updateMemberRoles auf aggregate in Demo-Tenant aufrufen (tenantIdOverride)", async () => {
|
|
95
|
+
// Setup-Stage: tenant + membership ES-konform via Handler erstellen.
|
|
96
|
+
// event lebt im demoTenant-stream.
|
|
97
|
+
const tenantRes = await dispatcher.write(
|
|
98
|
+
TenantHandlers.create,
|
|
99
|
+
{ id: demoTenantId, key: "demo", name: "Demo Tenant" },
|
|
100
|
+
{ ...systemAdmin, tenantId: demoTenantId },
|
|
101
|
+
);
|
|
102
|
+
expect(tenantRes.isSuccess).toBe(true);
|
|
103
|
+
|
|
104
|
+
const addRes = await dispatcher.write(
|
|
105
|
+
TenantHandlers.addMember,
|
|
106
|
+
{ userId: adminUserId, tenantId: demoTenantId, roles: ["Admin"] },
|
|
107
|
+
{ ...systemAdmin, tenantId: demoTenantId },
|
|
108
|
+
);
|
|
109
|
+
expect(addRes.isSuccess).toBe(true);
|
|
110
|
+
|
|
111
|
+
// Plant: seed migriert die Rolle. KEY: tenantIdOverride = demoTenantId,
|
|
112
|
+
// sonst greift SYSTEM_TENANT_ID-default und write geht in version_conflict.
|
|
113
|
+
const dir = makeTempSeedsDir([
|
|
114
|
+
{
|
|
115
|
+
name: "2026-05-21-add-tenant-admin.ts",
|
|
116
|
+
content: `
|
|
117
|
+
export default {
|
|
118
|
+
description: "ergänze TenantAdmin auf admin-membership im demo-tenant",
|
|
119
|
+
run: async (ctx) => {
|
|
120
|
+
const memberships = await ctx.findMembershipsOfUser("${adminUserId}");
|
|
121
|
+
for (const m of memberships) {
|
|
122
|
+
if (m.roles.includes("TenantAdmin")) continue;
|
|
123
|
+
await ctx.systemWriteAs(
|
|
124
|
+
"tenant:write:update-member-roles",
|
|
125
|
+
{ userId: "${adminUserId}", tenantId: m.tenantId, roles: [...m.roles, "TenantAdmin"] },
|
|
126
|
+
m.tenantId, // ← tenantIdOverride (Phase 1.5 API)
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
`,
|
|
132
|
+
},
|
|
133
|
+
]);
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const result = await runPendingSeedMigrations({
|
|
137
|
+
db: testDb.db,
|
|
138
|
+
seedsDir: dir,
|
|
139
|
+
appliedBy: "boot",
|
|
140
|
+
registry,
|
|
141
|
+
createContext: (dbRunner) => createSeedMigrationContext({ dispatcher, dbRunner }),
|
|
142
|
+
logger: () => {},
|
|
143
|
+
});
|
|
144
|
+
expect(result.appliedIds).toEqual(["2026-05-21-add-tenant-admin"]);
|
|
145
|
+
|
|
146
|
+
// Verify (a) marker
|
|
147
|
+
const markers = await testDb.db.select().from(esOperationsTable);
|
|
148
|
+
expect(markers).toHaveLength(1);
|
|
149
|
+
expect(markers[0]?.id).toBe("2026-05-21-add-tenant-admin");
|
|
150
|
+
|
|
151
|
+
// Verify (b) event in store mit tenant-membership.updated
|
|
152
|
+
const events = (await testDb.db.execute(
|
|
153
|
+
sql`SELECT type, tenant_id::text AS tenant_id FROM kumiko_events ORDER BY id`,
|
|
154
|
+
)) as unknown as readonly { type: string; tenant_id: string }[];
|
|
155
|
+
const updateEvents = events.filter((e) => e.type === "tenant-membership.updated");
|
|
156
|
+
expect(updateEvents).toHaveLength(1);
|
|
157
|
+
expect(updateEvents[0]?.tenant_id).toBe(demoTenantId);
|
|
158
|
+
|
|
159
|
+
// Verify (c) read-model aktualisiert
|
|
160
|
+
const memberships = (await testDb.db.execute(
|
|
161
|
+
sql`SELECT roles FROM read_tenant_memberships WHERE user_id = ${adminUserId}`,
|
|
162
|
+
)) as unknown as readonly { roles: string }[];
|
|
163
|
+
expect(JSON.parse(memberships[0]?.roles ?? "[]")).toEqual(["Admin", "TenantAdmin"]);
|
|
164
|
+
} finally {
|
|
165
|
+
rmSync(dir, { recursive: true, force: true });
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("seed-migration mit handler-QN-typo fail-t dry-run (vor write)", async () => {
|
|
170
|
+
// Phase 1.5 / A2 — seed-dry-run-validator soll camelCase-typo catchen
|
|
171
|
+
// BEVOR der dispatcher den write versucht.
|
|
172
|
+
const dir = makeTempSeedsDir([
|
|
173
|
+
{
|
|
174
|
+
name: "2026-05-21-bad-qn.ts",
|
|
175
|
+
content: `
|
|
176
|
+
export default {
|
|
177
|
+
description: "uses camelCase typo for handler-QN",
|
|
178
|
+
run: async (ctx) => {
|
|
179
|
+
await ctx.systemWriteAs(
|
|
180
|
+
"tenant:write:updateMemberRoles", // ← camelCase typo
|
|
181
|
+
{ userId: "x", tenantId: "y", roles: ["z"] },
|
|
182
|
+
"y",
|
|
183
|
+
);
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
`,
|
|
187
|
+
},
|
|
188
|
+
]);
|
|
189
|
+
try {
|
|
190
|
+
await expect(
|
|
191
|
+
runPendingSeedMigrations({
|
|
192
|
+
db: testDb.db,
|
|
193
|
+
seedsDir: dir,
|
|
194
|
+
appliedBy: "boot",
|
|
195
|
+
registry, // ← dry-run sieht den typo, wirft mit klarer message
|
|
196
|
+
createContext: (dbRunner) => createSeedMigrationContext({ dispatcher, dbRunner }),
|
|
197
|
+
logger: () => {},
|
|
198
|
+
}),
|
|
199
|
+
).rejects.toThrow(
|
|
200
|
+
// Phase 1.5 / A2 — Dry-Run-validator wirft mit der spezifischen
|
|
201
|
+
// Phrase "dry-run found ... unknown handler-QN" + dem qn im body.
|
|
202
|
+
/dry-run found.*unknown handler-QN/,
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const markers = await testDb.db.select().from(esOperationsTable);
|
|
206
|
+
expect(markers).toHaveLength(0);
|
|
207
|
+
} finally {
|
|
208
|
+
rmSync(dir, { recursive: true, force: true });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -327,7 +327,9 @@ describe("scenario 6: config integration with tenant", () => {
|
|
|
327
327
|
|
|
328
328
|
describe("scenario 7: access rules on handlers", () => {
|
|
329
329
|
test("all handlers have correct access rules", async () => {
|
|
330
|
+
// "system" für seed-migrations + ops-tooling, "SystemAdmin" für UI-Operator
|
|
330
331
|
expect(rolesOf(stack.registry.getWriteHandler(TenantHandlers.create)?.access)).toEqual([
|
|
332
|
+
"system",
|
|
331
333
|
"SystemAdmin",
|
|
332
334
|
]);
|
|
333
335
|
expect(rolesOf(stack.registry.getWriteHandler(TenantHandlers.update)?.access)).toEqual([
|
|
@@ -16,6 +16,10 @@ export const createWrite = defineWriteHandler({
|
|
|
16
16
|
key: z.string().min(1).max(50),
|
|
17
17
|
name: z.string().min(1).max(200),
|
|
18
18
|
}),
|
|
19
|
-
|
|
19
|
+
// "system" + "SystemAdmin" — symmetrisch zu update-member-roles.
|
|
20
|
+
// ops-tooling (seed-migrations + sample-recipes) nutzen System-User
|
|
21
|
+
// (roles=["system"]) als Executor; "SystemAdmin" bleibt der echte
|
|
22
|
+
// human-Operator-Pfad über die UI.
|
|
23
|
+
access: { roles: ["system", "SystemAdmin"] },
|
|
20
24
|
handler: async (event, ctx) => crud.create(event.payload, event.user, ctx.db),
|
|
21
25
|
});
|