@cosmicdrift/kumiko-framework 0.146.1 → 0.146.3
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/package.json +2 -2
- package/src/api/auth-routes.ts +4 -5
- package/src/engine/__tests__/codemod-pipeline.test.ts +13 -11
- package/src/engine/__tests__/pipeline-engine.test.ts +9 -9
- package/src/engine/__tests__/pipeline-handler.integration.test.ts +18 -18
- package/src/engine/__tests__/pipeline-observability.integration.test.ts +2 -2
- package/src/engine/__tests__/pipeline-performance.integration.test.ts +3 -3
- package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +9 -9
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +15 -15
- package/src/engine/codemod/pipeline-codemod.ts +5 -5
- package/src/engine/define-handler.ts +2 -2
- package/src/engine/define-workflow.ts +1 -1
- package/src/engine/index.ts +7 -5
- package/src/engine/membership-roles.ts +13 -0
- package/src/engine/pipeline.ts +6 -11
- package/src/engine/run-pipeline.ts +1 -1
- package/src/engine/types/handlers.ts +1 -1
- package/src/engine/types/step.ts +4 -4
- package/src/engine/validate-projection-allowlist.ts +1 -1
- package/src/pipeline/dispatch-batch.ts +187 -0
- package/src/pipeline/dispatch-query.ts +165 -0
- package/src/pipeline/dispatch-shared.ts +685 -0
- package/src/pipeline/dispatch-write.ts +451 -0
- package/src/pipeline/dispatcher.ts +30 -1374
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.146.
|
|
3
|
+
"version": "0.146.3",
|
|
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>",
|
|
@@ -189,7 +189,7 @@
|
|
|
189
189
|
"zod": "^4.4.3"
|
|
190
190
|
},
|
|
191
191
|
"devDependencies": {
|
|
192
|
-
"@cosmicdrift/kumiko-dispatcher-live": "0.146.
|
|
192
|
+
"@cosmicdrift/kumiko-dispatcher-live": "0.146.3",
|
|
193
193
|
"bun-types": "^1.3.13",
|
|
194
194
|
"pino-pretty": "^13.1.3"
|
|
195
195
|
},
|
package/src/api/auth-routes.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { Context } from "hono";
|
|
|
2
2
|
import { Hono } from "hono";
|
|
3
3
|
import { deleteCookie, setCookie } from "hono/cookie";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
-
import {
|
|
5
|
+
import { buildSessionRoles } from "../engine/membership-roles";
|
|
6
6
|
import { createSystemUser } from "../engine/system-user";
|
|
7
7
|
import { type SessionUser, SYSTEM_TENANT_ID, type TenantId } from "../engine/types";
|
|
8
8
|
import { NotFoundError } from "../errors";
|
|
@@ -870,13 +870,12 @@ export function createAuthRoutes(
|
|
|
870
870
|
// tenant A accidentally surviving into tenant B's session). The
|
|
871
871
|
// resolver runs each feature's r.authClaims() hook under the new
|
|
872
872
|
// TenantDb scope.
|
|
873
|
-
//
|
|
873
|
+
// buildSessionRoles calls stripForbiddenMembershipRoles internally and
|
|
874
|
+
// strips reserved roles from the membership side only — globalRoles
|
|
874
875
|
// (where SystemAdmin legitimately lives) is never filtered. Backstop for a
|
|
875
876
|
// membership role that a projection rebuild resurrected past command-time
|
|
876
877
|
// validation (see engine/membership-roles).
|
|
877
|
-
const mergedRoles =
|
|
878
|
-
new Set([...globalRoles, ...stripForbiddenMembershipRoles(membership.roles)]),
|
|
879
|
-
);
|
|
878
|
+
const mergedRoles = buildSessionRoles(globalRoles, membership.roles);
|
|
880
879
|
const targetSession: SessionUser = {
|
|
881
880
|
id: user.id,
|
|
882
881
|
tenantId: targetTenantId,
|
|
@@ -103,14 +103,14 @@ export const myHandler = defineWriteHandler({
|
|
|
103
103
|
`;
|
|
104
104
|
|
|
105
105
|
const alreadyPipelineContent = `\
|
|
106
|
-
import { access, defineWriteHandler,
|
|
106
|
+
import { access, defineWriteHandler, stepsPipeline } from "@cosmicdrift/kumiko-framework/engine";
|
|
107
107
|
import { z } from "zod";
|
|
108
108
|
|
|
109
109
|
export const myHandler = defineWriteHandler({
|
|
110
110
|
name: "test:pipeline",
|
|
111
111
|
schema: z.object({}),
|
|
112
112
|
access: { roles: access.authenticated },
|
|
113
|
-
perform:
|
|
113
|
+
perform: stepsPipeline(({ event, r }) => [
|
|
114
114
|
r.step.return((ctx) => ({ isSuccess: true, data: { ok: true } })),
|
|
115
115
|
]),
|
|
116
116
|
});
|
|
@@ -173,14 +173,14 @@ describe("analyzeFile", () => {
|
|
|
173
173
|
|
|
174
174
|
describe("convertFile", () => {
|
|
175
175
|
describe("static return handler", () => {
|
|
176
|
-
it("converts handler to perform:
|
|
176
|
+
it("converts handler to perform: stepsPipeline(...)", async () => {
|
|
177
177
|
const p = writeFixture("static-convert.write.ts", staticReturnContent);
|
|
178
178
|
const result = await convertFile(p);
|
|
179
179
|
expect(result.status).toBe("converted");
|
|
180
180
|
const content = readFileSync(p, "utf8");
|
|
181
|
-
expect(content).toContain("perform:
|
|
181
|
+
expect(content).toContain("perform: stepsPipeline");
|
|
182
182
|
expect(content).toContain("r.step.return");
|
|
183
|
-
expect(content).toContain("import { access, defineWriteHandler,
|
|
183
|
+
expect(content).toContain("import { access, defineWriteHandler, stepsPipeline }");
|
|
184
184
|
expect(content).not.toContain("handler:");
|
|
185
185
|
});
|
|
186
186
|
|
|
@@ -198,7 +198,7 @@ export const h = defineWriteHandler({
|
|
|
198
198
|
const result = await convertFile(p);
|
|
199
199
|
expect(result.status).toBe("converted");
|
|
200
200
|
const converted = readFileSync(p, "utf8");
|
|
201
|
-
expect(converted).toContain("
|
|
201
|
+
expect(converted).toContain("stepsPipeline<typeof MySchema, unknown>");
|
|
202
202
|
});
|
|
203
203
|
});
|
|
204
204
|
|
|
@@ -271,12 +271,12 @@ export const h = defineWriteHandler({
|
|
|
271
271
|
const result = await convertFile(p);
|
|
272
272
|
expect(result.status).toBe("converted");
|
|
273
273
|
const content = readFileSync(p, "utf8");
|
|
274
|
-
expect(content).toContain("import { access, defineWriteHandler,
|
|
274
|
+
expect(content).toContain("import { access, defineWriteHandler, stepsPipeline }");
|
|
275
275
|
});
|
|
276
276
|
|
|
277
277
|
it("does not duplicate pipeline import when already present", async () => {
|
|
278
278
|
const content = `\
|
|
279
|
-
import { access, defineWriteHandler,
|
|
279
|
+
import { access, defineWriteHandler, stepsPipeline } from "@cosmicdrift/kumiko-framework/engine";
|
|
280
280
|
import { z } from "zod";
|
|
281
281
|
|
|
282
282
|
export const h = defineWriteHandler({
|
|
@@ -288,7 +288,9 @@ export const h = defineWriteHandler({
|
|
|
288
288
|
const result = await convertFile(p);
|
|
289
289
|
expect(result.status).toBe("converted");
|
|
290
290
|
const final = readFileSync(p, "utf8").split("\n");
|
|
291
|
-
const pipelineImports = final.filter(
|
|
291
|
+
const pipelineImports = final.filter(
|
|
292
|
+
(l) => l.includes("import") && l.includes("stepsPipeline"),
|
|
293
|
+
);
|
|
292
294
|
// Only one import line should contain "pipeline"
|
|
293
295
|
expect(pipelineImports.length).toBe(1);
|
|
294
296
|
});
|
|
@@ -513,14 +515,14 @@ describe("generatePerformBlock", () => {
|
|
|
513
515
|
it("generates pipeline block for static return", () => {
|
|
514
516
|
const analysis = analyzeHandlerArrow("async () => ({ isSuccess: true, data: { ok: true } })");
|
|
515
517
|
const block = generatePerformBlock(analysis, "", " ");
|
|
516
|
-
expect(block).toContain("perform:
|
|
518
|
+
expect(block).toContain("perform: stepsPipeline(");
|
|
517
519
|
expect(block).toContain("r.step.return((ctx) => ({ isSuccess: true, data: { ok: true } })");
|
|
518
520
|
});
|
|
519
521
|
|
|
520
522
|
it("generates pipeline block with schema type parameter", () => {
|
|
521
523
|
const analysis = analyzeHandlerArrow("async () => ({ isSuccess: true, data: { ok: true } })");
|
|
522
524
|
const block = generatePerformBlock(analysis, "typeof InvoiceSchema", " ");
|
|
523
|
-
expect(block).toContain("
|
|
525
|
+
expect(block).toContain("stepsPipeline<typeof InvoiceSchema, unknown>");
|
|
524
526
|
});
|
|
525
527
|
|
|
526
528
|
it("returns null for non-convertible analysis", () => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Pipeline-engine unit tests — defineWriteHandler({ perform:
|
|
1
|
+
// Pipeline-engine unit tests — defineWriteHandler({ perform: stepsPipeline(...) })
|
|
2
2
|
// boundary, run-pipeline runner contract, defineStep registry guards.
|
|
3
3
|
// Sub-step-builders (branch, forEach) live in pipeline-sub-pipelines.test.ts;
|
|
4
4
|
// boot-validator in validate-projection-allowlist.test.ts.
|
|
@@ -9,7 +9,7 @@ import { z } from "zod";
|
|
|
9
9
|
import { TestUsers } from "../../stack";
|
|
10
10
|
import { defineWriteHandler } from "../define-handler";
|
|
11
11
|
import { defineStep } from "../define-step";
|
|
12
|
-
import {
|
|
12
|
+
import { stepsPipeline } from "../pipeline";
|
|
13
13
|
import type { WriteEvent } from "../types/handlers";
|
|
14
14
|
import { buildMinimalCtx } from "./_pipeline-test-utils";
|
|
15
15
|
|
|
@@ -19,7 +19,7 @@ describe("pipeline engine (return / compute / registry guards)", () => {
|
|
|
19
19
|
name: "demo:noop",
|
|
20
20
|
schema: z.object({ greeting: z.string() }),
|
|
21
21
|
access: { roles: ["User"] },
|
|
22
|
-
perform:
|
|
22
|
+
perform: stepsPipeline<{ greeting: string }, { echoed: string }>(({ event, r }) => [
|
|
23
23
|
r.step.return(() => ({
|
|
24
24
|
isSuccess: true as const,
|
|
25
25
|
data: { echoed: event.payload.greeting },
|
|
@@ -49,7 +49,7 @@ describe("pipeline engine (return / compute / registry guards)", () => {
|
|
|
49
49
|
name: "demo:static",
|
|
50
50
|
schema: z.object({}),
|
|
51
51
|
access: { roles: ["User"] },
|
|
52
|
-
perform:
|
|
52
|
+
perform: stepsPipeline<Record<string, never>, { ok: boolean }>(({ r }) => [
|
|
53
53
|
r.step.return({ isSuccess: true as const, data: { ok: true } }),
|
|
54
54
|
]),
|
|
55
55
|
});
|
|
@@ -89,7 +89,7 @@ describe("pipeline engine (return / compute / registry guards)", () => {
|
|
|
89
89
|
name: "demo:no-return",
|
|
90
90
|
schema: z.object({}),
|
|
91
91
|
access: { roles: ["User"] },
|
|
92
|
-
perform:
|
|
92
|
+
perform: stepsPipeline<Record<string, never>, never>(() => []),
|
|
93
93
|
});
|
|
94
94
|
|
|
95
95
|
await expect(
|
|
@@ -105,7 +105,7 @@ describe("pipeline engine (return / compute / registry guards)", () => {
|
|
|
105
105
|
name: "demo:thread",
|
|
106
106
|
schema: z.object({ base: z.number() }),
|
|
107
107
|
access: { roles: ["User"] },
|
|
108
|
-
perform:
|
|
108
|
+
perform: stepsPipeline<{ base: number }, { sum: number }>(({ event, r }) => [
|
|
109
109
|
r.step.compute("offset", () => 10),
|
|
110
110
|
r.step.compute("doubledBase", () => event.payload.base * 2),
|
|
111
111
|
r.step.return(({ steps }) => ({
|
|
@@ -133,7 +133,7 @@ describe("pipeline engine (return / compute / registry guards)", () => {
|
|
|
133
133
|
name: "demo:fresh",
|
|
134
134
|
schema: z.object({}),
|
|
135
135
|
access: { roles: ["User"] },
|
|
136
|
-
perform:
|
|
136
|
+
perform: stepsPipeline<Record<string, never>, { tick: number }>(({ r }) => [
|
|
137
137
|
r.step.compute("tick", () => ++counter),
|
|
138
138
|
r.step.return(({ steps }) => ({
|
|
139
139
|
isSuccess: true as const,
|
|
@@ -161,7 +161,7 @@ describe("pipeline engine (return / compute / registry guards)", () => {
|
|
|
161
161
|
name: "demo:unknown-kind",
|
|
162
162
|
schema: z.object({}),
|
|
163
163
|
access: { roles: ["User"] },
|
|
164
|
-
perform:
|
|
164
|
+
perform: stepsPipeline<Record<string, never>, never>(() => [
|
|
165
165
|
// Hand-crafted instance with a kind that's never been registered —
|
|
166
166
|
// simulates a typo in a future step-builder factory.
|
|
167
167
|
{ kind: "this-step-does-not-exist", args: {} },
|
|
@@ -190,7 +190,7 @@ describe("pipeline engine (return / compute / registry guards)", () => {
|
|
|
190
190
|
schema: z.object({}),
|
|
191
191
|
access: { roles: ["User"] },
|
|
192
192
|
handler: async () => ({ isSuccess: true as const, data: {} }),
|
|
193
|
-
perform:
|
|
193
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
194
194
|
r.step.return({ isSuccess: true as const, data: { ok: true } }),
|
|
195
195
|
]),
|
|
196
196
|
} as unknown as Parameters<typeof defineWriteHandler>[0];
|
|
@@ -60,7 +60,7 @@ import {
|
|
|
60
60
|
import { defineFeature } from "../define-feature";
|
|
61
61
|
import { defineWriteHandler } from "../define-handler";
|
|
62
62
|
import { createEntity, createTextField } from "../factories";
|
|
63
|
-
import {
|
|
63
|
+
import { stepsPipeline } from "../pipeline";
|
|
64
64
|
|
|
65
65
|
const echoSchema = z.object({ greeting: z.string() });
|
|
66
66
|
|
|
@@ -70,7 +70,7 @@ const echoHandler = defineWriteHandler({
|
|
|
70
70
|
name: "echo",
|
|
71
71
|
schema: echoSchema,
|
|
72
72
|
access: { roles: ["Admin"] },
|
|
73
|
-
perform:
|
|
73
|
+
perform: stepsPipeline<z.infer<typeof echoSchema>, { echoed: string; from: string }>(
|
|
74
74
|
({ event, r }) => [
|
|
75
75
|
r.step.return(() => ({
|
|
76
76
|
isSuccess: true as const,
|
|
@@ -90,7 +90,7 @@ const explodeHandler = defineWriteHandler({
|
|
|
90
90
|
name: "explode",
|
|
91
91
|
schema: explodeSchema,
|
|
92
92
|
access: { roles: ["Admin"] },
|
|
93
|
-
perform:
|
|
93
|
+
perform: stepsPipeline<z.infer<typeof explodeSchema>, never>(({ r }) => [
|
|
94
94
|
r.step.return(() => {
|
|
95
95
|
throw new Error("boom");
|
|
96
96
|
}),
|
|
@@ -106,7 +106,7 @@ const compoundHandler = defineWriteHandler({
|
|
|
106
106
|
name: "compound",
|
|
107
107
|
schema: compoundSchema,
|
|
108
108
|
access: { roles: ["Admin"] },
|
|
109
|
-
perform:
|
|
109
|
+
perform: stepsPipeline<z.infer<typeof compoundSchema>, { sum: number; userId: string }>(
|
|
110
110
|
({ event, r }) => [
|
|
111
111
|
r.step.compute("offset", () => 100),
|
|
112
112
|
r.step.compute("doubledBase", () => event.payload.base * 2),
|
|
@@ -147,7 +147,7 @@ const logHandler = defineWriteHandler({
|
|
|
147
147
|
schema: logSchema,
|
|
148
148
|
access: { roles: ["Admin"] },
|
|
149
149
|
// Outer-`event`-capture pattern — see compoundHandler above for the why.
|
|
150
|
-
perform:
|
|
150
|
+
perform: stepsPipeline<z.infer<typeof logSchema>, { correlationId: string }>(({ event, r }) => [
|
|
151
151
|
r.step.unsafeProjectionUpsert({
|
|
152
152
|
table: pipelineDemoLogTable,
|
|
153
153
|
on: ["correlationId"],
|
|
@@ -182,7 +182,7 @@ const widgetCreateHandler = defineWriteHandler({
|
|
|
182
182
|
name: "widget:create",
|
|
183
183
|
schema: widgetSchema,
|
|
184
184
|
access: { roles: ["Admin"] },
|
|
185
|
-
perform:
|
|
185
|
+
perform: stepsPipeline<z.infer<typeof widgetSchema>, { id: string }>(({ event, r }) => [
|
|
186
186
|
r.step.aggregate.create("widget", {
|
|
187
187
|
executor: widgetExecutor,
|
|
188
188
|
data: () => ({ label: event.payload.label }),
|
|
@@ -202,7 +202,7 @@ const annotateHandler = defineWriteHandler({
|
|
|
202
202
|
name: "widget:annotate",
|
|
203
203
|
schema: annotateSchema,
|
|
204
204
|
access: { roles: ["Admin"] },
|
|
205
|
-
perform:
|
|
205
|
+
perform: stepsPipeline<z.infer<typeof annotateSchema>, { id: string }>(({ event, r }) => [
|
|
206
206
|
r.step.aggregate.create("widget", {
|
|
207
207
|
executor: widgetExecutor,
|
|
208
208
|
data: () => ({ label: event.payload.label }),
|
|
@@ -230,7 +230,7 @@ const widgetUpdateHandler = defineWriteHandler({
|
|
|
230
230
|
name: "widget:update",
|
|
231
231
|
schema: widgetUpdateSchema,
|
|
232
232
|
access: { roles: ["Admin"] },
|
|
233
|
-
perform:
|
|
233
|
+
perform: stepsPipeline<z.infer<typeof widgetUpdateSchema>, { id: string }>(({ event, r }) => [
|
|
234
234
|
r.step.aggregate.update("widget", {
|
|
235
235
|
executor: widgetExecutor,
|
|
236
236
|
id: () => event.payload.id,
|
|
@@ -254,7 +254,7 @@ const widgetBrokenHandler = defineWriteHandler({
|
|
|
254
254
|
name: "widget:create-broken",
|
|
255
255
|
schema: z.object({}),
|
|
256
256
|
access: { roles: ["Admin"] },
|
|
257
|
-
perform:
|
|
257
|
+
perform: stepsPipeline<Record<string, never>, { id: string }>(({ r }) => [
|
|
258
258
|
r.step.aggregate.create("widget", {
|
|
259
259
|
executor: widgetExecutor,
|
|
260
260
|
// Intentionally omits the `label` field that the entity declares
|
|
@@ -278,7 +278,7 @@ const widgetCreateThenThrowHandler = defineWriteHandler({
|
|
|
278
278
|
name: "widget:create-then-throw",
|
|
279
279
|
schema: z.object({ label: z.string() }),
|
|
280
280
|
access: { roles: ["Admin"] },
|
|
281
|
-
perform:
|
|
281
|
+
perform: stepsPipeline<{ label: string }, never>(({ event, r }) => [
|
|
282
282
|
r.step.aggregate.create("widget", {
|
|
283
283
|
executor: widgetExecutor,
|
|
284
284
|
data: () => ({ label: event.payload.label }),
|
|
@@ -303,7 +303,7 @@ const forEachThenThrowHandler = defineWriteHandler({
|
|
|
303
303
|
name: "widget:foreach-then-throw",
|
|
304
304
|
schema: forEachThenThrowSchema,
|
|
305
305
|
access: { roles: ["Admin"] },
|
|
306
|
-
perform:
|
|
306
|
+
perform: stepsPipeline<{ labels: string[] }, never>(({ event, r }) => [
|
|
307
307
|
r.step.forEach({
|
|
308
308
|
over: () => event.payload.labels,
|
|
309
309
|
as: "label",
|
|
@@ -337,7 +337,7 @@ const lookupHandler = defineWriteHandler({
|
|
|
337
337
|
name: "widget:lookup",
|
|
338
338
|
schema: lookupSchema,
|
|
339
339
|
access: { roles: ["Admin"] },
|
|
340
|
-
perform:
|
|
340
|
+
perform: stepsPipeline<z.infer<typeof lookupSchema>, { found: boolean; label: string | null }>(
|
|
341
341
|
({ event, r }) => [
|
|
342
342
|
r.step.read.findOne("widget", {
|
|
343
343
|
table: widgetTable,
|
|
@@ -358,7 +358,7 @@ const listAllHandler = defineWriteHandler({
|
|
|
358
358
|
name: "widget:list",
|
|
359
359
|
schema: z.object({}),
|
|
360
360
|
access: { roles: ["Admin"] },
|
|
361
|
-
perform:
|
|
361
|
+
perform: stepsPipeline<Record<string, never>, { count: number }>(({ r }) => [
|
|
362
362
|
r.step.read.findMany("widgets", { table: widgetTable }),
|
|
363
363
|
r.step.return(({ steps }) => ({
|
|
364
364
|
isSuccess: true as const,
|
|
@@ -374,7 +374,7 @@ const listLimitedHandler = defineWriteHandler({
|
|
|
374
374
|
name: "widget:list-one",
|
|
375
375
|
schema: z.object({}),
|
|
376
376
|
access: { roles: ["Admin"] },
|
|
377
|
-
perform:
|
|
377
|
+
perform: stepsPipeline<Record<string, never>, { count: number }>(({ r }) => [
|
|
378
378
|
r.step.read.findMany("widgets", { table: widgetTable, limit: 1 }),
|
|
379
379
|
r.step.return(({ steps }) => ({
|
|
380
380
|
isSuccess: true as const,
|
|
@@ -388,7 +388,7 @@ const purgeLogHandler = defineWriteHandler({
|
|
|
388
388
|
name: "log:purge",
|
|
389
389
|
schema: purgeLogSchema,
|
|
390
390
|
access: { roles: ["Admin"] },
|
|
391
|
-
perform:
|
|
391
|
+
perform: stepsPipeline<z.infer<typeof purgeLogSchema>, { ok: true }>(({ event, r }) => [
|
|
392
392
|
r.step.unsafeProjectionDelete({
|
|
393
393
|
table: pipelineDemoLogTable,
|
|
394
394
|
where: () => ({ correlationId: event.payload.correlationId }),
|
|
@@ -403,7 +403,7 @@ const conditionalLogHandler = defineWriteHandler({
|
|
|
403
403
|
name: "log:conditional",
|
|
404
404
|
schema: conditionalLogSchema,
|
|
405
405
|
access: { roles: ["Admin"] },
|
|
406
|
-
perform:
|
|
406
|
+
perform: stepsPipeline<z.infer<typeof conditionalLogSchema>, { ok: true }>(({ event, r }) => [
|
|
407
407
|
r.step.branch({
|
|
408
408
|
if: () => event.payload.message.length > 0,
|
|
409
409
|
onTrue: [
|
|
@@ -428,7 +428,7 @@ const bulkLogHandler = defineWriteHandler({
|
|
|
428
428
|
name: "log:bulk",
|
|
429
429
|
schema: bulkLogSchema,
|
|
430
430
|
access: { roles: ["Admin"] },
|
|
431
|
-
perform:
|
|
431
|
+
perform: stepsPipeline<z.infer<typeof bulkLogSchema>, { count: number }>(({ event, r }) => [
|
|
432
432
|
r.step.forEach({
|
|
433
433
|
over: () => event.payload.correlationIds,
|
|
434
434
|
as: "correlationId",
|
|
@@ -476,7 +476,7 @@ const demoPipelineFeature = defineFeature("demoPipeline", (r) => {
|
|
|
476
476
|
let stack: TestStack;
|
|
477
477
|
const admin = TestUsers.admin;
|
|
478
478
|
|
|
479
|
-
describe("defineWriteHandler({ perform:
|
|
479
|
+
describe("defineWriteHandler({ perform: stepsPipeline(...) }) — real dispatcher path", () => {
|
|
480
480
|
beforeAll(async () => {
|
|
481
481
|
stack = await setupTestStack({ features: [demoPipelineFeature] });
|
|
482
482
|
// Push the read-side-projection table — not registered as an entity,
|
|
@@ -20,7 +20,7 @@ import { createRecordingProvider, type RecordingProvider } from "../../testing";
|
|
|
20
20
|
import { defineFeature } from "../define-feature";
|
|
21
21
|
import { defineWriteHandler } from "../define-handler";
|
|
22
22
|
import { createEntity, createTextField } from "../factories";
|
|
23
|
-
import {
|
|
23
|
+
import { stepsPipeline } from "../pipeline";
|
|
24
24
|
|
|
25
25
|
// Handler whose step bodies emit a span + a metric. The recording
|
|
26
26
|
// provider afterwards lets us assert both landed.
|
|
@@ -30,7 +30,7 @@ const observedHandler = defineWriteHandler({
|
|
|
30
30
|
name: "observed",
|
|
31
31
|
schema: observedSchema,
|
|
32
32
|
access: { roles: ["Admin"] },
|
|
33
|
-
perform:
|
|
33
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
34
34
|
r.step.compute("traced", (ctx) => {
|
|
35
35
|
// Span emitted from inside a step. End it explicitly because
|
|
36
36
|
// step.run is sync-or-async-but-not-otel-instrumented; the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Pipeline-engine performance smoke-test.
|
|
2
2
|
//
|
|
3
|
-
// Compares the M.1 pipeline-form handler ({ perform:
|
|
3
|
+
// Compares the M.1 pipeline-form handler ({ perform: stepsPipeline(...) })
|
|
4
4
|
// against the equivalent free-form handler ({ handler: async (...) })
|
|
5
5
|
// over N identical writes against the real Postgres stack. The point
|
|
6
6
|
// is NOT to optimise — the pipeline-form is a thin wrapper, not a
|
|
@@ -28,7 +28,7 @@ import { setupTestStack, type TestStack, TestUsers, unsafeCreateEntityTable } fr
|
|
|
28
28
|
import { defineFeature } from "../define-feature";
|
|
29
29
|
import { defineWriteHandler } from "../define-handler";
|
|
30
30
|
import { createEntity, createNumberField, createTextField } from "../factories";
|
|
31
|
-
import {
|
|
31
|
+
import { stepsPipeline } from "../pipeline";
|
|
32
32
|
|
|
33
33
|
// Same logical operation in both handler-forms: read input, return a
|
|
34
34
|
// trivial transformed payload. No DB-write — the goal is to compare
|
|
@@ -41,7 +41,7 @@ const trivialPipeline = defineWriteHandler({
|
|
|
41
41
|
name: "trivial:pipeline",
|
|
42
42
|
schema: trivialSchema,
|
|
43
43
|
access: { roles: ["Admin"] },
|
|
44
|
-
perform:
|
|
44
|
+
perform: stepsPipeline<{ n: number }, { doubled: number }>(({ event, r }) => [
|
|
45
45
|
r.step.return(() => ({
|
|
46
46
|
isSuccess: true as const,
|
|
47
47
|
data: { doubled: event.payload.n * 2 },
|
|
@@ -9,7 +9,7 @@ import { describe, expect, it } from "bun:test";
|
|
|
9
9
|
import { z } from "zod";
|
|
10
10
|
import { TestUsers } from "../../stack";
|
|
11
11
|
import { defineWriteHandler } from "../define-handler";
|
|
12
|
-
import {
|
|
12
|
+
import { stepsPipeline } from "../pipeline";
|
|
13
13
|
import { buildBranchStep } from "../steps/branch";
|
|
14
14
|
import { buildForEachStep } from "../steps/for-each";
|
|
15
15
|
import { buildReturnStep } from "../steps/return";
|
|
@@ -21,7 +21,7 @@ describe("r.step.branch", () => {
|
|
|
21
21
|
name: "demo:branch-then",
|
|
22
22
|
schema: z.object({}),
|
|
23
23
|
access: { roles: ["User"] },
|
|
24
|
-
perform:
|
|
24
|
+
perform: stepsPipeline<Record<string, never>, { value: number }>(({ r }) => [
|
|
25
25
|
r.step.branch({
|
|
26
26
|
if: () => true,
|
|
27
27
|
onTrue: [r.step.compute("inThen", () => 42)],
|
|
@@ -47,7 +47,7 @@ describe("r.step.branch", () => {
|
|
|
47
47
|
name: "demo:branch-else",
|
|
48
48
|
schema: z.object({}),
|
|
49
49
|
access: { roles: ["User"] },
|
|
50
|
-
perform:
|
|
50
|
+
perform: stepsPipeline<Record<string, never>, { value: number }>(({ r }) => [
|
|
51
51
|
r.step.branch({
|
|
52
52
|
if: () => false,
|
|
53
53
|
onTrue: [r.step.compute("inThen", () => 42)],
|
|
@@ -72,7 +72,7 @@ describe("r.step.branch", () => {
|
|
|
72
72
|
name: "demo:branch-noop",
|
|
73
73
|
schema: z.object({}),
|
|
74
74
|
access: { roles: ["User"] },
|
|
75
|
-
perform:
|
|
75
|
+
perform: stepsPipeline<Record<string, never>, { ran: boolean }>(({ r }) => [
|
|
76
76
|
r.step.branch({
|
|
77
77
|
if: () => false,
|
|
78
78
|
onTrue: [r.step.compute("ran", () => true)],
|
|
@@ -122,7 +122,7 @@ describe("r.step.forEach", () => {
|
|
|
122
122
|
name: "demo:foreach",
|
|
123
123
|
schema: z.object({}),
|
|
124
124
|
access: { roles: ["User"] },
|
|
125
|
-
perform:
|
|
125
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
126
126
|
r.step.forEach({
|
|
127
127
|
over: () => [10, 20, 30],
|
|
128
128
|
as: "n",
|
|
@@ -153,7 +153,7 @@ describe("r.step.forEach", () => {
|
|
|
153
153
|
name: "demo:foreach-cleanup",
|
|
154
154
|
schema: z.object({}),
|
|
155
155
|
access: { roles: ["User"] },
|
|
156
|
-
perform:
|
|
156
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
157
157
|
r.step.forEach({
|
|
158
158
|
over: () => [1],
|
|
159
159
|
as: "tmp",
|
|
@@ -180,7 +180,7 @@ describe("r.step.forEach", () => {
|
|
|
180
180
|
name: "demo:foreach-empty",
|
|
181
181
|
schema: z.object({}),
|
|
182
182
|
access: { roles: ["User"] },
|
|
183
|
-
perform:
|
|
183
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
184
184
|
r.step.forEach({
|
|
185
185
|
over: () => [],
|
|
186
186
|
as: "n",
|
|
@@ -233,7 +233,7 @@ describe("r.step.forEach", () => {
|
|
|
233
233
|
name: "demo:foreach-throws",
|
|
234
234
|
schema: z.object({}),
|
|
235
235
|
access: { roles: ["User"] },
|
|
236
|
-
perform:
|
|
236
|
+
perform: stepsPipeline<Record<string, never>, never>(({ r }) => [
|
|
237
237
|
r.step.forEach({
|
|
238
238
|
over: () => [1, 2, 3],
|
|
239
239
|
as: "item",
|
|
@@ -266,7 +266,7 @@ describe("r.step.forEach", () => {
|
|
|
266
266
|
name: "demo:foreach-bad-over",
|
|
267
267
|
schema: z.object({}),
|
|
268
268
|
access: { roles: ["User"] },
|
|
269
|
-
perform:
|
|
269
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
270
270
|
r.step.forEach({
|
|
271
271
|
// Cast: TypeScript would normally catch this; the test exercises
|
|
272
272
|
// the runtime guard for hand-crafted / dynamically built cases.
|
|
@@ -11,7 +11,7 @@ import { defineFeature } from "../define-feature";
|
|
|
11
11
|
import { defineWriteHandler } from "../define-handler";
|
|
12
12
|
import { defineStep } from "../define-step";
|
|
13
13
|
import { createEntity, createTextField } from "../factories";
|
|
14
|
-
import {
|
|
14
|
+
import { stepsPipeline } from "../pipeline";
|
|
15
15
|
import { validateProjectionAllowlist } from "../validate-projection-allowlist";
|
|
16
16
|
|
|
17
17
|
describe("validateProjectionAllowlist", () => {
|
|
@@ -28,7 +28,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
28
28
|
name: "log",
|
|
29
29
|
schema: z.object({ msg: z.string() }),
|
|
30
30
|
access: { roles: ["User"] },
|
|
31
|
-
perform:
|
|
31
|
+
perform: stepsPipeline<{ msg: string }, { ok: true }>(({ event, r }) => [
|
|
32
32
|
r.step.unsafeProjectionUpsert({
|
|
33
33
|
table: demoLogTable,
|
|
34
34
|
on: ["id"],
|
|
@@ -70,7 +70,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
70
70
|
name: "sneaky",
|
|
71
71
|
schema: z.object({}),
|
|
72
72
|
access: { roles: ["User"] },
|
|
73
|
-
perform:
|
|
73
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
74
74
|
r.step.unsafeProjectionUpsert({
|
|
75
75
|
table: widgetsTable,
|
|
76
76
|
on: ["id"],
|
|
@@ -113,7 +113,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
113
113
|
name: "sneaky-delete",
|
|
114
114
|
schema: z.object({}),
|
|
115
115
|
access: { roles: ["User"] },
|
|
116
|
-
perform:
|
|
116
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
117
117
|
r.step.unsafeProjectionDelete({
|
|
118
118
|
table: widgetsTable,
|
|
119
119
|
where: () => ({ id: "anything" }),
|
|
@@ -136,7 +136,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
136
136
|
name: "purge",
|
|
137
137
|
schema: z.object({}),
|
|
138
138
|
access: { roles: ["User"] },
|
|
139
|
-
perform:
|
|
139
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
140
140
|
r.step.unsafeProjectionDelete({
|
|
141
141
|
table: demoLogTable,
|
|
142
142
|
where: () => ({ id: "anything" }),
|
|
@@ -162,7 +162,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
162
162
|
name: "branchedWrite",
|
|
163
163
|
schema: z.object({}),
|
|
164
164
|
access: { roles: ["User"] },
|
|
165
|
-
perform:
|
|
165
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
166
166
|
r.step.branch({
|
|
167
167
|
if: () => true,
|
|
168
168
|
onTrue: [
|
|
@@ -195,7 +195,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
195
195
|
name: "nestedWrite",
|
|
196
196
|
schema: z.object({}),
|
|
197
197
|
access: { roles: ["User"] },
|
|
198
|
-
perform:
|
|
198
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
199
199
|
r.step.forEach({
|
|
200
200
|
over: () => [],
|
|
201
201
|
as: "item",
|
|
@@ -230,7 +230,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
230
230
|
name: "loopedWrite",
|
|
231
231
|
schema: z.object({}),
|
|
232
232
|
access: { roles: ["User"] },
|
|
233
|
-
perform:
|
|
233
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
234
234
|
r.step.forEach({
|
|
235
235
|
over: () => [],
|
|
236
236
|
as: "x",
|
|
@@ -267,7 +267,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
267
267
|
name: "futureBuilder",
|
|
268
268
|
schema: z.object({}),
|
|
269
269
|
access: { roles: ["User"] },
|
|
270
|
-
perform:
|
|
270
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
271
271
|
// Hand-crafted StepInstance simulating a future builder
|
|
272
272
|
// whose kind isn't registered via defineStep yet.
|
|
273
273
|
{
|
|
@@ -316,7 +316,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
316
316
|
name: "futureBuilder",
|
|
317
317
|
schema: z.object({}),
|
|
318
318
|
access: { roles: ["User"] },
|
|
319
|
-
perform:
|
|
319
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
320
320
|
{
|
|
321
321
|
kind: futureKind,
|
|
322
322
|
args: {
|
|
@@ -380,7 +380,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
380
380
|
name: "sneak",
|
|
381
381
|
schema: z.object({}),
|
|
382
382
|
access: { roles: ["Admin"] },
|
|
383
|
-
perform:
|
|
383
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
384
384
|
r.step.webhook.send({
|
|
385
385
|
url: "https://hooks.example/sneak",
|
|
386
386
|
mode: "deferred",
|
|
@@ -404,7 +404,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
404
404
|
name: "sneak",
|
|
405
405
|
schema: z.object({}),
|
|
406
406
|
access: { roles: ["Admin"] },
|
|
407
|
-
perform:
|
|
407
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
408
408
|
r.step.mail.send({
|
|
409
409
|
to: "x@y.com",
|
|
410
410
|
subject: "hi",
|
|
@@ -428,7 +428,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
428
428
|
name: "sneak",
|
|
429
429
|
schema: z.object({}),
|
|
430
430
|
access: { roles: ["Admin"] },
|
|
431
|
-
perform:
|
|
431
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
432
432
|
r.step.callFeature("subResult", {
|
|
433
433
|
handler: "other:write:do",
|
|
434
434
|
payload: () => ({}),
|
|
@@ -451,7 +451,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
451
451
|
name: "ok",
|
|
452
452
|
schema: z.object({}),
|
|
453
453
|
access: { roles: ["Admin"] },
|
|
454
|
-
perform:
|
|
454
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
455
455
|
r.step.webhook.send({
|
|
456
456
|
url: "https://hooks.example/ok",
|
|
457
457
|
mode: "deferred",
|
|
@@ -473,7 +473,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
473
473
|
name: "log",
|
|
474
474
|
schema: z.object({ msg: z.string() }),
|
|
475
475
|
access: { roles: ["User"] },
|
|
476
|
-
perform:
|
|
476
|
+
perform: stepsPipeline<{ msg: string }, { ok: true }>(({ event, r }) => [
|
|
477
477
|
r.step.unsafeProjectionUpsert({
|
|
478
478
|
table: demoLogTable,
|
|
479
479
|
on: ["id"],
|