@cosmicdrift/kumiko-dev-server 0.2.3 → 0.3.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 +58 -0
- package/package.json +27 -9
- package/src/codegen/__tests__/strict-mode-diagnostics.test.ts +198 -323
- package/src/codegen/render.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
# @cosmicdrift/kumiko-dev-server
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0.3.0 bringt zwei neue Subsysteme (Step-Engine Tier-3 + Visual-Tree) plus
|
|
8
|
+
eine AST-Codemod-Pipeline als Vorarbeit für den L2-AI-Layer.
|
|
9
|
+
|
|
10
|
+
### Breaking Changes
|
|
11
|
+
|
|
12
|
+
- `skipTransitionGuard` → `unsafeSkipTransitionGuard` (Rename in
|
|
13
|
+
feature-ast + engine). Der `unsafe`-Prefix macht die Tragweite des
|
|
14
|
+
Casts sichtbar und ist konsistent zur `unsafeProjectionUpsert`- und
|
|
15
|
+
`r.rawTable`-Konvention. Migration: 1:1-Ersetzung, keine Verhaltens-Änderung.
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
- **Step-Engine M.4 — Tier-3 Workflow-Engine.** Neue Step-Vocabulary
|
|
20
|
+
`wait`, `waitForEvent`, `retry` ermöglicht persistierte Long-Running-Flows
|
|
21
|
+
über Job-Boundaries hinweg. Q7 Snapshot-at-Start hängt jedem Step-Run
|
|
22
|
+
einen SHA-256-Fingerprint des Aggregat-Zustands an, sodass Replays
|
|
23
|
+
deterministisch gegen den ursprünglichen Eingangszustand laufen.
|
|
24
|
+
- **Visual-Tree V.1.x — Tree-API + Editor-Panel.** Neue `VisualTree`-
|
|
25
|
+
Component plus TreeProvider-Pattern; erste TreeProviders für
|
|
26
|
+
`text-content` und `legal-pages` (CMS-light + Impressum/Privacy).
|
|
27
|
+
Fundament für den späteren No-Code-Designer (~3000 LOC, 98 Tests).
|
|
28
|
+
- **Codemod-Pipeline.** AST-basierte Patcher-Module für strukturelle
|
|
29
|
+
Feature-Edits — wird vom kommenden L2-AI-Layer als Tool-Surface
|
|
30
|
+
verwendet, ist aber eigenständig nutzbar für ts-morph-style Migrationen.
|
|
31
|
+
- **user-data-rights Sample-Recipe.** DSGVO Art. 15/17/18/20 vollständig
|
|
32
|
+
als Sample-Recipe (`samples/recipes/`) inklusive README — zeigt die
|
|
33
|
+
Export- und Forget-Pipeline gegen den `compliance-profiles`-Default
|
|
34
|
+
(`eu-dsgvo`).
|
|
35
|
+
|
|
36
|
+
### Fixes
|
|
37
|
+
|
|
38
|
+
- `tier-engine`: auto-default-tier-Hook benutzt jetzt `ctx.db.raw` für
|
|
39
|
+
Event-Store-Operationen (#37, vorher: stiller Bug, 22 Tage live).
|
|
40
|
+
- `engine`: unsafe-projection-upsert nutzt `as never` statt `as any` —
|
|
41
|
+
schmaler Cast-Surface, weniger Compiler-Knebel.
|
|
42
|
+
- `visual-tree`: runtime-isolation marker für client-konsumierte Files,
|
|
43
|
+
damit der Multi-Entry-Build den richtigen Bundle-Split bekommt.
|
|
44
|
+
- `feature-ast`: vollständiger `unsafeSkipTransitionGuard`-Rename (war
|
|
45
|
+
in zwei Modulen noch der alte Name).
|
|
46
|
+
- `framework`: Error-Reasons + `noConsole`-Lint + No-Date-API-Guard
|
|
47
|
+
wieder push-ready.
|
|
48
|
+
|
|
49
|
+
### Library-Updates
|
|
50
|
+
|
|
51
|
+
hono 4.12, jose 6.2, stripe 22.1, meilisearch 0.58, marked 18,
|
|
52
|
+
bun-types 1.3.13, lucide-react 1.14, bullmq 5.76, ioredis 5.10,
|
|
53
|
+
i18next 26.0, react + radix-ui-primitives auf aktuelle Minors.
|
|
54
|
+
|
|
55
|
+
### Patch Changes
|
|
56
|
+
|
|
57
|
+
- Updated dependencies
|
|
58
|
+
- @cosmicdrift/kumiko-framework@0.3.0
|
|
59
|
+
- @cosmicdrift/kumiko-bundled-features@0.3.0
|
|
60
|
+
|
|
3
61
|
## 0.2.3
|
|
4
62
|
|
|
5
63
|
### Patch Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-dev-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Development server bootstrap for Kumiko apps. Bundles the client, mints dev-JWTs, injects the resolved AppSchema, and seeds an admin. Not for production.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -18,20 +18,38 @@
|
|
|
18
18
|
"runtime": "dev"
|
|
19
19
|
},
|
|
20
20
|
"exports": {
|
|
21
|
-
".":
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"./
|
|
26
|
-
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./src/index.ts",
|
|
23
|
+
"default": "./src/index.ts"
|
|
24
|
+
},
|
|
25
|
+
"./build": {
|
|
26
|
+
"types": "./src/build.ts",
|
|
27
|
+
"default": "./src/build.ts"
|
|
28
|
+
},
|
|
29
|
+
"./compose-features": {
|
|
30
|
+
"types": "./src/compose-features.ts",
|
|
31
|
+
"default": "./src/compose-features.ts"
|
|
32
|
+
},
|
|
33
|
+
"./drizzle-config": {
|
|
34
|
+
"types": "./src/drizzle-config.ts",
|
|
35
|
+
"default": "./src/drizzle-config.ts"
|
|
36
|
+
},
|
|
37
|
+
"./drizzle-tables-auth-mode": {
|
|
38
|
+
"types": "./src/drizzle-tables-auth-mode.ts",
|
|
39
|
+
"default": "./src/drizzle-tables-auth-mode.ts"
|
|
40
|
+
},
|
|
41
|
+
"./drizzle-tables-minimal": {
|
|
42
|
+
"types": "./src/drizzle-tables-minimal.ts",
|
|
43
|
+
"default": "./src/drizzle-tables-minimal.ts"
|
|
44
|
+
}
|
|
27
45
|
},
|
|
28
46
|
"bin": {
|
|
29
47
|
"kumiko-build": "./bin/kumiko-build.ts",
|
|
30
48
|
"kumiko-dev": "./bin/kumiko-dev.ts"
|
|
31
49
|
},
|
|
32
50
|
"dependencies": {
|
|
33
|
-
"@cosmicdrift/kumiko-bundled-features": "0.
|
|
34
|
-
"@cosmicdrift/kumiko-framework": "0.
|
|
51
|
+
"@cosmicdrift/kumiko-bundled-features": "0.3.0",
|
|
52
|
+
"@cosmicdrift/kumiko-framework": "0.3.0"
|
|
35
53
|
},
|
|
36
54
|
"publishConfig": {
|
|
37
55
|
"registry": "https://registry.npmjs.org",
|
|
@@ -1,39 +1,12 @@
|
|
|
1
|
-
// Regression-Guard für die EIGENTLICHE Behauptung der Codegen-Pipeline:
|
|
2
|
-
// `ctx.appendEvent` wird via Lokal-Wrapper STRICT typgeprüft.
|
|
3
|
-
//
|
|
4
|
-
// Ohne diesen Test verifizieren die anderen 12 Codegen-Tests nur, dass
|
|
5
|
-
// die richtigen Strings ins File geschrieben werden. Wenn jemand später
|
|
6
|
-
// das `export *`-Shadowing kaputt macht, den `KumikoEventTypeMap`-
|
|
7
|
-
// Re-Export aus `engine/index.ts` entfernt, oder TS-Verhalten in einer
|
|
8
|
-
// neuen Version subtil bricht — die anderen Tests bleiben grün und der
|
|
9
|
-
// strict-mode stirbt schweigend. Genau das wäre der Fall den dieser
|
|
10
|
-
// Test fängt.
|
|
11
|
-
//
|
|
12
|
-
// Ablauf pro Test-Case:
|
|
13
|
-
// 1. tmp-App mit feature.ts + events.ts + bin/main.ts erzeugen.
|
|
14
|
-
// 2. `runCodegen` auf die tmp-App fahren — schreibt
|
|
15
|
-
// `.kumiko/types.generated.d.ts` + `define.ts`.
|
|
16
|
-
// 3. Eine synthetische Test-Datei mit den gewünschten Aufrufen schreiben.
|
|
17
|
-
// 4. ts.createProgram über die App + paths-alias zur framework-source.
|
|
18
|
-
// 5. Diagnostics auswerten — auf konkrete TS-Codes prüfen.
|
|
19
|
-
//
|
|
20
|
-
// `paths` zeigt direkt auf die framework-source (`packages/framework/src`),
|
|
21
|
-
// damit der TS-Type-Checker die Augmentation als Teil DESSELBEN Compiles
|
|
22
|
-
// sieht (Use-Site-Substitution funktioniert nur so — siehe
|
|
23
|
-
// project_x1_typemap_findings memory).
|
|
24
|
-
|
|
25
1
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
26
2
|
import { dirname, join } from "node:path";
|
|
27
3
|
import * as ts from "typescript";
|
|
28
|
-
import { afterAll, describe, expect, test } from "vitest";
|
|
4
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
29
5
|
import { runCodegen } from "../run-codegen";
|
|
30
6
|
|
|
31
7
|
const REPO_ROOT = join(__dirname, "../../../../..");
|
|
32
8
|
const FRAMEWORK_SRC = join(REPO_ROOT, "packages/framework/src");
|
|
33
9
|
|
|
34
|
-
// Test-Apps werden IM Repo-Tree angelegt (gitignored), damit Node's
|
|
35
|
-
// natürliches `node_modules`-Hochsuchen 'zod' & Co finden kann. tmpdir
|
|
36
|
-
// liegt außerhalb des Repo-Trees → keine node_modules-Sicht.
|
|
37
10
|
const TEST_FIXTURE_DIR = join(__dirname, ".tmp-fixtures");
|
|
38
11
|
const createdDirs: string[] = [];
|
|
39
12
|
|
|
@@ -48,15 +21,11 @@ afterAll(() => {
|
|
|
48
21
|
for (const d of createdDirs) {
|
|
49
22
|
try {
|
|
50
23
|
rmSync(d, { recursive: true, force: true });
|
|
51
|
-
} catch {
|
|
52
|
-
// best-effort cleanup
|
|
53
|
-
}
|
|
24
|
+
} catch {}
|
|
54
25
|
}
|
|
55
26
|
try {
|
|
56
27
|
rmSync(TEST_FIXTURE_DIR, { recursive: true, force: true });
|
|
57
|
-
} catch {
|
|
58
|
-
// ditto
|
|
59
|
-
}
|
|
28
|
+
} catch {}
|
|
60
29
|
});
|
|
61
30
|
|
|
62
31
|
function write(dir: string, relPath: string, content: string): string {
|
|
@@ -66,16 +35,7 @@ function write(dir: string, relPath: string, content: string): string {
|
|
|
66
35
|
return full;
|
|
67
36
|
}
|
|
68
37
|
|
|
69
|
-
/**
|
|
70
|
-
* Baut ein TS-Program über die App + framework-source, gibt die
|
|
71
|
-
* semantischen Diagnostics zurück. Lib-files werden vom installierten
|
|
72
|
-
* typescript-Package geholt; sonst meckert TS über fehlende DOM-types.
|
|
73
|
-
*/
|
|
74
38
|
function compileApp(appRoot: string): readonly ts.Diagnostic[] {
|
|
75
|
-
// Wir lassen ts node_modules vom REPO_ROOT auflösen (tmp-Dir hat kein
|
|
76
|
-
// eigenes node_modules). `baseUrl` zeigt auf repo, `paths` mappt
|
|
77
|
-
// framework + tmp-app explizit; rest fällt auf node_modules-Lookup
|
|
78
|
-
// im repo-Tree zurück.
|
|
79
39
|
const compilerOptions: ts.CompilerOptions = {
|
|
80
40
|
target: ts.ScriptTarget.ESNext,
|
|
81
41
|
module: ts.ModuleKind.ESNext,
|
|
@@ -91,8 +51,6 @@ function compileApp(appRoot: string): readonly ts.Diagnostic[] {
|
|
|
91
51
|
types: [],
|
|
92
52
|
};
|
|
93
53
|
|
|
94
|
-
// Sammle alle .ts-Files unter src/ + .kumiko/, plus die framework-
|
|
95
|
-
// source-tree die wir via paths erreichen wollen.
|
|
96
54
|
const program = ts.createProgram({
|
|
97
55
|
rootNames: collectFiles(appRoot),
|
|
98
56
|
options: compilerOptions,
|
|
@@ -125,140 +83,214 @@ function collectFiles(dir: string): string[] {
|
|
|
125
83
|
return out;
|
|
126
84
|
}
|
|
127
85
|
|
|
128
|
-
// Default-shape feature: setup callback registers `placed` and returns
|
|
129
|
-
// nothing. Used by tests that exercise the standard `ctx.appendEvent({
|
|
130
|
-
// type: "orders:event:placed", ... })` literal-string path.
|
|
131
|
-
function setupApp(): string {
|
|
132
|
-
const appRoot = makeAppDir();
|
|
133
|
-
writeOrderPlacedSchema(appRoot);
|
|
134
|
-
write(
|
|
135
|
-
appRoot,
|
|
136
|
-
"src/feature/feature.ts",
|
|
137
|
-
`import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
138
|
-
import { orderPlacedSchema } from "./events";
|
|
139
|
-
|
|
140
|
-
export const ordersFeature = defineFeature("orders", (r) => {
|
|
141
|
-
r.defineEvent("placed", orderPlacedSchema);
|
|
142
|
-
});
|
|
143
|
-
`,
|
|
144
|
-
);
|
|
145
|
-
return appRoot;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// Exports-shape feature: setup callback returns `{ placed }` so handler
|
|
149
|
-
// modules can do `ordersFeature.exports.placed.name` and pick up the
|
|
150
|
-
// literal type. Used by the eventDef.name pattern test.
|
|
151
|
-
function setupAppWithExports(): string {
|
|
152
|
-
const appRoot = makeAppDir();
|
|
153
|
-
writeOrderPlacedSchema(appRoot);
|
|
154
|
-
write(
|
|
155
|
-
appRoot,
|
|
156
|
-
"src/feature/feature.ts",
|
|
157
|
-
`import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";
|
|
158
|
-
import { orderPlacedSchema } from "./events";
|
|
159
|
-
|
|
160
|
-
export const ordersFeature = defineFeature("orders", (r) => ({
|
|
161
|
-
placed: r.defineEvent("placed", orderPlacedSchema),
|
|
162
|
-
}));
|
|
163
|
-
`,
|
|
164
|
-
);
|
|
165
|
-
return appRoot;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
86
|
function writeOrderPlacedSchema(appRoot: string): void {
|
|
169
87
|
write(
|
|
170
88
|
appRoot,
|
|
171
89
|
"src/feature/events.ts",
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
90
|
+
[
|
|
91
|
+
'import { z } from "zod";',
|
|
92
|
+
"export const orderPlacedSchema = z.object({",
|
|
93
|
+
" orderId: z.string(),",
|
|
94
|
+
" customerId: z.string(),",
|
|
95
|
+
" amount: z.number(),",
|
|
96
|
+
"});",
|
|
97
|
+
"",
|
|
98
|
+
].join("\n"),
|
|
179
99
|
);
|
|
180
100
|
}
|
|
181
101
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
// >60s — vermutlich kleinere Cores + weniger RAM-cache als M-Series-
|
|
186
|
-
// Mac. 120s gibt genug Puffer für den langsamsten Run-Path
|
|
187
|
-
// (eventDef.name pattern lädt full augmented map) ohne echte Hänger
|
|
188
|
-
// zu maskieren.
|
|
189
|
-
const STRICT_MODE_TIMEOUT_MS = 120_000;
|
|
102
|
+
describe("strict-mode diagnostics -- the actual contract of the codegen", () => {
|
|
103
|
+
let appRoot: string;
|
|
104
|
+
let allDiagnostics: readonly ts.Diagnostic[];
|
|
190
105
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
106
|
+
beforeAll(() => {
|
|
107
|
+
appRoot = makeAppDir();
|
|
108
|
+
writeOrderPlacedSchema(appRoot);
|
|
109
|
+
write(
|
|
110
|
+
appRoot,
|
|
111
|
+
"src/feature/feature.ts",
|
|
112
|
+
[
|
|
113
|
+
'import { defineFeature } from "@cosmicdrift/kumiko-framework/engine";',
|
|
114
|
+
'import { orderPlacedSchema } from "./events";',
|
|
115
|
+
"",
|
|
116
|
+
'export const ordersFeature = defineFeature("orders", (r) => ({',
|
|
117
|
+
' placed: r.defineEvent("placed", orderPlacedSchema),',
|
|
118
|
+
"}));",
|
|
119
|
+
"",
|
|
120
|
+
].join("\n"),
|
|
121
|
+
);
|
|
194
122
|
runCodegen({ appRoot });
|
|
195
123
|
|
|
196
124
|
write(
|
|
197
125
|
appRoot,
|
|
198
|
-
"src/feature/handler.ts",
|
|
199
|
-
|
|
200
|
-
import {
|
|
126
|
+
"src/feature/handler-good.ts",
|
|
127
|
+
[
|
|
128
|
+
'import { defineWriteHandler } from "../../.kumiko/define";',
|
|
129
|
+
'import { z } from "zod";',
|
|
130
|
+
"",
|
|
131
|
+
"export const placeOrder = defineWriteHandler({",
|
|
132
|
+
' name: "orders.placeOrder",',
|
|
133
|
+
" schema: z.object({}),",
|
|
134
|
+
' access: { roles: ["Admin"] },',
|
|
135
|
+
" handler: async (_event, ctx) => {",
|
|
136
|
+
" await ctx.appendEvent({",
|
|
137
|
+
' aggregateId: "x",',
|
|
138
|
+
' aggregateType: "order",',
|
|
139
|
+
' type: "orders:event:placed",',
|
|
140
|
+
' payload: { orderId: "o1", customerId: "c1", amount: 99 },',
|
|
141
|
+
" });",
|
|
142
|
+
' return { isSuccess: true as const, data: { id: "o1" } };',
|
|
143
|
+
" },",
|
|
144
|
+
"});",
|
|
145
|
+
"",
|
|
146
|
+
].join("\n"),
|
|
147
|
+
);
|
|
201
148
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
149
|
+
write(
|
|
150
|
+
appRoot,
|
|
151
|
+
"src/feature/handler-unknown-type.ts",
|
|
152
|
+
[
|
|
153
|
+
'import { defineWriteHandler } from "../../.kumiko/define";',
|
|
154
|
+
'import { z } from "zod";',
|
|
155
|
+
"",
|
|
156
|
+
"export const placeOrder = defineWriteHandler({",
|
|
157
|
+
' name: "orders.placeOrder",',
|
|
158
|
+
" schema: z.object({}),",
|
|
159
|
+
' access: { roles: ["Admin"] },',
|
|
160
|
+
" handler: async (_event, ctx) => {",
|
|
161
|
+
" await ctx.appendEvent({",
|
|
162
|
+
' aggregateId: "x",',
|
|
163
|
+
' aggregateType: "order",',
|
|
164
|
+
' type: "totally:made:up",',
|
|
165
|
+
" payload: { whatever: 1 },",
|
|
166
|
+
" });",
|
|
167
|
+
' return { isSuccess: true as const, data: { id: "x" } };',
|
|
168
|
+
" },",
|
|
169
|
+
"});",
|
|
170
|
+
"",
|
|
171
|
+
].join("\n"),
|
|
217
172
|
);
|
|
218
173
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
174
|
+
write(
|
|
175
|
+
appRoot,
|
|
176
|
+
"src/feature/handler-payload-mismatch.ts",
|
|
177
|
+
[
|
|
178
|
+
'import { defineWriteHandler } from "../../.kumiko/define";',
|
|
179
|
+
'import { z } from "zod";',
|
|
180
|
+
"",
|
|
181
|
+
"export const placeOrder = defineWriteHandler({",
|
|
182
|
+
' name: "orders.placeOrder",',
|
|
183
|
+
" schema: z.object({}),",
|
|
184
|
+
' access: { roles: ["Admin"] },',
|
|
185
|
+
" handler: async (_event, ctx) => {",
|
|
186
|
+
" await ctx.appendEvent({",
|
|
187
|
+
' aggregateId: "x",',
|
|
188
|
+
' aggregateType: "order",',
|
|
189
|
+
' type: "orders:event:placed",',
|
|
190
|
+
' payload: { orderId: "o1", customerId: "c1", amount: 99, bogus: "extra" },',
|
|
191
|
+
" });",
|
|
192
|
+
' return { isSuccess: true as const, data: { id: "o1" } };',
|
|
193
|
+
" },",
|
|
194
|
+
"});",
|
|
195
|
+
"",
|
|
196
|
+
].join("\n"),
|
|
222
197
|
);
|
|
223
|
-
expect(handlerErrors).toHaveLength(0);
|
|
224
|
-
});
|
|
225
198
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
199
|
+
write(
|
|
200
|
+
appRoot,
|
|
201
|
+
"src/feature/handler-direct.ts",
|
|
202
|
+
[
|
|
203
|
+
'import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";',
|
|
204
|
+
'import { z } from "zod";',
|
|
205
|
+
"",
|
|
206
|
+
"export const placeOrder = defineWriteHandler({",
|
|
207
|
+
' name: "orders.placeOrder",',
|
|
208
|
+
" schema: z.object({}),",
|
|
209
|
+
' access: { roles: ["Admin"] },',
|
|
210
|
+
" handler: async (_event, ctx) => {",
|
|
211
|
+
" await ctx.appendEvent({",
|
|
212
|
+
' aggregateId: "x",',
|
|
213
|
+
' aggregateType: "order",',
|
|
214
|
+
' type: "orders:event:placed",',
|
|
215
|
+
' payload: { orderId: "o1", customerId: "c1", amount: 99 },',
|
|
216
|
+
" });",
|
|
217
|
+
' return { isSuccess: true as const, data: { id: "o1" } };',
|
|
218
|
+
" },",
|
|
219
|
+
"});",
|
|
220
|
+
"",
|
|
221
|
+
].join("\n"),
|
|
222
|
+
);
|
|
231
223
|
|
|
232
224
|
write(
|
|
233
225
|
appRoot,
|
|
234
|
-
"src/feature/handler.ts",
|
|
235
|
-
|
|
236
|
-
import {
|
|
226
|
+
"src/feature/handler-byname-good.ts",
|
|
227
|
+
[
|
|
228
|
+
'import { defineWriteHandler } from "../../.kumiko/define";',
|
|
229
|
+
'import { z } from "zod";',
|
|
230
|
+
'import { ordersFeature } from "./feature";',
|
|
231
|
+
"",
|
|
232
|
+
"const { placed } = ordersFeature.exports;",
|
|
233
|
+
"",
|
|
234
|
+
"export const placeOrder = defineWriteHandler({",
|
|
235
|
+
' name: "orders.placeOrder",',
|
|
236
|
+
" schema: z.object({}),",
|
|
237
|
+
' access: { roles: ["Admin"] },',
|
|
238
|
+
" handler: async (_event, ctx) => {",
|
|
239
|
+
" await ctx.appendEvent({",
|
|
240
|
+
' aggregateId: "x",',
|
|
241
|
+
' aggregateType: "order",',
|
|
242
|
+
" type: placed.name,",
|
|
243
|
+
' payload: { orderId: "o1", customerId: "c1", amount: 99 },',
|
|
244
|
+
" });",
|
|
245
|
+
' return { isSuccess: true as const, data: { id: "o1" } };',
|
|
246
|
+
" },",
|
|
247
|
+
"});",
|
|
248
|
+
"",
|
|
249
|
+
].join("\n"),
|
|
250
|
+
);
|
|
237
251
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
},
|
|
251
|
-
}
|
|
252
|
-
|
|
252
|
+
write(
|
|
253
|
+
appRoot,
|
|
254
|
+
"src/feature/handler-byname-bad.ts",
|
|
255
|
+
[
|
|
256
|
+
'import { defineWriteHandler } from "../../.kumiko/define";',
|
|
257
|
+
'import { z } from "zod";',
|
|
258
|
+
'import { ordersFeature } from "./feature";',
|
|
259
|
+
"",
|
|
260
|
+
"const { placed } = ordersFeature.exports;",
|
|
261
|
+
"",
|
|
262
|
+
"export const placeOrder = defineWriteHandler({",
|
|
263
|
+
' name: "orders.placeOrder",',
|
|
264
|
+
" schema: z.object({}),",
|
|
265
|
+
' access: { roles: ["Admin"] },',
|
|
266
|
+
" handler: async (_event, ctx) => {",
|
|
267
|
+
" await ctx.appendEvent({",
|
|
268
|
+
' aggregateId: "x",',
|
|
269
|
+
' aggregateType: "order",',
|
|
270
|
+
" type: placed.name,",
|
|
271
|
+
' payload: { orderId: "o1", customerId: "c1", amount: 99, bogus: "extra" },',
|
|
272
|
+
" });",
|
|
273
|
+
' return { isSuccess: true as const, data: { id: "o1" } };',
|
|
274
|
+
" },",
|
|
275
|
+
"});",
|
|
276
|
+
"",
|
|
277
|
+
].join("\n"),
|
|
253
278
|
);
|
|
254
279
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
280
|
+
allDiagnostics = compileApp(appRoot);
|
|
281
|
+
}, 120_000);
|
|
282
|
+
|
|
283
|
+
test("good ctx.appendEvent compiles cleanly", () => {
|
|
284
|
+
const handlerErrors = allDiagnostics.filter((d) =>
|
|
285
|
+
d.file?.fileName.endsWith("/feature/handler-good.ts"),
|
|
286
|
+
);
|
|
287
|
+
expect(handlerErrors).toHaveLength(0);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("unknown event-type triggers TS2322 with augmented map in error message", () => {
|
|
291
|
+
const handlerErrors = allDiagnostics.filter((d) =>
|
|
292
|
+
d.file?.fileName.endsWith("/feature/handler-unknown-type.ts"),
|
|
258
293
|
);
|
|
259
|
-
// We expect at least one TS2322 ("not assignable") for the bogus
|
|
260
|
-
// type-string. The exact column may move with TS versions; the code
|
|
261
|
-
// + the type-name are the stable contract.
|
|
262
294
|
const ts2322 = handlerErrors.filter((d) => d.code === 2322);
|
|
263
295
|
expect(ts2322.length).toBeGreaterThan(0);
|
|
264
296
|
const flattened = ts2322
|
|
@@ -267,42 +299,10 @@ export const placeOrder = defineWriteHandler({
|
|
|
267
299
|
expect(flattened).toMatch(/keyof KumikoEventTypeMap|"orders:event:placed"/);
|
|
268
300
|
});
|
|
269
301
|
|
|
270
|
-
test("payload-shape mismatch triggers a property-error", {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
const appRoot = setupApp();
|
|
274
|
-
runCodegen({ appRoot });
|
|
275
|
-
|
|
276
|
-
write(
|
|
277
|
-
appRoot,
|
|
278
|
-
"src/feature/handler.ts",
|
|
279
|
-
`import { defineWriteHandler } from "../../.kumiko/define";
|
|
280
|
-
import { z } from "zod";
|
|
281
|
-
|
|
282
|
-
export const placeOrder = defineWriteHandler({
|
|
283
|
-
name: "orders.placeOrder",
|
|
284
|
-
schema: z.object({}),
|
|
285
|
-
access: { roles: ["Admin"] },
|
|
286
|
-
handler: async (_event, ctx) => {
|
|
287
|
-
await ctx.appendEvent({
|
|
288
|
-
aggregateId: "x",
|
|
289
|
-
aggregateType: "order",
|
|
290
|
-
type: "orders:event:placed",
|
|
291
|
-
payload: { orderId: "o1", customerId: "c1", amount: 99, bogus: "extra" },
|
|
292
|
-
});
|
|
293
|
-
return { isSuccess: true as const, data: { id: "o1" } };
|
|
294
|
-
},
|
|
295
|
-
});
|
|
296
|
-
`,
|
|
297
|
-
);
|
|
298
|
-
|
|
299
|
-
const diagnostics = compileApp(appRoot);
|
|
300
|
-
const handlerErrors = diagnostics.filter((d) =>
|
|
301
|
-
d.file?.fileName.endsWith("/feature/handler.ts"),
|
|
302
|
+
test("payload-shape mismatch triggers a property-error", () => {
|
|
303
|
+
const handlerErrors = allDiagnostics.filter((d) =>
|
|
304
|
+
d.file?.fileName.endsWith("/feature/handler-payload-mismatch.ts"),
|
|
302
305
|
);
|
|
303
|
-
// TS2353 = "Object literal may only specify known properties, and
|
|
304
|
-
// 'bogus' does not exist in type". This is the property-level
|
|
305
|
-
// strict-check we promised.
|
|
306
306
|
const propErrors = handlerErrors.filter((d) => d.code === 2353);
|
|
307
307
|
expect(propErrors.length).toBeGreaterThan(0);
|
|
308
308
|
const flattened = propErrors
|
|
@@ -311,151 +311,26 @@ export const placeOrder = defineWriteHandler({
|
|
|
311
311
|
expect(flattened).toMatch(/'bogus'/);
|
|
312
312
|
});
|
|
313
313
|
|
|
314
|
-
test("direct framework-import + augmentation-included compiles strict too", {
|
|
315
|
-
|
|
316
|
-
}, () => {
|
|
317
|
-
// Sanity-Check: in einem isolated app-tsc (tmp-fixture mit paths-
|
|
318
|
-
// mapping zur framework-source UND .kumiko/types.generated.d.ts im
|
|
319
|
-
// include-Glob) greift strict-mode auch beim direct framework-import.
|
|
320
|
-
// Generic-function-inference nimmt die augmentation am use-site wahr.
|
|
321
|
-
//
|
|
322
|
-
// Konsequenz: der Wrapper ist NICHT der einzige Weg zu strict —
|
|
323
|
-
// aber er ist DER ROBUSTE Weg. Er importiert `types.generated`
|
|
324
|
-
// explicit als side-effect, sodass die Augmentation auch in
|
|
325
|
-
// partial-builds / IDE-Sprachserver-stati garantiert visible ist.
|
|
326
|
-
// Direkter Import setzt voraus, dass das tsconfig-Setup stimmt.
|
|
327
|
-
//
|
|
328
|
-
// Die alte "K=never"-Beobachtung aus den 13 Probes war im
|
|
329
|
-
// bundled-features-Compile, wo das `.kumiko/`-Output nicht im
|
|
330
|
-
// include-Glob lag — die Augmentation aus inline `declare module`
|
|
331
|
-
// hatte einen anderen Resolution-Pfad. Der Wrapper bleibt der
|
|
332
|
-
// empfohlene Pfad für Apps, weil er diese Setup-Sensibilität wegabstrahiert.
|
|
333
|
-
const appRoot = setupApp();
|
|
334
|
-
runCodegen({ appRoot });
|
|
335
|
-
|
|
336
|
-
write(
|
|
337
|
-
appRoot,
|
|
338
|
-
"src/feature/handler-direct.ts",
|
|
339
|
-
`import { defineWriteHandler } from "@cosmicdrift/kumiko-framework/engine";
|
|
340
|
-
import { z } from "zod";
|
|
341
|
-
|
|
342
|
-
export const placeOrder = defineWriteHandler({
|
|
343
|
-
name: "orders.placeOrder",
|
|
344
|
-
schema: z.object({}),
|
|
345
|
-
access: { roles: ["Admin"] },
|
|
346
|
-
handler: async (_event, ctx) => {
|
|
347
|
-
await ctx.appendEvent({
|
|
348
|
-
aggregateId: "x",
|
|
349
|
-
aggregateType: "order",
|
|
350
|
-
type: "orders:event:placed",
|
|
351
|
-
payload: { orderId: "o1", customerId: "c1", amount: 99 },
|
|
352
|
-
});
|
|
353
|
-
return { isSuccess: true as const, data: { id: "o1" } };
|
|
354
|
-
},
|
|
355
|
-
});
|
|
356
|
-
`,
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
const diagnostics = compileApp(appRoot);
|
|
360
|
-
const handlerErrors = diagnostics.filter((d) =>
|
|
314
|
+
test("direct framework-import + augmentation-included compiles strict too", () => {
|
|
315
|
+
const handlerErrors = allDiagnostics.filter((d) =>
|
|
361
316
|
d.file?.fileName.endsWith("/feature/handler-direct.ts"),
|
|
362
317
|
);
|
|
363
|
-
// Good call should compile — augmentation is visible via include of
|
|
364
|
-
// `.kumiko/types.generated.d.ts`.
|
|
365
318
|
expect(handlerErrors).toHaveLength(0);
|
|
366
319
|
});
|
|
367
320
|
|
|
368
|
-
test("eventDef.name pattern: literal-typed name resolves to correct payload-shape", {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
// Marten pattern: `const placed = r.defineEvent(...)`, then
|
|
372
|
-
// `type: placed.name` in appendEvent. This requires `EventDef.name`
|
|
373
|
-
// to be LITERAL-typed (`"orders:event:placed"`, NOT `string`) —
|
|
374
|
-
// otherwise the lookup collapses to `string` and the strict check
|
|
375
|
-
// silently disappears.
|
|
376
|
-
//
|
|
377
|
-
// This test catches regressions in `EventDef<TPayload, TName>` and
|
|
378
|
-
// the `<const TInner>` inference in `defineFeature`/`defineEvent`
|
|
379
|
-
// — both have to cooperate so that `placed.name` resolves as a
|
|
380
|
-
// literal into the `KumikoEventTypeMap` key.
|
|
381
|
-
//
|
|
382
|
-
// Setup: `setupAppWithExports` returns `{ placed }` from the
|
|
383
|
-
// defineFeature callback so handler modules can read it as
|
|
384
|
-
// `ordersFeature.exports.placed.name`.
|
|
385
|
-
const appRoot = setupAppWithExports();
|
|
386
|
-
runCodegen({ appRoot });
|
|
387
|
-
|
|
388
|
-
write(
|
|
389
|
-
appRoot,
|
|
390
|
-
"src/feature/handler-byname.ts",
|
|
391
|
-
`import { defineWriteHandler } from "../../.kumiko/define";
|
|
392
|
-
import { z } from "zod";
|
|
393
|
-
import { ordersFeature } from "./feature";
|
|
394
|
-
|
|
395
|
-
const { placed } = ordersFeature.exports;
|
|
396
|
-
|
|
397
|
-
export const placeOrder = defineWriteHandler({
|
|
398
|
-
name: "orders.placeOrder",
|
|
399
|
-
schema: z.object({}),
|
|
400
|
-
access: { roles: ["Admin"] },
|
|
401
|
-
handler: async (_event, ctx) => {
|
|
402
|
-
await ctx.appendEvent({
|
|
403
|
-
aggregateId: "x",
|
|
404
|
-
aggregateType: "order",
|
|
405
|
-
type: placed.name,
|
|
406
|
-
payload: { orderId: "o1", customerId: "c1", amount: 99 },
|
|
407
|
-
});
|
|
408
|
-
return { isSuccess: true as const, data: { id: "o1" } };
|
|
409
|
-
},
|
|
410
|
-
});
|
|
411
|
-
`,
|
|
412
|
-
);
|
|
413
|
-
|
|
414
|
-
const goodDiagnostics = compileApp(appRoot);
|
|
415
|
-
const goodErrors = goodDiagnostics.filter((d) =>
|
|
416
|
-
d.file?.fileName.endsWith("/feature/handler-byname.ts"),
|
|
321
|
+
test("eventDef.name pattern: literal-typed name resolves to correct payload-shape", () => {
|
|
322
|
+
const goodErrors = allDiagnostics.filter((d) =>
|
|
323
|
+
d.file?.fileName.endsWith("/feature/handler-byname-good.ts"),
|
|
417
324
|
);
|
|
418
325
|
if (goodErrors.length > 0) {
|
|
419
326
|
const msgs = goodErrors
|
|
420
327
|
.map((d) => ` TS${d.code}: ${ts.flattenDiagnosticMessageText(d.messageText, "\n")}`)
|
|
421
328
|
.join("\n");
|
|
422
|
-
throw new Error(`expected handler-byname.ts to compile cleanly, got:\n${msgs}`);
|
|
329
|
+
throw new Error(`expected handler-byname-good.ts to compile cleanly, got:\n${msgs}`);
|
|
423
330
|
}
|
|
424
331
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
// wäre, würde TS hier eine `Record<string, unknown>` annehmen und
|
|
428
|
-
// die extra property NICHT melden. TS2353 hier beweist die
|
|
429
|
-
// literal-typed Auflösung über `.name`.
|
|
430
|
-
write(
|
|
431
|
-
appRoot,
|
|
432
|
-
"src/feature/handler-byname.ts",
|
|
433
|
-
`import { defineWriteHandler } from "../../.kumiko/define";
|
|
434
|
-
import { z } from "zod";
|
|
435
|
-
import { ordersFeature } from "./feature";
|
|
436
|
-
|
|
437
|
-
const { placed } = ordersFeature.exports;
|
|
438
|
-
|
|
439
|
-
export const placeOrder = defineWriteHandler({
|
|
440
|
-
name: "orders.placeOrder",
|
|
441
|
-
schema: z.object({}),
|
|
442
|
-
access: { roles: ["Admin"] },
|
|
443
|
-
handler: async (_event, ctx) => {
|
|
444
|
-
await ctx.appendEvent({
|
|
445
|
-
aggregateId: "x",
|
|
446
|
-
aggregateType: "order",
|
|
447
|
-
type: placed.name,
|
|
448
|
-
payload: { orderId: "o1", customerId: "c1", amount: 99, bogus: "extra" },
|
|
449
|
-
});
|
|
450
|
-
return { isSuccess: true as const, data: { id: "o1" } };
|
|
451
|
-
},
|
|
452
|
-
});
|
|
453
|
-
`,
|
|
454
|
-
);
|
|
455
|
-
|
|
456
|
-
const badDiagnostics = compileApp(appRoot);
|
|
457
|
-
const badErrors = badDiagnostics.filter((d) =>
|
|
458
|
-
d.file?.fileName.endsWith("/feature/handler-byname.ts"),
|
|
332
|
+
const badErrors = allDiagnostics.filter((d) =>
|
|
333
|
+
d.file?.fileName.endsWith("/feature/handler-byname-bad.ts"),
|
|
459
334
|
);
|
|
460
335
|
const propErrors = badErrors.filter((d) => d.code === 2353);
|
|
461
336
|
expect(propErrors.length).toBeGreaterThan(0);
|
package/src/codegen/render.ts
CHANGED
|
@@ -192,6 +192,7 @@ export function renderDefineFile(): string {
|
|
|
192
192
|
`import type {`,
|
|
193
193
|
` KumikoEventTypeMap,`,
|
|
194
194
|
` WriteHandlerDefinition,`,
|
|
195
|
+
` WriteHandlerInput,`,
|
|
195
196
|
` QueryHandlerDefinition,`,
|
|
196
197
|
`} from "@cosmicdrift/kumiko-framework/engine";`,
|
|
197
198
|
`import type { ZodType } from "zod";`,
|
|
@@ -204,7 +205,7 @@ export function renderDefineFile(): string {
|
|
|
204
205
|
` TSchema extends ZodType,`,
|
|
205
206
|
` TData = unknown,`,
|
|
206
207
|
`>(`,
|
|
207
|
-
` def:
|
|
208
|
+
` def: WriteHandlerInput<TName, TSchema, TData, KumikoEventTypeMap>,`,
|
|
208
209
|
`): WriteHandlerDefinition<TName, TSchema, TData, KumikoEventTypeMap> {`,
|
|
209
210
|
` return fwDefineWriteHandler<TName, TSchema, TData, KumikoEventTypeMap>(def);`,
|
|
210
211
|
`}`,
|