@cosmicdrift/kumiko-framework 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -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 +45 -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 +92 -1
- package/src/engine/types/handlers.ts +18 -10
- package/src/engine/types/identifiers.ts +1 -0
- package/src/engine/types/index.ts +12 -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 +130 -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
|
@@ -597,7 +597,7 @@ const writeHandlerSchema: PatternFormSchema = {
|
|
|
597
597
|
input: "json-readonly",
|
|
598
598
|
},
|
|
599
599
|
{
|
|
600
|
-
path: "
|
|
600
|
+
path: "unsafeSkipTransitionGuard",
|
|
601
601
|
label: { en: "Skip transition guard", de: "Übergangs-Guard überspringen" },
|
|
602
602
|
input: "boolean",
|
|
603
603
|
},
|
|
@@ -1054,6 +1054,44 @@ const exposesApiSchema: PatternFormSchema = {
|
|
|
1054
1054
|
],
|
|
1055
1055
|
};
|
|
1056
1056
|
|
|
1057
|
+
// Visual-Tree pattern schemas. treeActions is a static map (Designer
|
|
1058
|
+
// renders the action-name → ActionDef pairs as a nested form), tree is
|
|
1059
|
+
// closure-only (Designer shows the provider body as read-only code).
|
|
1060
|
+
const treeActionsSchema: PatternFormSchema = {
|
|
1061
|
+
kind: "treeActions",
|
|
1062
|
+
label: { en: "Tree actions", de: "Tree-Actions" },
|
|
1063
|
+
summary: { en: "Action verbs the Visual-Tree dispatches via buildTarget." },
|
|
1064
|
+
category: "ui",
|
|
1065
|
+
editability: "static",
|
|
1066
|
+
singleton: true,
|
|
1067
|
+
fields: [
|
|
1068
|
+
{
|
|
1069
|
+
path: "definitions",
|
|
1070
|
+
label: { en: "Action definitions", de: "Action-Definitionen" },
|
|
1071
|
+
input: "json-readonly",
|
|
1072
|
+
readOnly: true,
|
|
1073
|
+
},
|
|
1074
|
+
],
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
const treeSchema: PatternFormSchema = {
|
|
1078
|
+
kind: "tree",
|
|
1079
|
+
label: { en: "Tree provider", de: "Tree-Provider" },
|
|
1080
|
+
summary: { en: "Subscribe-Function emitting top-level Visual-Tree nodes." },
|
|
1081
|
+
category: "ui",
|
|
1082
|
+
editability: "opaque",
|
|
1083
|
+
singleton: true,
|
|
1084
|
+
fields: [
|
|
1085
|
+
{
|
|
1086
|
+
path: "providerBody",
|
|
1087
|
+
label: { en: "Provider body (source)", de: "Provider-Body (Source)" },
|
|
1088
|
+
input: "code-block",
|
|
1089
|
+
language: "typescript",
|
|
1090
|
+
readOnly: true,
|
|
1091
|
+
},
|
|
1092
|
+
],
|
|
1093
|
+
};
|
|
1094
|
+
|
|
1057
1095
|
const unknownSchema: PatternFormSchema = {
|
|
1058
1096
|
kind: "unknown",
|
|
1059
1097
|
label: { en: "Unknown call", de: "Unbekannter Call" },
|
|
@@ -1113,8 +1151,10 @@ export const PATTERN_LIBRARY: Readonly<Record<FeaturePatternKind, PatternFormSch
|
|
|
1113
1151
|
extendsRegistrar: extendsRegistrarSchema,
|
|
1114
1152
|
usesApi: usesApiSchema,
|
|
1115
1153
|
exposesApi: exposesApiSchema,
|
|
1154
|
+
treeActions: treeActionsSchema,
|
|
1155
|
+
tree: treeSchema,
|
|
1116
1156
|
unknown: unknownSchema,
|
|
1117
|
-
}
|
|
1157
|
+
} satisfies Readonly<Record<FeaturePatternKind, PatternFormSchema>>;
|
|
1118
1158
|
|
|
1119
1159
|
/**
|
|
1120
1160
|
* Lookup helper — convenience over `PATTERN_LIBRARY[kind]`. Throws when
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
// pipeline() — public factory used in defineWriteHandler({ perform: pipeline(...) }).
|
|
2
|
+
//
|
|
3
|
+
// The closure receives { event, r } and returns the immutable list of
|
|
4
|
+
// step instances. `r` is the StepBuilder singleton; new tier-1 steps
|
|
5
|
+
// add a builder factory in steps/<x>.ts and expose it under `step` below.
|
|
6
|
+
//
|
|
7
|
+
// `steps` and `scope` are NOT exposed at build time — they only exist on
|
|
8
|
+
// the resolver-side PipelineCtx (run-pipeline.ts). Resolvers that need
|
|
9
|
+
// prior step results destructure them from the resolver's ctx.
|
|
10
|
+
//
|
|
11
|
+
// Naming note (Followup #1): the public `pipeline()` helper shares its
|
|
12
|
+
// name with the internal `packages/framework/src/pipeline/` directory
|
|
13
|
+
// (dispatcher, lifecycle, outbox-poller). No user-visible collision —
|
|
14
|
+
// the internal directory isn't an export — but maintainer repo-wide
|
|
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.
|
|
20
|
+
|
|
21
|
+
import { buildAggregateAppendEventStep } from "./steps/aggregate-append-event";
|
|
22
|
+
import { buildAggregateCreateStep } from "./steps/aggregate-create";
|
|
23
|
+
import { buildAggregateUpdateStep } from "./steps/aggregate-update";
|
|
24
|
+
import { buildBranchStep } from "./steps/branch";
|
|
25
|
+
import { buildCallFeatureStep } from "./steps/call-feature";
|
|
26
|
+
import { buildComputeStep } from "./steps/compute";
|
|
27
|
+
import { buildForEachStep } from "./steps/for-each";
|
|
28
|
+
import { buildMailSendStep } from "./steps/mail-send";
|
|
29
|
+
import { buildReadFindManyStep } from "./steps/read-find-many";
|
|
30
|
+
import { buildReadFindOneStep } from "./steps/read-find-one";
|
|
31
|
+
import { buildRetryStep } from "./steps/retry";
|
|
32
|
+
import { buildReturnStep } from "./steps/return";
|
|
33
|
+
import { buildUnsafeProjectionDeleteStep } from "./steps/unsafe-projection-delete";
|
|
34
|
+
import { buildUnsafeProjectionUpsertStep } from "./steps/unsafe-projection-upsert";
|
|
35
|
+
import { buildWaitStep } from "./steps/wait";
|
|
36
|
+
import { buildWaitForEventStep } from "./steps/wait-for-event";
|
|
37
|
+
import { buildWebhookSendStep } from "./steps/webhook-send";
|
|
38
|
+
import type { WriteEvent } from "./types/handlers";
|
|
39
|
+
import type { PipelineBuildCtx, PipelineDef, StepBuilder, StepInstance } from "./types/step";
|
|
40
|
+
|
|
41
|
+
const stepBuilder: StepBuilder = {
|
|
42
|
+
step: {
|
|
43
|
+
return: buildReturnStep,
|
|
44
|
+
compute: buildComputeStep,
|
|
45
|
+
branch: buildBranchStep,
|
|
46
|
+
forEach: buildForEachStep,
|
|
47
|
+
unsafeProjectionUpsert: buildUnsafeProjectionUpsertStep,
|
|
48
|
+
unsafeProjectionDelete: buildUnsafeProjectionDeleteStep,
|
|
49
|
+
aggregate: {
|
|
50
|
+
create: buildAggregateCreateStep,
|
|
51
|
+
update: buildAggregateUpdateStep,
|
|
52
|
+
appendEvent: buildAggregateAppendEventStep,
|
|
53
|
+
},
|
|
54
|
+
read: {
|
|
55
|
+
findOne: buildReadFindOneStep,
|
|
56
|
+
findMany: buildReadFindManyStep,
|
|
57
|
+
},
|
|
58
|
+
webhook: {
|
|
59
|
+
send: buildWebhookSendStep,
|
|
60
|
+
},
|
|
61
|
+
mail: {
|
|
62
|
+
send: buildMailSendStep,
|
|
63
|
+
},
|
|
64
|
+
callFeature: buildCallFeatureStep,
|
|
65
|
+
// Tier-3 / Workflow-only steps
|
|
66
|
+
wait: buildWaitStep,
|
|
67
|
+
waitForEvent: buildWaitForEventStep,
|
|
68
|
+
retry: buildRetryStep,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export function pipeline<TPayload = unknown, TData = unknown>(
|
|
73
|
+
closure: (ctx: PipelineBuildCtx<TPayload>) => readonly StepInstance[],
|
|
74
|
+
): PipelineDef<TPayload, TData> {
|
|
75
|
+
return {
|
|
76
|
+
__kind: "pipeline",
|
|
77
|
+
build: closure,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Internal: invoked by run-pipeline.ts to materialise the step list.
|
|
82
|
+
// Not exported from the engine barrel — pipeline-internal plumbing.
|
|
83
|
+
export function buildPipelineSteps<TPayload>(
|
|
84
|
+
pipelineDef: PipelineDef<TPayload>,
|
|
85
|
+
event: WriteEvent<TPayload>,
|
|
86
|
+
): readonly StepInstance[] {
|
|
87
|
+
return pipelineDef.build({ event, r: stepBuilder });
|
|
88
|
+
}
|
|
@@ -76,7 +76,7 @@ export function setFields(
|
|
|
76
76
|
// strict about the concrete row, so we feed it the erased value; the
|
|
77
77
|
// type-safety guarantee for `values` lives at the setFields call-site.
|
|
78
78
|
// biome-ignore lint/suspicious/noExplicitAny: see note above.
|
|
79
|
-
const set = values as any;
|
|
79
|
+
const set = values as any; // @cast-boundary engine-bridge
|
|
80
80
|
await tx
|
|
81
81
|
.update(table)
|
|
82
82
|
.set(set)
|
package/src/engine/read-claim.ts
CHANGED
|
@@ -27,5 +27,5 @@ export function readClaim<T extends ClaimKeyType>(
|
|
|
27
27
|
if (!claims) return undefined;
|
|
28
28
|
const raw = claims[handle.name];
|
|
29
29
|
if (raw === undefined || raw === null) return undefined;
|
|
30
|
-
return raw as ClaimKeyJsType<T>;
|
|
30
|
+
return raw as ClaimKeyJsType<T>; // @cast-boundary schema-walk
|
|
31
31
|
}
|
package/src/engine/registry.ts
CHANGED
|
@@ -35,6 +35,8 @@ import type {
|
|
|
35
35
|
ScreenDefinition,
|
|
36
36
|
SecretKeyDefinition,
|
|
37
37
|
TranslationKeys,
|
|
38
|
+
TreeActionDef,
|
|
39
|
+
TreeChildrenSubscribe,
|
|
38
40
|
WorkspaceDefinition,
|
|
39
41
|
WriteHandlerDef,
|
|
40
42
|
} from "./types";
|
|
@@ -198,6 +200,14 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
198
200
|
const navsByWorkspace = new Map<string, string[]>();
|
|
199
201
|
let defaultWorkspace: WorkspaceDefinition | undefined;
|
|
200
202
|
|
|
203
|
+
// Visual-Tree-Provider — keyed by declaring feature name (NOT qualified;
|
|
204
|
+
// ein Feature liefert genau einen Provider). Visual-Tree-Component
|
|
205
|
+
// iteriert die Map zur Mount-Zeit. Tree-Actions parallel — featureName
|
|
206
|
+
// → erased Action-Map (compile-time-typed Variante geht über
|
|
207
|
+
// setup-export-handle, siehe FeatureRegistrar.treeActions docs).
|
|
208
|
+
const treeProvidersMap = new Map<string, TreeChildrenSubscribe>();
|
|
209
|
+
const treeActionsMap = new Map<string, Readonly<Record<string, TreeActionDef>>>();
|
|
210
|
+
|
|
201
211
|
// Local alias for readability — `qualifyEntityName` is the shared helper
|
|
202
212
|
// from qualified-name.ts, also used by validateBoot to keep ingest and
|
|
203
213
|
// validation in lockstep on the qualification rule.
|
|
@@ -596,6 +606,16 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
596
606
|
}
|
|
597
607
|
}
|
|
598
608
|
|
|
609
|
+
// Visual-Tree slots — at-most-one per feature (only-once-guard im
|
|
610
|
+
// registrar). Erased Maps für Runtime-Lookup; compile-time-typed
|
|
611
|
+
// Surface läuft über FeatureDefinition.exports (TreeActionsHandle).
|
|
612
|
+
if (feature.treeProvider !== undefined) {
|
|
613
|
+
treeProvidersMap.set(feature.name, feature.treeProvider);
|
|
614
|
+
}
|
|
615
|
+
if (feature.treeActions !== undefined) {
|
|
616
|
+
treeActionsMap.set(feature.name, feature.treeActions);
|
|
617
|
+
}
|
|
618
|
+
|
|
599
619
|
// Auth-claims hooks: order of registration is preserved. Feature name is
|
|
600
620
|
// captured alongside so the resolver can apply the auto-prefix at merge
|
|
601
621
|
// time — the feature author never ships pre-prefixed keys.
|
|
@@ -984,7 +1004,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
984
1004
|
const allHandlerNames = new Set([...writeHandlerMap.keys(), ...queryHandlerMap.keys()]);
|
|
985
1005
|
for (const [qualifiedName, notifDef] of notificationMap) {
|
|
986
1006
|
// Both maps are populated in lockstep — same key-set by construction.
|
|
987
|
-
const featureName = notificationFeatureMap.get(qualifiedName) as string;
|
|
1007
|
+
const featureName = notificationFeatureMap.get(qualifiedName) as string; // @cast-boundary engine-bridge
|
|
988
1008
|
// I'll try the easy path first: if the trigger is already a fully qualified QN
|
|
989
1009
|
// (cross-feature), I take it as-is. Otherwise I qualify with the own feature —
|
|
990
1010
|
// as a write handler first (the common case), then as a query. If nothing
|
|
@@ -1124,7 +1144,7 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
1124
1144
|
},
|
|
1125
1145
|
|
|
1126
1146
|
getRelations(entityName: string): EntityRelations {
|
|
1127
|
-
return (relationMap.get(entityName) ?? {}) as EntityRelations;
|
|
1147
|
+
return (relationMap.get(entityName) ?? {}) as EntityRelations; // @cast-boundary schema-walk
|
|
1128
1148
|
},
|
|
1129
1149
|
|
|
1130
1150
|
getSearchIncludes(entityName: string): ReadonlyMap<string, readonly string[]> {
|
|
@@ -1355,6 +1375,14 @@ export function createRegistry(features: readonly FeatureDefinition[]): Registry
|
|
|
1355
1375
|
getDefaultWorkspace(): WorkspaceDefinition | undefined {
|
|
1356
1376
|
return defaultWorkspace;
|
|
1357
1377
|
},
|
|
1378
|
+
|
|
1379
|
+
getTreeProviders(): ReadonlyMap<string, TreeChildrenSubscribe> {
|
|
1380
|
+
return treeProvidersMap;
|
|
1381
|
+
},
|
|
1382
|
+
|
|
1383
|
+
getTreeActions(featureName: string): Readonly<Record<string, TreeActionDef>> | undefined {
|
|
1384
|
+
return treeActionsMap.get(featureName);
|
|
1385
|
+
},
|
|
1358
1386
|
};
|
|
1359
1387
|
}
|
|
1360
1388
|
|
|
@@ -141,6 +141,10 @@ export async function resolveConfigOrParam<T extends ConfigKeyType>(
|
|
|
141
141
|
// The caller is signalling intent; we honour the constraint instead.
|
|
142
142
|
return ctx.config(handle);
|
|
143
143
|
}
|
|
144
|
+
default: {
|
|
145
|
+
const _exhaustive: never = keyDef.type;
|
|
146
|
+
throw new Error(`resolveConfigOrParam: unhandled config key type "${_exhaustive}"`);
|
|
147
|
+
}
|
|
144
148
|
}
|
|
145
149
|
}
|
|
146
150
|
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// run-pipeline — executes a PipelineDef against (event, ctx) and
|
|
2
|
+
// returns a WriteResult.
|
|
3
|
+
//
|
|
4
|
+
// Execution model:
|
|
5
|
+
// 1. Build the immutable step list by invoking the pipeline closure.
|
|
6
|
+
// 2. Walk the list sequentially via runStepList. For each step:
|
|
7
|
+
// - Look up the registered StepDef by kind.
|
|
8
|
+
// - Build the live PipelineCtx (handler-ctx + accumulated steps + scope).
|
|
9
|
+
// - Call step.run(args, ctx) — capture its return value.
|
|
10
|
+
// - Detect the sentinel RETURN_RESULT_KEY → end pipeline with that
|
|
11
|
+
// WriteResult; otherwise stash the value under resultKey if any.
|
|
12
|
+
// 3. If the loop exhausts without a `return` step, surface a loud error
|
|
13
|
+
// so a forgotten r.step.return doesn't fall through silently.
|
|
14
|
+
//
|
|
15
|
+
// runStepList is exported for sub-step builders (branch/forEach in M.1.6)
|
|
16
|
+
// — they invoke it recursively over their own step-arrays, sharing the
|
|
17
|
+
// outer pipeline's stepsAcc + scopeAcc maps so sub-step results
|
|
18
|
+
// propagate up to subsequent top-level steps.
|
|
19
|
+
//
|
|
20
|
+
// Failure-strategy is "throw" only in M.1 — runPipeline lets thrown
|
|
21
|
+
// errors propagate to the dispatcher's catch which maps them to
|
|
22
|
+
// WriteFailure / HTTP. "return" / "skip" / fallback strategies land in
|
|
23
|
+
// later slices together with their own integration tests.
|
|
24
|
+
|
|
25
|
+
import { getStep } from "./define-step";
|
|
26
|
+
import { buildPipelineSteps } from "./pipeline";
|
|
27
|
+
import { SUSPEND_SENTINEL } from "./steps/_step-dispatch-constants";
|
|
28
|
+
import { RETURN_RESULT_KEY } from "./steps/return";
|
|
29
|
+
import type { KumikoEventTypeMap } from "./types/event-type-map";
|
|
30
|
+
import type { HandlerContext, WriteEvent, WriteResult } from "./types/handlers";
|
|
31
|
+
import type { PipelineCtx, PipelineDef, StepInstance } from "./types/step";
|
|
32
|
+
|
|
33
|
+
// Result of walking a step-list. "return" surfaces the WriteResult of an
|
|
34
|
+
// r.step.return; "exhausted" means all steps ran without hitting a return
|
|
35
|
+
// — the caller (top-level pipeline) treats that as an error, sub-step
|
|
36
|
+
// callers (branch/forEach) treat it as normal completion.
|
|
37
|
+
// "suspended" means a Tier-3 step returned SUSPEND_SENTINEL — the pipeline
|
|
38
|
+
// is paused pending external (time/event) and must be resumed later.
|
|
39
|
+
export type StepListOutcome =
|
|
40
|
+
| { readonly kind: "return"; readonly result: WriteResult<unknown> }
|
|
41
|
+
| { readonly kind: "suspended"; readonly stepIndex: number }
|
|
42
|
+
| { readonly kind: "exhausted" };
|
|
43
|
+
|
|
44
|
+
export async function runPipeline<TPayload, TData, TMap extends object = KumikoEventTypeMap>(
|
|
45
|
+
pipelineDef: PipelineDef<TPayload, TData>,
|
|
46
|
+
event: WriteEvent<TPayload>,
|
|
47
|
+
handlerCtx: HandlerContext<TMap>,
|
|
48
|
+
workflow?: PipelineCtx["workflow"],
|
|
49
|
+
resumeFrom?: number,
|
|
50
|
+
): Promise<WriteResult<TData>> {
|
|
51
|
+
const steps = buildPipelineSteps(pipelineDef, event);
|
|
52
|
+
const stepsAcc: Record<string, unknown> = {};
|
|
53
|
+
const scopeAcc: Record<string, unknown> = {};
|
|
54
|
+
|
|
55
|
+
const outcome = await runStepList(
|
|
56
|
+
steps,
|
|
57
|
+
event,
|
|
58
|
+
handlerCtx,
|
|
59
|
+
stepsAcc,
|
|
60
|
+
scopeAcc,
|
|
61
|
+
workflow,
|
|
62
|
+
resumeFrom,
|
|
63
|
+
);
|
|
64
|
+
if (outcome.kind === "return") {
|
|
65
|
+
// RETURN_RESULT_KEY is only produced by r.step.return, whose run()
|
|
66
|
+
// returns WriteResult<unknown>. The pipeline's generic TData is
|
|
67
|
+
// bound at build time (defineWriteHandler ↔ pipeline<P, D>(...));
|
|
68
|
+
// matching the runtime value to that compile-time type is the
|
|
69
|
+
// contract user-side. Cast crosses that boundary.
|
|
70
|
+
return outcome.result as WriteResult<TData>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (outcome.kind === "suspended") {
|
|
74
|
+
// Suspension is only valid when running inside a workflow context.
|
|
75
|
+
// The caller (workflow-engine) handles the suspension lifecycle;
|
|
76
|
+
// we throw here because runPipeline's contract requires a WriteResult.
|
|
77
|
+
// The workflow engine calls runStepList directly to detect suspension.
|
|
78
|
+
if (!workflow) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
"Pipeline suspended without a workflow context — Tier-3 steps are only allowed inside defineWorkflow.",
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
// Return a minimal WriteResult signalling suspension. The workflow
|
|
84
|
+
// engine extracts the outcome from runStepList directly.
|
|
85
|
+
return { isSuccess: true, data: undefined } as unknown as WriteResult<TData>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
throw new Error(
|
|
89
|
+
"Pipeline ended without an r.step.return(...) — every pipeline must explicitly return a WriteResult.",
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Walk a step-list. Stateful in `stepsAcc` + `scopeAcc` (the caller's
|
|
95
|
+
* mutable maps) — sub-step builders share those with the outer pipeline
|
|
96
|
+
* so step results propagate. Returns either an early r.step.return
|
|
97
|
+
* outcome or an "exhausted" signal.
|
|
98
|
+
*
|
|
99
|
+
* Sub-step builders (branch.run, forEach.run) re-enter via this
|
|
100
|
+
* function. The same TMap-variance bridge applies — sub-steps treat
|
|
101
|
+
* ctx as PipelineCtx<unknown, KumikoEventTypeMap>, the runtime value
|
|
102
|
+
* is the outer's full HandlerContext.
|
|
103
|
+
*/
|
|
104
|
+
export async function runStepList<TPayload, TMap extends object = KumikoEventTypeMap>(
|
|
105
|
+
steps: readonly StepInstance[],
|
|
106
|
+
event: WriteEvent<TPayload>,
|
|
107
|
+
handlerCtx: HandlerContext<TMap>,
|
|
108
|
+
stepsAcc: Record<string, unknown>,
|
|
109
|
+
scopeAcc: Record<string, unknown>,
|
|
110
|
+
workflow?: PipelineCtx["workflow"],
|
|
111
|
+
resumeFrom?: number,
|
|
112
|
+
): Promise<StepListOutcome> {
|
|
113
|
+
for (const [i, instance] of steps.entries()) {
|
|
114
|
+
// On resume, skip steps at or before the resume point — their
|
|
115
|
+
// effects (waiting/retry-scheduled events) were already written
|
|
116
|
+
// during the original pipeline run. The next unexecuted step
|
|
117
|
+
// is at resumeFrom + 1.
|
|
118
|
+
if (resumeFrom !== undefined && i < resumeFrom) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// When resuming, re-execute the suspended step itself. Steps
|
|
123
|
+
// like wait/waitForEvent detect resumption by checking if a
|
|
124
|
+
// waiting event for their stepIndex already exists; retry
|
|
125
|
+
// uses retryAttempt from the workflow context.
|
|
126
|
+
// Steps that don't handle resumption (read/compute/aggregate)
|
|
127
|
+
// re-execute naturally — idempotent reads are safe, and
|
|
128
|
+
// event-sourced writes append new positions.
|
|
129
|
+
const stepDef = getStep(instance.kind);
|
|
130
|
+
if (!stepDef) {
|
|
131
|
+
throw new Error(`Unknown step kind "${instance.kind}" at step index ${i}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const pipelineCtx: PipelineCtx<TPayload, TMap> = {
|
|
135
|
+
...handlerCtx,
|
|
136
|
+
event,
|
|
137
|
+
steps: stepsAcc,
|
|
138
|
+
scope: scopeAcc,
|
|
139
|
+
...(workflow && {
|
|
140
|
+
workflow: { ...workflow, stepIndex: i },
|
|
141
|
+
}),
|
|
142
|
+
} as PipelineCtx<TPayload, TMap>;
|
|
143
|
+
|
|
144
|
+
const value = await stepDef.run(instance.args, pipelineCtx as unknown as PipelineCtx);
|
|
145
|
+
|
|
146
|
+
// Tier-3 suspension: the step wrote a waiting event and returned
|
|
147
|
+
// SUSPEND_SENTINEL to signal the pipeline should stop. The caller
|
|
148
|
+
// (defineWorkflow/workflow-engine) persists the suspension state.
|
|
149
|
+
if (value === SUSPEND_SENTINEL) {
|
|
150
|
+
return { kind: "suspended", stepIndex: i };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const key = stepDef.resultKey?.(instance.args);
|
|
154
|
+
if (key === RETURN_RESULT_KEY) {
|
|
155
|
+
return { kind: "return", result: value as WriteResult<unknown> };
|
|
156
|
+
}
|
|
157
|
+
if (key !== undefined) {
|
|
158
|
+
stepsAcc[key] = value;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { kind: "exhausted" };
|
|
162
|
+
}
|
|
@@ -173,10 +173,8 @@ export function buildUpdateSchema(
|
|
|
173
173
|
// data via the event-store-executor's `changes` payload.
|
|
174
174
|
// Cast widens the discriminated union so destructure works for variants
|
|
175
175
|
// without a `default` field; remainder is structurally a FieldDefinition.
|
|
176
|
-
const { default: _default, ...stripped } = field as FieldDefinition & {
|
|
177
|
-
|
|
178
|
-
};
|
|
179
|
-
shape[name] = fieldToZod(stripped as FieldDefinition, currencies).optional();
|
|
176
|
+
const { default: _default, ...stripped } = field as FieldDefinition & { default?: unknown }; // @cast-boundary schema-walk
|
|
177
|
+
shape[name] = fieldToZod(stripped as FieldDefinition, currencies).optional(); // @cast-boundary schema-walk
|
|
180
178
|
}
|
|
181
179
|
|
|
182
180
|
return z.object(shape);
|
|
@@ -36,7 +36,7 @@ export function defineTransitions<const TMap extends Record<string, readonly str
|
|
|
36
36
|
canTransition: (from, to) => internal.get(from)?.has(to) === true,
|
|
37
37
|
allowedFrom: (from) => {
|
|
38
38
|
const set = internal.get(from);
|
|
39
|
-
return set ? ([...set] as TStates[]) : [];
|
|
39
|
+
return set ? ([...set] as TStates[]) : []; // @cast-boundary schema-walk
|
|
40
40
|
},
|
|
41
41
|
assertTransition: (from, to) => {
|
|
42
42
|
const set = internal.get(from);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Drizzle-boundary cast helper — drizzle's `db.insert()/select()/delete()`
|
|
2
|
+
// expect a PgTable<...> shape with `enableRLS` (driver-added). The
|
|
3
|
+
// abstract `Table` we accept on step args is missing that method, so
|
|
4
|
+
// TS rejects direct assignment. Runtime is identical — drizzle's
|
|
5
|
+
// builder methods only read the table-name + column-defs, both of
|
|
6
|
+
// which `Table` carries. Cast at the boundary, document it once.
|
|
7
|
+
//
|
|
8
|
+
// Used by read-find-one, read-find-many, unsafe-projection-upsert,
|
|
9
|
+
// unsafe-projection-delete. Followup #13 (closed at the M.1.6
|
|
10
|
+
// cleanup-pass).
|
|
11
|
+
|
|
12
|
+
import type { Table } from "drizzle-orm";
|
|
13
|
+
|
|
14
|
+
// biome-ignore lint/suspicious/noExplicitAny: drizzle type-boundary
|
|
15
|
+
type DrizzleQueryTarget = any;
|
|
16
|
+
|
|
17
|
+
export function asQueryTarget(t: Table): DrizzleQueryTarget {
|
|
18
|
+
return t;
|
|
19
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// ISO-8601 duration arithmetic — shared by wait and waitForEvent steps.
|
|
2
|
+
// Accepts "P1D", "PT1H", "P1Y2M3DT4H5M6S" etc.
|
|
3
|
+
// Uses approximate calendar math (365d/year, 30d/month). See #23.
|
|
4
|
+
|
|
5
|
+
export function addDuration(baseIso: string, duration: string): string {
|
|
6
|
+
const pattern = /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/;
|
|
7
|
+
const match = duration.match(pattern);
|
|
8
|
+
if (!match) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
`Invalid ISO-8601 duration "${duration}" — expected format like "PT1H", "P1D", "P7D"`,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const parts = match.slice(1).map((n) => Number(n) || 0);
|
|
15
|
+
const years = parts[0] ?? 0;
|
|
16
|
+
const months = parts[1] ?? 0;
|
|
17
|
+
const days = parts[2] ?? 0;
|
|
18
|
+
const hours = parts[3] ?? 0;
|
|
19
|
+
const minutes = parts[4] ?? 0;
|
|
20
|
+
const seconds = parts[5] ?? 0;
|
|
21
|
+
|
|
22
|
+
// Compute in ms (Temporal.Instant.add accepts only smaller units below
|
|
23
|
+
// hours for calendar-agnostic shifts; we approximate years/months as
|
|
24
|
+
// fixed-length days, see file header).
|
|
25
|
+
let ms = years * 365 * 24 * 60 * 60 * 1000;
|
|
26
|
+
ms += months * 30 * 24 * 60 * 60 * 1000;
|
|
27
|
+
ms += days * 24 * 60 * 60 * 1000;
|
|
28
|
+
ms += hours * 60 * 60 * 1000;
|
|
29
|
+
ms += minutes * 60 * 1000;
|
|
30
|
+
ms += seconds * 1000;
|
|
31
|
+
|
|
32
|
+
return Temporal.Instant.from(baseIso).add({ milliseconds: ms }).toString();
|
|
33
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Build-time guard for Q12 — sub-pipelines (branch.onTrue/onFalse,
|
|
2
|
+
// forEach.do) may not contain r.step.return. Centralised here so the
|
|
3
|
+
// error message is wordlaut-identical across both sub-step-builders;
|
|
4
|
+
// inline duplication would drift the moment someone edits one site.
|
|
5
|
+
//
|
|
6
|
+
// Extracted at the second sub-step-builder rather than the third because
|
|
7
|
+
// the drift-risk on the Q12 wording is real (advisor M.1.6 cleanup), not
|
|
8
|
+
// because the line-count alone justifies it.
|
|
9
|
+
|
|
10
|
+
import type { StepInstance } from "../types/step";
|
|
11
|
+
|
|
12
|
+
export function validateNoReturnSteps(steps: readonly StepInstance[], where: string): void {
|
|
13
|
+
for (const step of steps) {
|
|
14
|
+
if (step.kind === "return") {
|
|
15
|
+
throw new Error(
|
|
16
|
+
`r.step.return is not allowed inside ${where} — branch/forEach are side-effect containers (Q12). ` +
|
|
17
|
+
`Restructure the pipeline so the return happens at the top level.`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// Resolver-call helpers — eliminate the typeof-narrow boilerplate that
|
|
2
|
+
// was repeated across 11 step files (return, compute, branch, forEach,
|
|
3
|
+
// read.findOne/findMany, aggregate.create/update/appendEvent,
|
|
4
|
+
// unsafeProjectionUpsert/Delete).
|
|
5
|
+
//
|
|
6
|
+
// Pattern before:
|
|
7
|
+
// const value = typeof args.x === "function" ? args.x(ctx) : args.x;
|
|
8
|
+
//
|
|
9
|
+
// Pattern after:
|
|
10
|
+
// const value = resolveRequired(args.x, ctx);
|
|
11
|
+
//
|
|
12
|
+
// The local-alias version (`const r = args.x; typeof r === "function" ? ...`)
|
|
13
|
+
// was needed to satisfy TS narrowing on a property-access; passing the
|
|
14
|
+
// arg through a function-call achieves the same narrowing trivially via
|
|
15
|
+
// the function-parameter binding.
|
|
16
|
+
//
|
|
17
|
+
// Followup #10 (closed at the M.1.6 cleanup-pass).
|
|
18
|
+
|
|
19
|
+
import type { PipelineCtx, StepResolver } from "../types/step";
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a required StepResolver — either a static value or a function.
|
|
23
|
+
* Throws if `arg` is undefined; caller must guarantee presence.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveRequired<T>(arg: StepResolver<T>, ctx: PipelineCtx): T {
|
|
26
|
+
if (typeof arg === "function") {
|
|
27
|
+
return (arg as (c: PipelineCtx) => T)(ctx);
|
|
28
|
+
}
|
|
29
|
+
return arg;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve an optional StepResolver — returns undefined when arg is
|
|
34
|
+
* undefined, otherwise the resolved value.
|
|
35
|
+
*/
|
|
36
|
+
export function resolveOptional<T>(
|
|
37
|
+
arg: StepResolver<T> | undefined,
|
|
38
|
+
ctx: PipelineCtx,
|
|
39
|
+
): T | undefined {
|
|
40
|
+
if (arg === undefined) return undefined;
|
|
41
|
+
return resolveRequired(arg, ctx);
|
|
42
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Shared constants for the deferred-step dispatcher pipeline. Extracted
|
|
2
|
+
// so individual step-builders (webhook.send, mail.send, ...) don't
|
|
3
|
+
// import from each other and silently break on a future split.
|
|
4
|
+
|
|
5
|
+
export const STEP_DISPATCH_AGGREGATE_TYPE = "step-dispatch";
|
|
6
|
+
// System-event namespace (kumiko:system:*) — bypasses registry +
|
|
7
|
+
// ownership checks in append-event-core. Reserved for framework-internal
|
|
8
|
+
// step-engine coordination. The bundled step-dispatcher MSP listens
|
|
9
|
+
// for the literal type-string.
|
|
10
|
+
export const STEP_DISPATCH_REQUESTED_TYPE = "kumiko:system:step.dispatch-requested";
|
|
11
|
+
export const STEP_DISPATCHED_TYPE = "kumiko:system:step.dispatched";
|
|
12
|
+
export const STEP_DISPATCH_FAILED_TYPE = "kumiko:system:step.dispatch-failed";
|
|
13
|
+
|
|
14
|
+
// --- Tier-3 / Workflow async step constants ---
|
|
15
|
+
// Written by wait / waitForEvent / retry steps onto the workflow-run stream.
|
|
16
|
+
// The Resume-Loop reads these to decide when to resume a suspended run.
|
|
17
|
+
export const WORKFLOW_WAITING_TYPE = "kumiko:system:workflow.step.waiting";
|
|
18
|
+
export const WORKFLOW_WAITING_FOR_EVENT_TYPE = "kumiko:system:workflow.step.waiting-for-event";
|
|
19
|
+
export const WORKFLOW_RESUMED_TYPE = "kumiko:system:workflow.step.resumed";
|
|
20
|
+
|
|
21
|
+
// Workflow-run aggregate type — each workflow run is an event-sourced
|
|
22
|
+
// aggregate stream.
|
|
23
|
+
export const WORKFLOW_AGGREGATE_TYPE = "workflow-run";
|
|
24
|
+
|
|
25
|
+
// Workflow-run lifecycle events — written by the event-trigger subscriber
|
|
26
|
+
// and the resume-loop onto a workflow-run aggregate stream.
|
|
27
|
+
export const WORKFLOW_RUN_STARTED_TYPE = "kumiko:system:workflow.run-started";
|
|
28
|
+
export const WORKFLOW_RUN_COMPLETED_TYPE = "kumiko:system:workflow.run-completed";
|
|
29
|
+
export const WORKFLOW_RUN_FAILED_TYPE = "kumiko:system:workflow.run-failed";
|
|
30
|
+
|
|
31
|
+
// Step return sentinel — when a step's run() returns this value,
|
|
32
|
+
// runStepList stops and yields a "suspended" outcome. The caller
|
|
33
|
+
// (defineWorkflow / workflow-engine) persists the suspension state.
|
|
34
|
+
export const SUSPEND_SENTINEL = Symbol("kumiko:step:suspend");
|
|
35
|
+
|
|
36
|
+
// Workflow retry scheduled — written by the retry step when a sub-pipeline
|
|
37
|
+
// fails and a retry attempt is scheduled with backoff.
|
|
38
|
+
export const WORKFLOW_RETRY_SCHEDULED_TYPE = "kumiko:system:workflow.retry.scheduled";
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// r.step.aggregate.appendEvent — write an additional domain-event onto
|
|
2
|
+
// an existing aggregate stream.
|
|
3
|
+
//
|
|
4
|
+
// Wraps ctx.unsafeAppendEvent: the event lands on the named aggregate
|
|
5
|
+
// stream in the active TX, downstream projections (multiStreamProjection)
|
|
6
|
+
// fire, audit-trail captures it. Used when a write-handler needs to
|
|
7
|
+
// record a domain event that's NOT one of the auto-generated CRUD
|
|
8
|
+
// events (e.g. "incident.update-posted" on the same incident stream
|
|
9
|
+
// that already carries "incident.created").
|
|
10
|
+
//
|
|
11
|
+
// Why `unsafeAppendEvent` (not the strict `appendEvent`): step.run sees
|
|
12
|
+
// `ctx` as PipelineCtx<unknown, KumikoEventTypeMap> after the variance-
|
|
13
|
+
// bridge cast in run-pipeline.ts. The strict TMap-typed appendEvent
|
|
14
|
+
// would collapse `keyof TMap` to `never` from the framework-side. Strict
|
|
15
|
+
// typing of appendEvent inside steps is a deferred pass (post-M.1.5).
|
|
16
|
+
// At the call-site users still spell `type` as a string-literal — TS
|
|
17
|
+
// catches typos against the QualifiedEventName union when the literal
|
|
18
|
+
// matches a registered EventDef.
|
|
19
|
+
//
|
|
20
|
+
// No result-key — appendEvent doesn't surface a value to subsequent
|
|
21
|
+
// steps (the event-store assigns the position, but consumers don't
|
|
22
|
+
// need it during the same handler call).
|
|
23
|
+
|
|
24
|
+
import { defineStep } from "../define-step";
|
|
25
|
+
import type { PipelineCtx, StepInstance, StepResolver } from "../types/step";
|
|
26
|
+
import { resolveOptional, resolveRequired } from "./_resolver-utils";
|
|
27
|
+
|
|
28
|
+
type AggregateAppendEventArgs = {
|
|
29
|
+
readonly aggregateId: StepResolver<string>;
|
|
30
|
+
readonly aggregateType: string;
|
|
31
|
+
readonly type: string;
|
|
32
|
+
readonly payload: StepResolver<unknown>;
|
|
33
|
+
readonly headers?: StepResolver<Readonly<Record<string, string | number | boolean>>>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
defineStep<AggregateAppendEventArgs, void>({
|
|
37
|
+
kind: "aggregate.appendEvent",
|
|
38
|
+
defaultFailureStrategy: "throw",
|
|
39
|
+
run: async (args, ctx: PipelineCtx) => {
|
|
40
|
+
const aggregateId = resolveRequired(args.aggregateId, ctx);
|
|
41
|
+
const payload = resolveRequired(args.payload, ctx);
|
|
42
|
+
const headers = resolveOptional(args.headers, ctx);
|
|
43
|
+
|
|
44
|
+
await ctx.unsafeAppendEvent({
|
|
45
|
+
aggregateId,
|
|
46
|
+
aggregateType: args.aggregateType,
|
|
47
|
+
type: args.type,
|
|
48
|
+
payload,
|
|
49
|
+
...(headers !== undefined && { headers }),
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
export function buildAggregateAppendEventStep(args: AggregateAppendEventArgs): StepInstance {
|
|
55
|
+
return { kind: "aggregate.appendEvent", args };
|
|
56
|
+
}
|