@cosmicdrift/kumiko-framework 0.2.3 → 0.4.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 +93 -0
- package/package.json +124 -39
- package/src/__tests__/full-stack.integration.ts +2 -2
- package/src/api/auth-routes.ts +5 -5
- package/src/api/jwt.ts +2 -2
- package/src/api/route-registrars.ts +1 -1
- package/src/api/routes.ts +3 -3
- package/src/api/server.ts +6 -7
- package/src/compliance/profiles.ts +8 -8
- package/src/db/assert-exists-in.ts +2 -2
- package/src/db/cursor.ts +3 -3
- package/src/db/event-store-executor.ts +19 -13
- package/src/db/located-timestamp.ts +1 -1
- package/src/db/money.ts +12 -2
- package/src/db/pg-error.ts +1 -1
- package/src/db/row-helpers.ts +1 -1
- package/src/db/table-builder.ts +3 -5
- package/src/db/tenant-db.ts +9 -9
- package/src/engine/__tests__/_pipeline-test-utils.ts +23 -0
- package/src/engine/__tests__/build-target.test.ts +135 -0
- package/src/engine/__tests__/codemod-pipeline.test.ts +551 -0
- package/src/engine/__tests__/entity-handlers.test.ts +3 -3
- package/src/engine/__tests__/event-helpers.test.ts +4 -4
- package/src/engine/__tests__/pipeline-engine.test.ts +215 -0
- package/src/engine/__tests__/pipeline-handler.integration.ts +894 -0
- package/src/engine/__tests__/pipeline-observability.integration.ts +142 -0
- package/src/engine/__tests__/pipeline-performance.integration.ts +152 -0
- package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +288 -0
- package/src/engine/__tests__/raw-table.test.ts +2 -2
- package/src/engine/__tests__/steps-aggregate-append-event.test.ts +115 -0
- package/src/engine/__tests__/steps-aggregate-create.test.ts +92 -0
- package/src/engine/__tests__/steps-aggregate-update.test.ts +127 -0
- package/src/engine/__tests__/steps-call-feature.test.ts +123 -0
- package/src/engine/__tests__/steps-mail-send.test.ts +136 -0
- package/src/engine/__tests__/steps-read.test.ts +142 -0
- package/src/engine/__tests__/steps-resolver-utils.test.ts +50 -0
- package/src/engine/__tests__/steps-unsafe-projection-delete.test.ts +69 -0
- package/src/engine/__tests__/steps-unsafe-projection-upsert.test.ts +117 -0
- package/src/engine/__tests__/steps-webhook-send.test.ts +135 -0
- package/src/engine/__tests__/steps-workflow.test.ts +198 -0
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +491 -0
- package/src/engine/__tests__/visual-tree-patterns.test.ts +251 -0
- package/src/engine/boot-validator/api-ext.ts +77 -0
- package/src/engine/boot-validator/config-deps.ts +163 -0
- package/src/engine/boot-validator/entity-handler.ts +466 -0
- package/src/engine/boot-validator/index.ts +159 -0
- package/src/engine/boot-validator/ownership.ts +198 -0
- package/src/engine/boot-validator/pii-retention.ts +155 -0
- package/src/engine/boot-validator/screens-nav.ts +624 -0
- package/src/engine/boot-validator.ts +1 -1804
- package/src/engine/build-app-schema.ts +1 -1
- package/src/engine/build-target.ts +99 -0
- package/src/engine/codemod/index.ts +15 -0
- package/src/engine/codemod/pipeline-codemod.ts +641 -0
- package/src/engine/config-helpers.ts +9 -19
- package/src/engine/constants.ts +1 -1
- package/src/engine/define-feature.ts +88 -9
- package/src/engine/define-handler.ts +89 -3
- package/src/engine/define-roles.ts +2 -2
- package/src/engine/define-step.ts +28 -0
- package/src/engine/define-workflow.ts +110 -0
- package/src/engine/entity-handlers.ts +10 -9
- package/src/engine/event-helpers.ts +4 -4
- package/src/engine/factories.ts +12 -12
- package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +184 -0
- package/src/engine/feature-ast/extractors/index.ts +74 -0
- package/src/engine/feature-ast/extractors/round1.ts +110 -0
- package/src/engine/feature-ast/extractors/round2.ts +253 -0
- package/src/engine/feature-ast/extractors/round3.ts +471 -0
- package/src/engine/feature-ast/extractors/round4.ts +1365 -0
- package/src/engine/feature-ast/extractors/round5.ts +72 -0
- package/src/engine/feature-ast/extractors/round6.ts +66 -0
- package/src/engine/feature-ast/extractors/shared.ts +177 -0
- package/src/engine/feature-ast/parse.ts +7 -0
- package/src/engine/feature-ast/patch.ts +9 -1
- package/src/engine/feature-ast/patcher.ts +10 -3
- package/src/engine/feature-ast/patterns.ts +49 -1
- package/src/engine/feature-ast/render.ts +17 -1
- package/src/engine/index.ts +44 -2
- package/src/engine/pattern-library/__tests__/library.test.ts +6 -0
- package/src/engine/pattern-library/library.ts +42 -2
- package/src/engine/pipeline.ts +88 -0
- package/src/engine/projection-helpers.ts +1 -1
- package/src/engine/read-claim.ts +1 -1
- package/src/engine/registry.ts +30 -2
- package/src/engine/resolve-config-or-param.ts +4 -0
- package/src/engine/run-pipeline.ts +162 -0
- package/src/engine/schema-builder.ts +2 -4
- package/src/engine/state-machine.ts +1 -1
- package/src/engine/steps/_drizzle-boundary.ts +19 -0
- package/src/engine/steps/_duration-utils.ts +33 -0
- package/src/engine/steps/_no-return-guard.ts +21 -0
- package/src/engine/steps/_resolver-utils.ts +42 -0
- package/src/engine/steps/_step-dispatch-constants.ts +38 -0
- package/src/engine/steps/aggregate-append-event.ts +56 -0
- package/src/engine/steps/aggregate-create.ts +56 -0
- package/src/engine/steps/aggregate-update.ts +68 -0
- package/src/engine/steps/branch.ts +84 -0
- package/src/engine/steps/call-feature.ts +49 -0
- package/src/engine/steps/compute.ts +41 -0
- package/src/engine/steps/for-each.ts +111 -0
- package/src/engine/steps/mail-send.ts +44 -0
- package/src/engine/steps/read-find-many.ts +51 -0
- package/src/engine/steps/read-find-one.ts +58 -0
- package/src/engine/steps/retry.ts +87 -0
- package/src/engine/steps/return.ts +34 -0
- package/src/engine/steps/unsafe-projection-delete.ts +46 -0
- package/src/engine/steps/unsafe-projection-upsert.ts +69 -0
- package/src/engine/steps/wait-for-event.ts +71 -0
- package/src/engine/steps/wait.ts +69 -0
- package/src/engine/steps/webhook-send.ts +71 -0
- package/src/engine/system-user.ts +1 -1
- package/src/engine/types/feature.ts +93 -1
- package/src/engine/types/handlers.ts +18 -10
- package/src/engine/types/index.ts +11 -1
- package/src/engine/types/step.ts +334 -0
- package/src/engine/types/target-ref.ts +21 -0
- package/src/engine/types/tree-node.ts +132 -0
- package/src/engine/types/workspace.ts +7 -0
- package/src/engine/validate-projection-allowlist.ts +161 -0
- package/src/event-store/snapshot.ts +1 -1
- package/src/event-store/upcaster-dead-letter.ts +1 -1
- package/src/event-store/upcaster.ts +1 -1
- package/src/files/file-routes.ts +1 -1
- package/src/files/types.ts +2 -2
- package/src/jobs/job-runner.ts +10 -10
- package/src/lifecycle/lifecycle.ts +0 -3
- package/src/logging/index.ts +1 -0
- package/src/logging/pino-logger.ts +11 -7
- package/src/logging/utils.ts +24 -0
- package/src/observability/prometheus-meter.ts +7 -5
- package/src/pipeline/__tests__/archive-stream.integration.ts +1 -1
- package/src/pipeline/__tests__/causation-chain.integration.ts +1 -1
- package/src/pipeline/__tests__/domain-events-projections.integration.ts +3 -3
- package/src/pipeline/__tests__/event-define-event-strict.integration.ts +4 -4
- package/src/pipeline/__tests__/load-aggregate-query.integration.ts +1 -1
- package/src/pipeline/__tests__/msp-multi-hop.integration.ts +3 -3
- package/src/pipeline/__tests__/msp-rebuild.integration.ts +3 -3
- package/src/pipeline/__tests__/multi-stream-projection.integration.ts +2 -2
- package/src/pipeline/__tests__/query-projection.integration.ts +5 -5
- package/src/pipeline/append-event-core.ts +22 -6
- package/src/pipeline/dispatcher-utils.ts +188 -0
- package/src/pipeline/dispatcher.ts +63 -283
- package/src/pipeline/distributed-lock.ts +1 -1
- package/src/pipeline/entity-cache.ts +2 -2
- package/src/pipeline/event-consumer-state.ts +0 -13
- package/src/pipeline/event-dispatcher.ts +4 -4
- package/src/pipeline/index.ts +0 -2
- package/src/pipeline/lifecycle-pipeline.ts +6 -12
- package/src/pipeline/msp-rebuild.ts +5 -5
- package/src/pipeline/multi-stream-apply-context.ts +6 -7
- package/src/pipeline/projection-rebuild.ts +2 -2
- package/src/pipeline/projection-state.ts +0 -12
- package/src/rate-limit/__tests__/resolver.integration.ts +8 -4
- package/src/rate-limit/resolver.ts +1 -1
- package/src/search/in-memory-adapter.ts +1 -1
- package/src/search/meilisearch-adapter.ts +3 -3
- package/src/search/types.ts +1 -1
- package/src/secrets/leak-guard.ts +2 -2
- package/src/stack/request-helper.ts +9 -5
- package/src/stack/test-stack.ts +1 -1
- package/src/testing/handler-context.ts +4 -4
- package/src/testing/http-cookies.ts +1 -1
- package/src/time/tz-context.ts +1 -2
- package/src/ui-types/index.ts +4 -0
- package/src/engine/feature-ast/extractors.ts +0 -2602
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// Pipeline-engine unit tests — defineWriteHandler({ perform: pipeline(...) })
|
|
2
|
+
// boundary, run-pipeline runner contract, defineStep registry guards.
|
|
3
|
+
// Sub-step-builders (branch, forEach) live in pipeline-sub-pipelines.test.ts;
|
|
4
|
+
// boot-validator in validate-projection-allowlist.test.ts.
|
|
5
|
+
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
import { TestUsers } from "../../stack";
|
|
10
|
+
import { defineWriteHandler } from "../define-handler";
|
|
11
|
+
import { defineStep } from "../define-step";
|
|
12
|
+
import { pipeline } from "../pipeline";
|
|
13
|
+
import type { WriteEvent } from "../types/handlers";
|
|
14
|
+
import { buildMinimalCtx } from "./_pipeline-test-utils";
|
|
15
|
+
|
|
16
|
+
describe("pipeline engine (return / compute / registry guards)", () => {
|
|
17
|
+
it("compiles a perform-block into a callable handler that returns the resolver's WriteResult", async () => {
|
|
18
|
+
const handlerDef = defineWriteHandler({
|
|
19
|
+
name: "demo:noop",
|
|
20
|
+
schema: z.object({ greeting: z.string() }),
|
|
21
|
+
access: { roles: ["User"] },
|
|
22
|
+
perform: pipeline<{ greeting: string }, { echoed: string }>(({ event, r }) => [
|
|
23
|
+
r.step.return(() => ({
|
|
24
|
+
isSuccess: true as const,
|
|
25
|
+
data: { echoed: event.payload.greeting },
|
|
26
|
+
})),
|
|
27
|
+
]),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(typeof handlerDef.handler).toBe("function");
|
|
31
|
+
expect(handlerDef.perform).toBeDefined();
|
|
32
|
+
expect(handlerDef.perform?.__kind).toBe("pipeline");
|
|
33
|
+
|
|
34
|
+
const event: WriteEvent<{ greeting: string }> = {
|
|
35
|
+
type: "demo:noop",
|
|
36
|
+
payload: { greeting: "hello" },
|
|
37
|
+
user: TestUsers.admin,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const result = await handlerDef.handler(event, buildMinimalCtx());
|
|
41
|
+
expect(result.isSuccess).toBe(true);
|
|
42
|
+
if (result.isSuccess) {
|
|
43
|
+
expect(result.data).toEqual({ echoed: "hello" });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("supports a static (non-resolver) WriteResult passed to r.step.return", async () => {
|
|
48
|
+
const handlerDef = defineWriteHandler({
|
|
49
|
+
name: "demo:static",
|
|
50
|
+
schema: z.object({}),
|
|
51
|
+
access: { roles: ["User"] },
|
|
52
|
+
perform: pipeline<Record<string, never>, { ok: boolean }>(({ r }) => [
|
|
53
|
+
r.step.return({ isSuccess: true as const, data: { ok: true } }),
|
|
54
|
+
]),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const result = await handlerDef.handler(
|
|
58
|
+
{ type: "demo:static", payload: {}, user: TestUsers.admin },
|
|
59
|
+
buildMinimalCtx(),
|
|
60
|
+
);
|
|
61
|
+
expect(result).toEqual({ isSuccess: true, data: { ok: true } });
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("preserves the free-form handler path unchanged", async () => {
|
|
65
|
+
const handlerDef = defineWriteHandler({
|
|
66
|
+
name: "demo:freeform",
|
|
67
|
+
schema: z.object({ n: z.number() }),
|
|
68
|
+
access: { roles: ["User"] },
|
|
69
|
+
handler: async (event) => ({
|
|
70
|
+
isSuccess: true as const,
|
|
71
|
+
data: { doubled: event.payload.n * 2 },
|
|
72
|
+
}),
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(handlerDef.perform).toBeUndefined();
|
|
76
|
+
|
|
77
|
+
const result = await handlerDef.handler(
|
|
78
|
+
{ type: "demo:freeform", payload: { n: 21 }, user: TestUsers.admin },
|
|
79
|
+
buildMinimalCtx(),
|
|
80
|
+
);
|
|
81
|
+
expect(result.isSuccess).toBe(true);
|
|
82
|
+
if (result.isSuccess) {
|
|
83
|
+
expect(result.data).toEqual({ doubled: 42 });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("surfaces an explicit error when a pipeline ends without r.step.return", async () => {
|
|
88
|
+
const handlerDef = defineWriteHandler({
|
|
89
|
+
name: "demo:no-return",
|
|
90
|
+
schema: z.object({}),
|
|
91
|
+
access: { roles: ["User"] },
|
|
92
|
+
perform: pipeline<Record<string, never>, never>(() => []),
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await expect(
|
|
96
|
+
handlerDef.handler(
|
|
97
|
+
{ type: "demo:no-return", payload: {}, user: TestUsers.admin },
|
|
98
|
+
buildMinimalCtx(),
|
|
99
|
+
),
|
|
100
|
+
).rejects.toThrow(/r\.step\.return/);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("threads compute results into subsequent step resolvers via steps.<name>", async () => {
|
|
104
|
+
const handlerDef = defineWriteHandler({
|
|
105
|
+
name: "demo:thread",
|
|
106
|
+
schema: z.object({ base: z.number() }),
|
|
107
|
+
access: { roles: ["User"] },
|
|
108
|
+
perform: pipeline<{ base: number }, { sum: number }>(({ event, r }) => [
|
|
109
|
+
r.step.compute("offset", () => 10),
|
|
110
|
+
r.step.compute("doubledBase", () => event.payload.base * 2),
|
|
111
|
+
r.step.return(({ steps }) => ({
|
|
112
|
+
isSuccess: true as const,
|
|
113
|
+
data: {
|
|
114
|
+
sum: (steps["offset"] as number) + (steps["doubledBase"] as number),
|
|
115
|
+
},
|
|
116
|
+
})),
|
|
117
|
+
]),
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const result = await handlerDef.handler(
|
|
121
|
+
{ type: "demo:thread", payload: { base: 5 }, user: TestUsers.admin },
|
|
122
|
+
buildMinimalCtx(),
|
|
123
|
+
);
|
|
124
|
+
expect(result.isSuccess).toBe(true);
|
|
125
|
+
if (result.isSuccess) {
|
|
126
|
+
expect(result.data).toEqual({ sum: 20 });
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it("re-evaluates compute resolvers per pipeline run (not at build time)", async () => {
|
|
131
|
+
let counter = 0;
|
|
132
|
+
const handlerDef = defineWriteHandler({
|
|
133
|
+
name: "demo:fresh",
|
|
134
|
+
schema: z.object({}),
|
|
135
|
+
access: { roles: ["User"] },
|
|
136
|
+
perform: pipeline<Record<string, never>, { tick: number }>(({ r }) => [
|
|
137
|
+
r.step.compute("tick", () => ++counter),
|
|
138
|
+
r.step.return(({ steps }) => ({
|
|
139
|
+
isSuccess: true as const,
|
|
140
|
+
data: { tick: steps["tick"] as number },
|
|
141
|
+
})),
|
|
142
|
+
]),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const a = await handlerDef.handler(
|
|
146
|
+
{ type: "demo:fresh", payload: {}, user: TestUsers.admin },
|
|
147
|
+
buildMinimalCtx(),
|
|
148
|
+
);
|
|
149
|
+
const b = await handlerDef.handler(
|
|
150
|
+
{ type: "demo:fresh", payload: {}, user: TestUsers.admin },
|
|
151
|
+
buildMinimalCtx(),
|
|
152
|
+
);
|
|
153
|
+
if (a.isSuccess && b.isSuccess) {
|
|
154
|
+
expect(a.data.tick).toBe(1);
|
|
155
|
+
expect(b.data.tick).toBe(2);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("rejects a step with an unknown kind at runtime", async () => {
|
|
160
|
+
const handlerDef = defineWriteHandler({
|
|
161
|
+
name: "demo:unknown-kind",
|
|
162
|
+
schema: z.object({}),
|
|
163
|
+
access: { roles: ["User"] },
|
|
164
|
+
perform: pipeline<Record<string, never>, never>(() => [
|
|
165
|
+
// Hand-crafted instance with a kind that's never been registered —
|
|
166
|
+
// simulates a typo in a future step-builder factory.
|
|
167
|
+
{ kind: "this-step-does-not-exist", args: {} },
|
|
168
|
+
]),
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
await expect(
|
|
172
|
+
handlerDef.handler(
|
|
173
|
+
{ type: "demo:unknown-kind", payload: {}, user: TestUsers.admin },
|
|
174
|
+
buildMinimalCtx(),
|
|
175
|
+
),
|
|
176
|
+
).rejects.toThrow(/Unknown step kind "this-step-does-not-exist"/);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("defineWriteHandler throws a clear runtime error when both handler+perform are set (#3)", () => {
|
|
180
|
+
// Type-system rejects this via discriminated union (handler?: never /
|
|
181
|
+
// perform?: never), but the error message ('Type X not assignable to
|
|
182
|
+
// undefined') is opaque. The runtime guard fires regardless of how
|
|
183
|
+
// the type-error was bypassed (any-cast, generated code, JS-call) and
|
|
184
|
+
// names BOTH fields explicitly.
|
|
185
|
+
// Deliberately bypass the discriminated union via `as never` so the
|
|
186
|
+
// runtime guard is what's under test (the type-system path already
|
|
187
|
+
// rejects the conflict).
|
|
188
|
+
const conflictingDef = {
|
|
189
|
+
name: "demo:both",
|
|
190
|
+
schema: z.object({}),
|
|
191
|
+
access: { roles: ["User"] },
|
|
192
|
+
handler: async () => ({ isSuccess: true as const, data: {} }),
|
|
193
|
+
perform: pipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
194
|
+
r.step.return({ isSuccess: true as const, data: { ok: true } }),
|
|
195
|
+
]),
|
|
196
|
+
} as unknown as Parameters<typeof defineWriteHandler>[0];
|
|
197
|
+
expect(() => defineWriteHandler(conflictingDef)).toThrow(
|
|
198
|
+
/both `handler` and `perform` are set/,
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("defineStep throws when the same kind is registered with a different definition", () => {
|
|
203
|
+
// Unique-per-run kind so this test is safe under vitest --watch
|
|
204
|
+
// (where the file may re-execute in the same process). Without the
|
|
205
|
+
// unique-kind, the first defineStep call on the second run would
|
|
206
|
+
// already throw against the registration left from the first run.
|
|
207
|
+
const kind = `test-only:duplicate-guard:${randomUUID()}`;
|
|
208
|
+
|
|
209
|
+
defineStep({ kind, defaultFailureStrategy: "throw", run: () => undefined });
|
|
210
|
+
|
|
211
|
+
expect(() =>
|
|
212
|
+
defineStep({ kind, defaultFailureStrategy: "throw", run: () => "different" }),
|
|
213
|
+
).toThrow(/already registered/);
|
|
214
|
+
});
|
|
215
|
+
});
|