@cosmicdrift/kumiko-framework 0.146.1 → 0.146.2
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-framework",
|
|
3
|
-
"version": "0.146.
|
|
3
|
+
"version": "0.146.2",
|
|
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.2",
|
|
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"],
|
|
@@ -370,7 +370,7 @@ export function generatePerformBlock(
|
|
|
370
370
|
const pipelineType = schemaType ? `<${schemaType}, unknown>` : "";
|
|
371
371
|
|
|
372
372
|
return [
|
|
373
|
-
`perform:
|
|
373
|
+
`perform: stepsPipeline${pipelineType}(({ event, r }) => [`,
|
|
374
374
|
stepsStr,
|
|
375
375
|
` ${stepIndent}]),`,
|
|
376
376
|
].join("\n");
|
|
@@ -513,7 +513,7 @@ function contentHasPipelineImport(content: string): boolean {
|
|
|
513
513
|
const importLine = content
|
|
514
514
|
.split("\n")
|
|
515
515
|
.find((l) => l.includes("import") && l.includes("@cosmicdrift/kumiko-framework/engine"));
|
|
516
|
-
return !!importLine && importLine.includes("
|
|
516
|
+
return !!importLine && importLine.includes("stepsPipeline");
|
|
517
517
|
}
|
|
518
518
|
|
|
519
519
|
function ensurePipelineImport(content: string): string | null {
|
|
@@ -525,7 +525,7 @@ function ensurePipelineImport(content: string): string | null {
|
|
|
525
525
|
const match = content.match(importRegex);
|
|
526
526
|
if (match) {
|
|
527
527
|
const existingImports = (match[1] as string).trim();
|
|
528
|
-
const newImports = existingImports ? `${existingImports},
|
|
528
|
+
const newImports = existingImports ? `${existingImports}, stepsPipeline` : "stepsPipeline";
|
|
529
529
|
return content.replace(
|
|
530
530
|
importRegex,
|
|
531
531
|
`import { ${newImports} } from "@cosmicdrift/kumiko-framework/engine"`,
|
|
@@ -611,7 +611,7 @@ export async function convertFile(
|
|
|
611
611
|
if (importResult) {
|
|
612
612
|
content = importResult;
|
|
613
613
|
} else if (options.verbose) {
|
|
614
|
-
console.log(` ~ ${filePath}:
|
|
614
|
+
console.log(` ~ ${filePath}: stepsPipeline import not needed or already present`);
|
|
615
615
|
}
|
|
616
616
|
}
|
|
617
617
|
|
|
@@ -621,7 +621,7 @@ export async function convertFile(
|
|
|
621
621
|
|
|
622
622
|
const status = hadChanges ? "converted" : "skipped";
|
|
623
623
|
const reason = hadChanges
|
|
624
|
-
? "handler replaced with perform:
|
|
624
|
+
? "handler replaced with perform: stepsPipeline(...)"
|
|
625
625
|
: "no convertible handler";
|
|
626
626
|
return { filePath, status, reason };
|
|
627
627
|
} catch (err) {
|
|
@@ -23,7 +23,7 @@ import type { PipelineDef } from "./types/step";
|
|
|
23
23
|
// visible) and collapse `keyof TMap` to `never`. See the spike-findings
|
|
24
24
|
// memory for the empirical proof.
|
|
25
25
|
//
|
|
26
|
-
// Two authoring forms — `handler` (free-form) or `perform:
|
|
26
|
+
// Two authoring forms — `handler` (free-form) or `perform: stepsPipeline(...)`
|
|
27
27
|
// (step-pipeline). A `perform` is compiled to a handler-function at
|
|
28
28
|
// definition time; the dispatcher only ever sees `handler`.
|
|
29
29
|
|
|
@@ -126,7 +126,7 @@ export function defineWriteHandler<
|
|
|
126
126
|
throw new Error(
|
|
127
127
|
`defineWriteHandler("${def.name}"): both \`handler\` and \`perform\` are set. ` +
|
|
128
128
|
`Pick one — \`handler\` for the free-form async function, ` +
|
|
129
|
-
`\`perform:
|
|
129
|
+
`\`perform: stepsPipeline(...)\` for the step-pipeline form. ` +
|
|
130
130
|
`(See step-vocabulary.md for which form fits.)`,
|
|
131
131
|
);
|
|
132
132
|
}
|
|
@@ -57,7 +57,7 @@ export type WorkflowInput<TPayload = unknown, TData = unknown> = {
|
|
|
57
57
|
* defineWorkflow({
|
|
58
58
|
* name: "user-onboarding",
|
|
59
59
|
* trigger: { kind: "event", eventType: "user.signed-up" },
|
|
60
|
-
* steps:
|
|
60
|
+
* steps: stepsPipeline(({ event, r }) => [
|
|
61
61
|
* r.step.mail.send({ to: () => event.payload.email, subject: "Welcome!", body: "..." }),
|
|
62
62
|
* r.step.wait({ for: "P1D" }),
|
|
63
63
|
* r.step.read.findOne("user", { table: userTable, where: ... }),
|
package/src/engine/index.ts
CHANGED
|
@@ -179,18 +179,20 @@ export {
|
|
|
179
179
|
filterReadFields,
|
|
180
180
|
} from "./field-access";
|
|
181
181
|
// findForbiddenMembershipRole/isForbiddenMembershipRole/
|
|
182
|
-
// stripForbiddenMembershipRoles are Public API for host
|
|
183
|
-
// their own membership handlers. FORBIDDEN_MEMBERSHIP_ROLES
|
|
184
|
-
// internal (637/3) — exporting the raw Set would make its
|
|
185
|
-
// semver promise; the predicate functions are the intended
|
|
182
|
+
// stripForbiddenMembershipRoles/buildSessionRoles are Public API for host
|
|
183
|
+
// apps that build their own membership handlers. FORBIDDEN_MEMBERSHIP_ROLES
|
|
184
|
+
// itself stays internal (637/3) — exporting the raw Set would make its
|
|
185
|
+
// representation a semver promise; the predicate functions are the intended
|
|
186
|
+
// surface.
|
|
186
187
|
export {
|
|
188
|
+
buildSessionRoles,
|
|
187
189
|
findForbiddenMembershipRole,
|
|
188
190
|
isForbiddenMembershipRole,
|
|
189
191
|
stripForbiddenMembershipRoles,
|
|
190
192
|
} from "./membership-roles";
|
|
191
193
|
export type { OwnershipClause, OwnershipMap, OwnershipRef, OwnershipRule } from "./ownership";
|
|
192
194
|
export { from } from "./ownership";
|
|
193
|
-
export { buildPipelineSteps,
|
|
195
|
+
export { buildPipelineSteps, stepsPipeline } from "./pipeline";
|
|
194
196
|
export { defineApply, defineMspApply, setFields } from "./projection-helpers";
|
|
195
197
|
export type { BuiltinQnType, ParsedQn, QnType } from "./qualified-name";
|
|
196
198
|
export { isValidQn, parseQn, QnTypes, qn, toKebab } from "./qualified-name";
|
|
@@ -30,3 +30,16 @@ export function findForbiddenMembershipRole(roles: readonly string[]): string |
|
|
|
30
30
|
export function stripForbiddenMembershipRoles(roles: readonly string[]): readonly string[] {
|
|
31
31
|
return roles.filter((role) => !isForbiddenMembershipRole(role));
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
// Single mint path for session roles: merges globalRoles with the stripped
|
|
35
|
+
// membership portion and dedupes. Every place that builds a session's roles
|
|
36
|
+
// from a membership should go through this instead of repeating
|
|
37
|
+
// `new Set([...globalRoles, ...stripForbiddenMembershipRoles(x)])` — a new
|
|
38
|
+
// mint site that forgets the strip is exactly the bug class this guards
|
|
39
|
+
// against.
|
|
40
|
+
export function buildSessionRoles(
|
|
41
|
+
globalRoles: readonly string[],
|
|
42
|
+
membershipRoles: readonly string[],
|
|
43
|
+
): readonly string[] {
|
|
44
|
+
return Array.from(new Set([...globalRoles, ...stripForbiddenMembershipRoles(membershipRoles)]));
|
|
45
|
+
}
|
package/src/engine/pipeline.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
1
|
+
// stepsPipeline() — public factory used in defineWriteHandler({ perform: stepsPipeline(...) }).
|
|
2
2
|
//
|
|
3
3
|
// The closure receives { event, r } and returns the immutable list of
|
|
4
4
|
// step instances. `r` is the StepBuilder singleton; new tier-1 steps
|
|
@@ -8,15 +8,10 @@
|
|
|
8
8
|
// the resolver-side PipelineCtx (run-pipeline.ts). Resolvers that need
|
|
9
9
|
// prior step results destructure them from the resolver's ctx.
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// grep for `pipeline` returns mixed results. If a rename ever lands,
|
|
16
|
-
// candidates are `r.steps([...])` for the public API or
|
|
17
|
-
// `engine-pipeline/` / `runtime/` for the internal directory.
|
|
18
|
-
// Decision-cost grows with each new caller; the rename window narrows
|
|
19
|
-
// after the first external consumer.
|
|
11
|
+
// Renamed from `pipeline()` (Followup #1) — it shared its name with the
|
|
12
|
+
// internal `packages/framework/src/pipeline/` directory (dispatcher,
|
|
13
|
+
// lifecycle, outbox-poller), which made repo-wide grep for `pipeline`
|
|
14
|
+
// return mixed results.
|
|
20
15
|
|
|
21
16
|
import { buildAggregateAppendEventStep } from "./steps/aggregate-append-event";
|
|
22
17
|
import { buildAggregateCreateStep } from "./steps/aggregate-create";
|
|
@@ -69,7 +64,7 @@ const stepBuilder: StepBuilder = {
|
|
|
69
64
|
},
|
|
70
65
|
};
|
|
71
66
|
|
|
72
|
-
export function
|
|
67
|
+
export function stepsPipeline<TPayload = unknown, TData = unknown>(
|
|
73
68
|
closure: (ctx: PipelineBuildCtx<TPayload>) => readonly StepInstance[],
|
|
74
69
|
): PipelineDef<TPayload, TData> {
|
|
75
70
|
return {
|
|
@@ -64,7 +64,7 @@ export async function runPipeline<TPayload, TData, TMap extends object = KumikoE
|
|
|
64
64
|
if (outcome.kind === "return") {
|
|
65
65
|
// RETURN_RESULT_KEY is only produced by r.step.return, whose run()
|
|
66
66
|
// returns WriteResult<unknown>. The pipeline's generic TData is
|
|
67
|
-
// bound at build time (defineWriteHandler ↔
|
|
67
|
+
// bound at build time (defineWriteHandler ↔ stepsPipeline<P, D>(...));
|
|
68
68
|
// matching the runtime value to that compile-time type is the
|
|
69
69
|
// contract user-side. Cast crosses that boundary.
|
|
70
70
|
return outcome.result as WriteResult<TData>;
|
|
@@ -796,7 +796,7 @@ export type WriteHandlerDef = {
|
|
|
796
796
|
readonly access?: AccessRule;
|
|
797
797
|
readonly unsafeSkipTransitionGuard?: boolean;
|
|
798
798
|
readonly rateLimit?: RateLimitOption;
|
|
799
|
-
// Set when the author wrote a `perform:
|
|
799
|
+
// Set when the author wrote a `perform: stepsPipeline(...)` block. Boot-
|
|
800
800
|
// validators (projection-allowlist) and Designer/AI tooling read this
|
|
801
801
|
// to inspect the step list. Absent on free-form handlers.
|
|
802
802
|
// Inline-import is intentional: step.ts imports HandlerContext from
|
package/src/engine/types/step.ts
CHANGED
|
@@ -126,7 +126,7 @@ export type StepInstance = {
|
|
|
126
126
|
};
|
|
127
127
|
|
|
128
128
|
/**
|
|
129
|
-
* What `
|
|
129
|
+
* What `stepsPipeline(closure)` returns. Carries the closure (instead of an
|
|
130
130
|
* eagerly-built array) so each handler-call sees a fresh event ref and
|
|
131
131
|
* the `r` step-builder is resolved at runtime, not at module-load time.
|
|
132
132
|
*
|
|
@@ -140,7 +140,7 @@ export type StepInstance = {
|
|
|
140
140
|
* `noUnusedVariables` marker, not user-facing). _TData is NOT inferred
|
|
141
141
|
* from the closure body (r.step.return has its own per-call TData), so
|
|
142
142
|
* callers must spell it explicitly:
|
|
143
|
-
* `
|
|
143
|
+
* `stepsPipeline<{ greeting: string }, { echoed: string }>(...)`
|
|
144
144
|
* Better DX is a known follow-up — see step-vocabulary.md M.1-Followups.
|
|
145
145
|
*/
|
|
146
146
|
export type PipelineDef<TPayload = unknown, _TData = unknown> = {
|
|
@@ -149,7 +149,7 @@ export type PipelineDef<TPayload = unknown, _TData = unknown> = {
|
|
|
149
149
|
};
|
|
150
150
|
|
|
151
151
|
/**
|
|
152
|
-
* Argument bundle passed to the closure inside `
|
|
152
|
+
* Argument bundle passed to the closure inside `stepsPipeline(closure)`.
|
|
153
153
|
* Build-time only — no `steps`, no `scope`, no `db`: at build time no
|
|
154
154
|
* step has run yet.
|
|
155
155
|
*
|
|
@@ -314,7 +314,7 @@ export type StepNamespace = {
|
|
|
314
314
|
},
|
|
315
315
|
) => StepInstance;
|
|
316
316
|
// --- Tier-3 / Workflow-only steps ---
|
|
317
|
-
// Only available inside defineWorkflow ({ steps:
|
|
317
|
+
// Only available inside defineWorkflow ({ steps: stepsPipeline(...) }).
|
|
318
318
|
// Runtime guard: throws when used inside sync defineWriteHandler.
|
|
319
319
|
readonly wait: (args: { readonly for: StepResolver<string> }) => StepInstance;
|
|
320
320
|
readonly waitForEvent: (args: {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// don't run at build time, so dummy event-payloads are fine.
|
|
14
14
|
//
|
|
15
15
|
// CLOSURE-BODY CONTRACT: the pipeline-closure body (the function passed
|
|
16
|
-
// to `
|
|
16
|
+
// to `stepsPipeline(...)`) must produce its step-list deterministically based
|
|
17
17
|
// on the step-builder `r` alone. Reading `event.payload` outside of
|
|
18
18
|
// resolvers (i.e. at the top of the closure, not inside a step's `row:`
|
|
19
19
|
// or `data:` callback) is forbidden — at boot the dummy payload is `{}`
|