@cosmicdrift/kumiko-framework 0.2.2 → 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 +54 -0
- package/package.json +124 -38
- 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/auth/__tests__/roles.test.ts +24 -0
- package/src/auth/index.ts +7 -0
- package/src/auth/roles.ts +42 -0
- package/src/compliance/__tests__/duration-spec.test.ts +72 -0
- package/src/compliance/__tests__/profiles.test.ts +308 -0
- package/src/compliance/__tests__/sub-processors.test.ts +139 -0
- package/src/compliance/duration-spec.ts +44 -0
- package/src/compliance/index.ts +31 -0
- package/src/compliance/override-schema.ts +136 -0
- package/src/compliance/profiles.ts +427 -0
- package/src/compliance/sub-processors.ts +152 -0
- package/src/db/__tests__/big-int-field.test.ts +131 -0
- 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 +20 -5
- package/src/db/tenant-db.ts +9 -9
- package/src/engine/__tests__/_pipeline-test-utils.ts +23 -0
- package/src/engine/__tests__/boot-validator-api-exposure.test.ts +142 -0
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +570 -0
- package/src/engine/__tests__/boot-validator-s0-integration.test.ts +160 -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 -1528
- 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 +127 -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/extension-names.ts +105 -0
- package/src/engine/extensions/user-data.ts +106 -0
- package/src/engine/factories.ts +26 -16
- 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 +13 -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 +71 -1
- package/src/engine/feature-ast/render.ts +31 -1
- package/src/engine/index.ts +66 -2
- package/src/engine/pattern-library/__tests__/library.test.ts +11 -0
- package/src/engine/pattern-library/library.ts +78 -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 +10 -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 +143 -1
- package/src/engine/types/fields.ts +134 -10
- package/src/engine/types/handlers.ts +18 -10
- package/src/engine/types/identifiers.ts +1 -0
- package/src/engine/types/index.ts +15 -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/__tests__/read-stream.test.ts +105 -0
- package/src/files/__tests__/write-stream.test.ts +233 -0
- package/src/files/__tests__/zip-stream.test.ts +357 -0
- package/src/files/file-routes.ts +1 -1
- package/src/files/in-memory-provider.ts +38 -0
- package/src/files/index.ts +3 -0
- package/src/files/local-provider.ts +58 -1
- package/src/files/types.ts +36 -8
- package/src/files/zip-stream.ts +251 -0
- 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 -2562
|
@@ -56,15 +56,20 @@ import type {
|
|
|
56
56
|
SecretOptions,
|
|
57
57
|
TranslationKeys,
|
|
58
58
|
TranslationsDef,
|
|
59
|
+
TreeActionDef,
|
|
60
|
+
TreeActionsHandle,
|
|
61
|
+
TreeChildrenSubscribe,
|
|
59
62
|
ValidationHookFn,
|
|
60
63
|
WriteHandlerDef,
|
|
61
64
|
WriteHandlerFn,
|
|
62
65
|
} from "./types";
|
|
63
66
|
import { HookPhases } from "./types";
|
|
67
|
+
import type { RequiresApi } from "./types/feature";
|
|
64
68
|
import { resolveName } from "./types/handlers";
|
|
65
69
|
import type { HttpRouteDefinition } from "./types/http-route";
|
|
66
70
|
import type { NavDefinition } from "./types/nav";
|
|
67
71
|
import type { ScreenDefinition } from "./types/screen";
|
|
72
|
+
import type { PipelineDef } from "./types/step";
|
|
68
73
|
import type { WorkspaceDefinition } from "./types/workspace";
|
|
69
74
|
|
|
70
75
|
const LIFECYCLE_TYPES = Object.values(LifecycleHookTypes);
|
|
@@ -87,6 +92,11 @@ export function defineFeature<const TName extends string, TExports = undefined>(
|
|
|
87
92
|
): FeatureDefinition & { readonly exports: TExports } {
|
|
88
93
|
const requires: string[] = [];
|
|
89
94
|
const optionalRequires: string[] = [];
|
|
95
|
+
// Read-side projection-tables declared via r.requires.projection("table").
|
|
96
|
+
// Boot-validator checks unsafeProjection-* step calls against this set.
|
|
97
|
+
const requiredProjections = new Set<string>();
|
|
98
|
+
// Tier-2 step kinds declared via r.requires.step("webhook.send"). Q9.
|
|
99
|
+
const requiredSteps = new Set<string>();
|
|
90
100
|
const entities: Record<string, EntityDefinition> = {};
|
|
91
101
|
const relations: Record<string, Record<string, RelationDefinition>> = {};
|
|
92
102
|
const writeHandlers: Record<string, WriteHandlerDef> = {};
|
|
@@ -112,6 +122,8 @@ export function defineFeature<const TName extends string, TExports = undefined>(
|
|
|
112
122
|
const notifications: Record<string, NotificationDefinition> = {};
|
|
113
123
|
const registrarExtensions: Record<string, RegistrarExtensionDef> = {};
|
|
114
124
|
const extensionUsages: RegistrarExtensionRegistration[] = [];
|
|
125
|
+
const exposedApis: Set<string> = new Set();
|
|
126
|
+
const usedApis: Set<string> = new Set();
|
|
115
127
|
const referenceData: ReferenceDataDef[] = [];
|
|
116
128
|
const handlerEntityMappings: Record<string, string> = {};
|
|
117
129
|
const metrics: Record<string, FeatureMetricDef> = {};
|
|
@@ -133,6 +145,14 @@ export function defineFeature<const TName extends string, TExports = undefined>(
|
|
|
133
145
|
|
|
134
146
|
let isSystemScoped = false;
|
|
135
147
|
let toggleableDefault: boolean | undefined;
|
|
148
|
+
// Visual-Tree-Slots — at-most-one per feature, only-once-guard im
|
|
149
|
+
// registrar (siehe r.treeActions / r.tree). Undefined wenn das Feature
|
|
150
|
+
// keinen Visual-Tree-Beitrag liefert (Zero-Whitelist-Filter).
|
|
151
|
+
// Name-Collision-Sicherheit: object-literal-method-Names im registrar
|
|
152
|
+
// sind keine bindings im closure-scope, daher kollidiert die `treeActions`
|
|
153
|
+
// closure-let-var nicht mit der `treeActions(...)` registrar-Methode.
|
|
154
|
+
let treeActions: Readonly<Record<string, TreeActionDef>> | undefined;
|
|
155
|
+
let treeProvider: TreeChildrenSubscribe | undefined;
|
|
136
156
|
|
|
137
157
|
// Map handler name to entity via colon convention.
|
|
138
158
|
// "task:create" → entity "task". No colon → standalone handler, no mapping.
|
|
@@ -151,9 +171,18 @@ export function defineFeature<const TName extends string, TExports = undefined>(
|
|
|
151
171
|
isSystemScoped = true;
|
|
152
172
|
},
|
|
153
173
|
|
|
154
|
-
requires
|
|
155
|
-
|
|
156
|
-
|
|
174
|
+
requires: (() => {
|
|
175
|
+
const fn = (...featureNames: string[]) => {
|
|
176
|
+
requires.push(...featureNames);
|
|
177
|
+
};
|
|
178
|
+
fn.projection = (tableName: string) => {
|
|
179
|
+
requiredProjections.add(tableName);
|
|
180
|
+
};
|
|
181
|
+
fn.step = (stepKind: string) => {
|
|
182
|
+
requiredSteps.add(stepKind);
|
|
183
|
+
};
|
|
184
|
+
return fn as RequiresApi;
|
|
185
|
+
})(),
|
|
157
186
|
|
|
158
187
|
optionalRequires(...featureNames: string[]): void {
|
|
159
188
|
optionalRequires.push(...featureNames);
|
|
@@ -184,11 +213,27 @@ export function defineFeature<const TName extends string, TExports = undefined>(
|
|
|
184
213
|
writeHandlers[def.name] = {
|
|
185
214
|
name: def.name,
|
|
186
215
|
schema: def.schema,
|
|
187
|
-
// @cast-boundary engine-bridge — typed Dev-API
|
|
216
|
+
// @cast-boundary engine-bridge — typed Dev-API's handler is
|
|
217
|
+
// generic over the schema's parsed payload (`WriteEvent<output<TSchema>>`),
|
|
218
|
+
// the storage form WriteHandlerFn carries `WriteEvent<unknown>`.
|
|
219
|
+
// Function-arg variance: TS sees the typed handler as stricter
|
|
220
|
+
// than the loose storage shape and rejects direct assignment.
|
|
221
|
+
// The runtime value is identical — the cast crosses that boundary.
|
|
222
|
+
// `satisfies` does not work here (it asserts assignability, which
|
|
223
|
+
// is what fails). Explicit cast is the right tool.
|
|
188
224
|
handler: def.handler as WriteHandlerFn,
|
|
189
225
|
...(def.access && { access: def.access }),
|
|
190
|
-
...(def.
|
|
226
|
+
...(def.unsafeSkipTransitionGuard && { unsafeSkipTransitionGuard: true }),
|
|
191
227
|
...(def.rateLimit && { rateLimit: def.rateLimit }),
|
|
228
|
+
// Forward the pipeline-build closure so boot-validators and
|
|
229
|
+
// Designer/AI tooling can inspect the step list. Absent on
|
|
230
|
+
// free-form handlers — defineWriteHandler only sets `perform`
|
|
231
|
+
// when the author used the pipeline form. Variance cast
|
|
232
|
+
// mirrors the handler-cast above: PipelineDef<output<TSchema>>
|
|
233
|
+
// is stricter than PipelineDef<unknown> for the same reason.
|
|
234
|
+
...(def.perform !== undefined && {
|
|
235
|
+
perform: def.perform as PipelineDef,
|
|
236
|
+
}),
|
|
192
237
|
};
|
|
193
238
|
tryMapEntity(def.name);
|
|
194
239
|
return { name: def.name };
|
|
@@ -218,7 +263,7 @@ export function defineFeature<const TName extends string, TExports = undefined>(
|
|
|
218
263
|
name: def.name,
|
|
219
264
|
schema: def.schema,
|
|
220
265
|
// @cast-boundary engine-bridge — typed Dev-API → erased internal storage
|
|
221
|
-
handler: def.handler as QueryHandlerFn,
|
|
266
|
+
handler: def.handler as QueryHandlerFn, // @cast-boundary engine-bridge
|
|
222
267
|
...(def.access && { access: def.access }),
|
|
223
268
|
...(def.rateLimit && { rateLimit: def.rateLimit }),
|
|
224
269
|
};
|
|
@@ -343,7 +388,7 @@ export function defineFeature<const TName extends string, TExports = undefined>(
|
|
|
343
388
|
? {
|
|
344
389
|
on: Array.isArray(options.trigger.on)
|
|
345
390
|
? options.trigger.on.map(resolveName)
|
|
346
|
-
: resolveName(options.trigger.on as NameOrRef),
|
|
391
|
+
: resolveName(options.trigger.on as NameOrRef), // @cast-boundary engine-bridge
|
|
347
392
|
}
|
|
348
393
|
: options.trigger;
|
|
349
394
|
jobs[jobName] = { ...options, trigger, name: jobName, handler };
|
|
@@ -464,6 +509,41 @@ export function defineFeature<const TName extends string, TExports = undefined>(
|
|
|
464
509
|
extensionUsages.push({ extensionName, entityName: resolveName(entityRef), options });
|
|
465
510
|
},
|
|
466
511
|
|
|
512
|
+
/**
|
|
513
|
+
* Marker-Deklaration: dieses Feature stellt eine Cross-Feature-API
|
|
514
|
+
* unter dem genannten Namen bereit. Die eigentliche Implementation
|
|
515
|
+
* wird separat als Query- oder Write-Handler unter dem QN-Pattern
|
|
516
|
+
* registriert; r.exposesApi ist reine Boot-Check-Surface.
|
|
517
|
+
*
|
|
518
|
+
* Beispiel:
|
|
519
|
+
* defineFeature("compliance-profiles", (r) => {
|
|
520
|
+
* r.exposesApi("compliance.forTenant");
|
|
521
|
+
* r.queryHandler({ name: "compliance:query:for-tenant", ... });
|
|
522
|
+
* });
|
|
523
|
+
* defineFeature("user-data-rights", (r) => {
|
|
524
|
+
* r.requires("compliance-profiles");
|
|
525
|
+
* r.usesApi("compliance.forTenant");
|
|
526
|
+
* // ruft im Handler: ctx.callQuery("compliance:query:for-tenant", ...)
|
|
527
|
+
* });
|
|
528
|
+
*/
|
|
529
|
+
exposesApi(apiName: string): void {
|
|
530
|
+
if (exposedApis.has(apiName)) {
|
|
531
|
+
throw new Error(
|
|
532
|
+
`[Feature ${name}] r.exposesApi("${apiName}") called twice — API names must be unique within a feature.`,
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
exposedApis.add(apiName);
|
|
536
|
+
},
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Declares that this feature calls a cross-feature API. Boot-Validator
|
|
540
|
+
* checkt dass irgendein anderes Feature `r.exposesApi(name)` macht und
|
|
541
|
+
* dass dieses Feature `r.requires` darauf hat.
|
|
542
|
+
*/
|
|
543
|
+
usesApi(apiName: string): void {
|
|
544
|
+
usedApis.add(apiName);
|
|
545
|
+
},
|
|
546
|
+
|
|
467
547
|
metric(shortName: string, options: MetricOptions): void {
|
|
468
548
|
if (metrics[shortName]) {
|
|
469
549
|
throw new Error(
|
|
@@ -686,9 +766,41 @@ export function defineFeature<const TName extends string, TExports = undefined>(
|
|
|
686
766
|
};
|
|
687
767
|
return { name: qualifiedName, type: options.type };
|
|
688
768
|
},
|
|
769
|
+
|
|
770
|
+
treeActions<const TActions extends Record<string, TreeActionDef>>(
|
|
771
|
+
actions: TActions,
|
|
772
|
+
): TreeActionsHandle<TName, TActions> {
|
|
773
|
+
// Only-once-guard: zweiter Aufruf ist Author-Bug, soll am
|
|
774
|
+
// Feature-File aufschlagen (gleicher Stil wie r.toggleable).
|
|
775
|
+
if (treeActions !== undefined) {
|
|
776
|
+
throw new Error(
|
|
777
|
+
`[Feature ${name}] r.treeActions() already called. ` +
|
|
778
|
+
`Each feature may declare a single tree-actions schema.`,
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
treeActions = actions;
|
|
782
|
+
// Return typed handle für setup-export. Frozen damit Caller die
|
|
783
|
+
// Map nicht nachträglich mutieren (würde Pattern-AST + Runtime-
|
|
784
|
+
// Lookup divergieren lassen).
|
|
785
|
+
return Object.freeze({
|
|
786
|
+
id: name,
|
|
787
|
+
treeActions: actions,
|
|
788
|
+
});
|
|
789
|
+
},
|
|
790
|
+
|
|
791
|
+
tree(provider: TreeChildrenSubscribe): void {
|
|
792
|
+
// Only-once-guard analog zu r.treeActions.
|
|
793
|
+
if (treeProvider !== undefined) {
|
|
794
|
+
throw new Error(
|
|
795
|
+
`[Feature ${name}] r.tree() already called. ` +
|
|
796
|
+
`Each feature may declare a single tree-provider.`,
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
treeProvider = provider;
|
|
800
|
+
},
|
|
689
801
|
};
|
|
690
802
|
|
|
691
|
-
const exports = setup(registrar) as TExports;
|
|
803
|
+
const exports = setup(registrar) as TExports; // @cast-boundary engine-bridge
|
|
692
804
|
|
|
693
805
|
return {
|
|
694
806
|
name,
|
|
@@ -696,6 +808,8 @@ export function defineFeature<const TName extends string, TExports = undefined>(
|
|
|
696
808
|
exports,
|
|
697
809
|
requires,
|
|
698
810
|
optionalRequires,
|
|
811
|
+
requiredProjections,
|
|
812
|
+
requiredSteps,
|
|
699
813
|
...(toggleableDefault !== undefined && { toggleableDefault }),
|
|
700
814
|
entities,
|
|
701
815
|
relations,
|
|
@@ -709,7 +823,7 @@ export function defineFeature<const TName extends string, TExports = undefined>(
|
|
|
709
823
|
preDelete: phasedLifecycleHooks.preDelete,
|
|
710
824
|
postDelete: phasedLifecycleHooks.postDelete,
|
|
711
825
|
preQuery: lifecycleHooks["preQuery"] ?? {},
|
|
712
|
-
} as HookMap,
|
|
826
|
+
} as HookMap, // @cast-boundary engine-payload
|
|
713
827
|
entityHooks: {
|
|
714
828
|
postSave: entityPostSave,
|
|
715
829
|
preDelete: entityPreDelete,
|
|
@@ -720,6 +834,8 @@ export function defineFeature<const TName extends string, TExports = undefined>(
|
|
|
720
834
|
notifications,
|
|
721
835
|
registrarExtensions,
|
|
722
836
|
extensionUsages,
|
|
837
|
+
exposedApis,
|
|
838
|
+
usedApis,
|
|
723
839
|
referenceData,
|
|
724
840
|
events,
|
|
725
841
|
eventMigrations,
|
|
@@ -736,5 +852,7 @@ export function defineFeature<const TName extends string, TExports = undefined>(
|
|
|
736
852
|
workspaces,
|
|
737
853
|
httpRoutes,
|
|
738
854
|
rawTables,
|
|
855
|
+
...(treeActions !== undefined && { treeActions }),
|
|
856
|
+
...(treeProvider !== undefined && { treeProvider }),
|
|
739
857
|
};
|
|
740
858
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ZodType, z } from "zod";
|
|
2
|
+
import { runPipeline } from "./run-pipeline";
|
|
2
3
|
import type {
|
|
3
4
|
AccessRule,
|
|
4
5
|
HandlerContext,
|
|
@@ -8,6 +9,7 @@ import type {
|
|
|
8
9
|
WriteEvent,
|
|
9
10
|
WriteResult,
|
|
10
11
|
} from "./types";
|
|
12
|
+
import type { PipelineDef } from "./types/step";
|
|
11
13
|
|
|
12
14
|
// --- Write Handler Definition ---
|
|
13
15
|
//
|
|
@@ -19,6 +21,10 @@ import type {
|
|
|
19
21
|
// definition-site (framework's compile, where the augmentation isn't
|
|
20
22
|
// visible) and collapse `keyof TMap` to `never`. See the spike-findings
|
|
21
23
|
// memory for the empirical proof.
|
|
24
|
+
//
|
|
25
|
+
// Two authoring forms — `handler` (free-form) or `perform: pipeline(...)`
|
|
26
|
+
// (step-pipeline). A `perform` is compiled to a handler-function at
|
|
27
|
+
// definition time; the dispatcher only ever sees `handler`.
|
|
22
28
|
|
|
23
29
|
export type WriteHandlerDefinition<
|
|
24
30
|
TName extends string = string,
|
|
@@ -29,23 +35,103 @@ export type WriteHandlerDefinition<
|
|
|
29
35
|
readonly name: TName;
|
|
30
36
|
readonly schema: TSchema;
|
|
31
37
|
readonly access?: AccessRule;
|
|
32
|
-
readonly
|
|
38
|
+
readonly unsafeSkipTransitionGuard?: boolean;
|
|
33
39
|
readonly rateLimit?: RateLimitOption;
|
|
34
40
|
readonly handler: (
|
|
35
41
|
event: WriteEvent<z.infer<TSchema>>,
|
|
36
42
|
context: HandlerContext<TMap>,
|
|
37
43
|
) => Promise<WriteResult<TData>>;
|
|
44
|
+
// Preserved when the author wrote a `perform` block — the original
|
|
45
|
+
// PipelineDef. Designer/AI/AST tools read this when present; the
|
|
46
|
+
// dispatcher ignores it and just calls `handler`. Absent on free-form
|
|
47
|
+
// handlers.
|
|
48
|
+
readonly perform?: PipelineDef<z.infer<TSchema>, TData>;
|
|
38
49
|
};
|
|
39
50
|
|
|
51
|
+
// Author-facing input — accepts either the free-form `handler` or the
|
|
52
|
+
// pipeline-form `perform`. defineWriteHandler narrows them to the
|
|
53
|
+
// canonical WriteHandlerDefinition shape.
|
|
54
|
+
export type WriteHandlerInput<
|
|
55
|
+
TName extends string = string,
|
|
56
|
+
TSchema extends ZodType = ZodType,
|
|
57
|
+
TData = unknown,
|
|
58
|
+
TMap extends object = KumikoEventTypeMap,
|
|
59
|
+
> = {
|
|
60
|
+
readonly name: TName;
|
|
61
|
+
readonly schema: TSchema;
|
|
62
|
+
readonly access?: AccessRule;
|
|
63
|
+
readonly unsafeSkipTransitionGuard?: boolean;
|
|
64
|
+
readonly rateLimit?: RateLimitOption;
|
|
65
|
+
} & (
|
|
66
|
+
| {
|
|
67
|
+
readonly handler: (
|
|
68
|
+
event: WriteEvent<z.infer<TSchema>>,
|
|
69
|
+
context: HandlerContext<TMap>,
|
|
70
|
+
) => Promise<WriteResult<TData>>;
|
|
71
|
+
readonly perform?: never;
|
|
72
|
+
}
|
|
73
|
+
| {
|
|
74
|
+
readonly perform: PipelineDef<z.infer<TSchema>, TData>;
|
|
75
|
+
readonly handler?: never;
|
|
76
|
+
}
|
|
77
|
+
);
|
|
78
|
+
|
|
40
79
|
export function defineWriteHandler<
|
|
41
80
|
const TName extends string,
|
|
42
81
|
TSchema extends ZodType,
|
|
43
82
|
TData = unknown,
|
|
44
83
|
TMap extends object = KumikoEventTypeMap,
|
|
45
84
|
>(
|
|
46
|
-
def:
|
|
85
|
+
def: WriteHandlerInput<TName, TSchema, TData, TMap>,
|
|
47
86
|
): WriteHandlerDefinition<TName, TSchema, TData, TMap> {
|
|
48
|
-
|
|
87
|
+
// Runtime-guard against accidentally setting BOTH handler+perform.
|
|
88
|
+
// The discriminated-union type-error
|
|
89
|
+
// "Type 'PipelineDef<...>' is not assignable to type 'undefined'."
|
|
90
|
+
// is functional but cryptic for less TS-experienced users; this throws
|
|
91
|
+
// a name-and-explanation error message instead. Followup #3.
|
|
92
|
+
// The cast is necessary because the discriminated union narrows
|
|
93
|
+
// `handler` away once `perform` is present (and vice-versa) — at this
|
|
94
|
+
// boundary we want to read both regardless of the narrowing.
|
|
95
|
+
const probe = def as {
|
|
96
|
+
readonly handler?: unknown;
|
|
97
|
+
readonly perform?: unknown;
|
|
98
|
+
readonly name: TName;
|
|
99
|
+
};
|
|
100
|
+
if (probe.handler !== undefined && probe.perform !== undefined) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`defineWriteHandler("${def.name}"): both \`handler\` and \`perform\` are set. ` +
|
|
103
|
+
`Pick one — \`handler\` for the free-form async function, ` +
|
|
104
|
+
`\`perform: pipeline(...)\` for the step-pipeline form. ` +
|
|
105
|
+
`(See step-vocabulary.md for which form fits.)`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Conditional spreads (`...(def.access && { access: def.access })`)
|
|
110
|
+
// mirror the existing convention in entity-handlers.ts /
|
|
111
|
+
// define-feature.ts — optional fields stay absent rather than being
|
|
112
|
+
// serialised as `key: undefined`.
|
|
113
|
+
const base = {
|
|
114
|
+
name: def.name,
|
|
115
|
+
schema: def.schema,
|
|
116
|
+
...(def.access && { access: def.access }),
|
|
117
|
+
...(def.unsafeSkipTransitionGuard && {
|
|
118
|
+
unsafeSkipTransitionGuard: def.unsafeSkipTransitionGuard,
|
|
119
|
+
}),
|
|
120
|
+
...(def.rateLimit && { rateLimit: def.rateLimit }),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if ("perform" in def && def.perform !== undefined) {
|
|
124
|
+
const performDef = def.perform;
|
|
125
|
+
const compiledHandler = async (
|
|
126
|
+
event: WriteEvent<z.infer<TSchema>>,
|
|
127
|
+
ctx: HandlerContext<TMap>,
|
|
128
|
+
): Promise<WriteResult<TData>> => {
|
|
129
|
+
return runPipeline<z.infer<TSchema>, TData, TMap>(performDef, event, ctx);
|
|
130
|
+
};
|
|
131
|
+
return { ...base, handler: compiledHandler, perform: performDef };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { ...base, handler: def.handler };
|
|
49
135
|
}
|
|
50
136
|
|
|
51
137
|
// --- Query Handler Definition ---
|
|
@@ -11,9 +11,9 @@ type RoleMap<T extends readonly string[]> = {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
export function defineRoles<const T extends readonly string[]>(roles: T): RoleMap<T> {
|
|
14
|
-
const map = {} as Record<string, string>;
|
|
14
|
+
const map = {} as Record<string, string>; // @cast-boundary schema-walk
|
|
15
15
|
for (const role of roles) {
|
|
16
16
|
map[role] = role;
|
|
17
17
|
}
|
|
18
|
-
return map as RoleMap<T>;
|
|
18
|
+
return map as RoleMap<T>; // @cast-boundary schema-walk
|
|
19
19
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Step-Registry + defineStep factory.
|
|
2
|
+
//
|
|
3
|
+
// Steps are registered at module-load time via defineStep(). The runtime
|
|
4
|
+
// (run-pipeline.ts) looks them up by `kind` to dispatch run-calls.
|
|
5
|
+
//
|
|
6
|
+
// The registry is process-global; Tier-2 step opt-in via
|
|
7
|
+
// `r.requires.step("…")` (Q9 in step-vocabulary.md) is a future pass.
|
|
8
|
+
|
|
9
|
+
import type { StepDef, StepKind } from "./types/step";
|
|
10
|
+
|
|
11
|
+
const stepRegistry = new Map<StepKind, StepDef>();
|
|
12
|
+
|
|
13
|
+
export function defineStep<TArgs, TResult>(def: StepDef<TArgs, TResult>): StepDef<TArgs, TResult> {
|
|
14
|
+
const existing = stepRegistry.get(def.kind);
|
|
15
|
+
if (existing && existing !== def) {
|
|
16
|
+
throw new Error(`Step kind "${def.kind}" is already registered with a different definition`);
|
|
17
|
+
}
|
|
18
|
+
stepRegistry.set(def.kind, def as StepDef);
|
|
19
|
+
return def;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getStep(kind: StepKind): StepDef | undefined {
|
|
23
|
+
return stepRegistry.get(kind);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function listStepKinds(): readonly StepKind[] {
|
|
27
|
+
return Array.from(stepRegistry.keys());
|
|
28
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// defineWorkflow — define a persistent, suspendable workflow.
|
|
2
|
+
// Tier-3 / Workflow-only mount-point. See step-vocabulary.md Sample 2 for
|
|
3
|
+
// the full lifecycle (run.started → wait → resume → run.completed) and
|
|
4
|
+
// Q7 (Snapshot-at-Start) for the in-flight upgrade story.
|
|
5
|
+
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import type { WriteEvent } from "./types/handlers";
|
|
8
|
+
import type { PipelineDef } from "./types/step";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Trigger configuration for a workflow. Determines what starts a run.
|
|
12
|
+
*/
|
|
13
|
+
export type WorkflowTrigger =
|
|
14
|
+
| {
|
|
15
|
+
readonly kind: "event";
|
|
16
|
+
readonly eventType: string;
|
|
17
|
+
readonly filter?: (event: WriteEvent) => boolean;
|
|
18
|
+
}
|
|
19
|
+
| {
|
|
20
|
+
readonly kind: "cron";
|
|
21
|
+
readonly schedule: string; // cron expression
|
|
22
|
+
}
|
|
23
|
+
| {
|
|
24
|
+
readonly kind: "webhook";
|
|
25
|
+
readonly path: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Workflow definition — the result of defineWorkflow().
|
|
30
|
+
*/
|
|
31
|
+
export type WorkflowDefinition<TPayload = unknown, TData = unknown> = {
|
|
32
|
+
readonly __kind: "workflow";
|
|
33
|
+
readonly name: string;
|
|
34
|
+
readonly trigger: WorkflowTrigger;
|
|
35
|
+
/** The pipeline definition containing the step list closure. */
|
|
36
|
+
readonly pipelineDef: PipelineDef<TPayload, TData>;
|
|
37
|
+
/** Idempotency key for deduplication — prevents duplicate runs. */
|
|
38
|
+
readonly idempotencyKey?: string | ((event: WriteEvent<TPayload>) => string);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Input shape for defineWorkflow() — the user-facing API.
|
|
43
|
+
*/
|
|
44
|
+
export type WorkflowInput<TPayload = unknown, TData = unknown> = {
|
|
45
|
+
readonly name: string;
|
|
46
|
+
readonly trigger: WorkflowTrigger;
|
|
47
|
+
readonly steps: PipelineDef<TPayload, TData>;
|
|
48
|
+
readonly idempotencyKey?: string | ((event: WriteEvent<TPayload>) => string);
|
|
49
|
+
readonly onError?: PipelineDef<unknown>;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Define a suspendable workflow.
|
|
54
|
+
*
|
|
55
|
+
* Example:
|
|
56
|
+
* ```ts
|
|
57
|
+
* defineWorkflow({
|
|
58
|
+
* name: "user-onboarding",
|
|
59
|
+
* trigger: { kind: "event", eventType: "user.signed-up" },
|
|
60
|
+
* steps: pipeline(({ event, r }) => [
|
|
61
|
+
* r.step.mail.send({ to: () => event.payload.email, subject: "Welcome!", body: "..." }),
|
|
62
|
+
* r.step.wait({ for: "P1D" }),
|
|
63
|
+
* r.step.read.findOne("user", { table: userTable, where: ... }),
|
|
64
|
+
* r.step.branch({ if: ({ steps }) => ..., onTrue: [...], onFalse: [...] }),
|
|
65
|
+
* r.step.retry({ times: 3, backoff: "exponential", do: [
|
|
66
|
+
* r.step.webhook.send({ url: "...", mode: "deferred" }),
|
|
67
|
+
* ]}),
|
|
68
|
+
* ]),
|
|
69
|
+
* });
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
export function defineWorkflow<TPayload = unknown, TData = unknown>(
|
|
73
|
+
input: WorkflowInput<TPayload, TData>,
|
|
74
|
+
): WorkflowDefinition<TPayload, TData> {
|
|
75
|
+
return {
|
|
76
|
+
__kind: "workflow",
|
|
77
|
+
name: input.name,
|
|
78
|
+
trigger: input.trigger,
|
|
79
|
+
pipelineDef: input.steps,
|
|
80
|
+
idempotencyKey: input.idempotencyKey,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Q7 Snapshot-at-Start fingerprint. SHA-256 over the workflow's stable
|
|
86
|
+
* identity (name + trigger + serialized pipeline-closure source). Persisted
|
|
87
|
+
* in `workflow.run.started` and re-checked at every resume so a library-
|
|
88
|
+
* upgrade that changes the closure source surfaces as a loud
|
|
89
|
+
* `workflow-definition-changed` failure on in-flight runs instead of a
|
|
90
|
+
* silent semantic drift.
|
|
91
|
+
*
|
|
92
|
+
* Limitations (will be tightened in M.5 with the Designer/AST layer):
|
|
93
|
+
* - `build.toString()` captures the closure source but not bindings —
|
|
94
|
+
* two definitions that import different external helpers with the
|
|
95
|
+
* same source bytes would collide. Acceptable for M.4 because the
|
|
96
|
+
* fingerprint is a *change-detector*, not a deep semantic identity.
|
|
97
|
+
* - Minifiers / source-maps will produce different fingerprints across
|
|
98
|
+
* environments. Run-the-fingerprint-in-the-same-environment is the
|
|
99
|
+
* contract; cross-env replay is out of scope.
|
|
100
|
+
*/
|
|
101
|
+
export function computeDefinitionFingerprint(
|
|
102
|
+
workflow: Pick<WorkflowDefinition, "name" | "trigger" | "pipelineDef">,
|
|
103
|
+
): string {
|
|
104
|
+
const material = JSON.stringify({
|
|
105
|
+
name: workflow.name,
|
|
106
|
+
trigger: workflow.trigger,
|
|
107
|
+
source: workflow.pipelineDef.build.toString(),
|
|
108
|
+
});
|
|
109
|
+
return createHash("sha256").update(material).digest("hex");
|
|
110
|
+
}
|
|
@@ -112,8 +112,8 @@ function parseHandlerName<TVerb extends string>(
|
|
|
112
112
|
if (!entityName) {
|
|
113
113
|
throw new Error(`Handler name "${name}" is missing the entity part before the colon.`);
|
|
114
114
|
}
|
|
115
|
-
// @cast-boundary engine-bridge
|
|
116
|
-
if (!
|
|
115
|
+
const verbs = validVerbs as readonly string[]; // @cast-boundary engine-bridge
|
|
116
|
+
if (!verbs.includes(verbCandidate)) {
|
|
117
117
|
throw new Error(
|
|
118
118
|
`Unknown verb "${verbCandidate}" in handler name "${name}". Standard verbs: ${validVerbs.join("/")}. For custom verbs use the explicit r.writeHandler / r.queryHandler form.`,
|
|
119
119
|
);
|
|
@@ -151,17 +151,17 @@ export function defineEntityWriteHandler(
|
|
|
151
151
|
changes: buildUpdateSchema(entity),
|
|
152
152
|
});
|
|
153
153
|
handler = async (event, ctx) =>
|
|
154
|
-
executor.update(event.payload as UpdatePayload, event.user, ctx.db);
|
|
154
|
+
executor.update(event.payload as UpdatePayload, event.user, ctx.db); // @cast-boundary engine-payload
|
|
155
155
|
break;
|
|
156
156
|
case "delete":
|
|
157
157
|
schema = idSchema;
|
|
158
158
|
handler = async (event, ctx) =>
|
|
159
|
-
executor.delete(event.payload as IdPayload, event.user, ctx.db);
|
|
159
|
+
executor.delete(event.payload as IdPayload, event.user, ctx.db); // @cast-boundary engine-payload
|
|
160
160
|
break;
|
|
161
161
|
case "restore":
|
|
162
162
|
schema = idSchema;
|
|
163
163
|
handler = async (event, ctx) =>
|
|
164
|
-
executor.restore(event.payload as IdPayload, event.user, ctx.db);
|
|
164
|
+
executor.restore(event.payload as IdPayload, event.user, ctx.db); // @cast-boundary engine-payload
|
|
165
165
|
break;
|
|
166
166
|
default:
|
|
167
167
|
assertUnreachable(verb, "write verb");
|
|
@@ -205,7 +205,8 @@ export function defineEntityQueryHandler(
|
|
|
205
205
|
// läuft (Remote-Combobox-Search). Der executor wird beim
|
|
206
206
|
// Definition-Time gebaut, kennt den Adapter also nicht —
|
|
207
207
|
// Runtime-Override holt das.
|
|
208
|
-
const
|
|
208
|
+
const listPayload = query.payload as ListPayload; // @cast-boundary engine-payload
|
|
209
|
+
const result = await executor.list(listPayload, query.user, ctx.db, {
|
|
209
210
|
...(ctx.searchAdapter !== undefined && { searchAdapter: ctx.searchAdapter }),
|
|
210
211
|
});
|
|
211
212
|
if (!hasRefFields) return result;
|
|
@@ -221,7 +222,7 @@ export function defineEntityQueryHandler(
|
|
|
221
222
|
case "detail":
|
|
222
223
|
schema = idSchema;
|
|
223
224
|
handler = async (query, ctx) => {
|
|
224
|
-
const row = await executor.detail(query.payload as IdPayload, query.user, ctx.db);
|
|
225
|
+
const row = await executor.detail(query.payload as IdPayload, query.user, ctx.db); // @cast-boundary engine-payload
|
|
225
226
|
if (row === null || !hasRefFields) return row;
|
|
226
227
|
return enrichRowWithReferences(row, entity, (name) => ctx.registry.getEntity(name), ctx.db);
|
|
227
228
|
};
|
|
@@ -345,7 +346,7 @@ export function createEntityExecutor(
|
|
|
345
346
|
export function defineProjectionQueryHandler(
|
|
346
347
|
name: string,
|
|
347
348
|
projectionQualifiedName: string,
|
|
348
|
-
options?: { access?: AccessRule;
|
|
349
|
+
options?: { access?: AccessRule; unsafeAllTenants?: boolean },
|
|
349
350
|
): QueryHandlerDef {
|
|
350
351
|
return {
|
|
351
352
|
name,
|
|
@@ -357,7 +358,7 @@ export function defineProjectionQueryHandler(
|
|
|
357
358
|
handler: async (_query, ctx) =>
|
|
358
359
|
ctx.queryProjection(
|
|
359
360
|
projectionQualifiedName,
|
|
360
|
-
options?.
|
|
361
|
+
options?.unsafeAllTenants ? { unsafeAllTenants: true } : undefined,
|
|
361
362
|
),
|
|
362
363
|
...(options?.access && { access: options.access }),
|
|
363
364
|
};
|
|
@@ -4,11 +4,11 @@ import type { AppendEventArgs, EventDef, HandlerContext } from "./types/handlers
|
|
|
4
4
|
// MultiStreamApplyContext-style callers pass their own appendEvent without
|
|
5
5
|
// a full HandlerContext. Real handlers just pass `ctx`.
|
|
6
6
|
//
|
|
7
|
-
// Uses
|
|
7
|
+
// Uses unsafeAppendEvent internally because EventDef's TPayload comes from
|
|
8
8
|
// a runtime-defined zod-schema — emitEvent does the type-check itself via
|
|
9
9
|
// the EventDef generic, so it doesn't need the strict KumikoEventTypeMap
|
|
10
10
|
// path. The strict appendEvent is for direct in-handler callsites.
|
|
11
|
-
export type EmitCtx = Pick<HandlerContext, "
|
|
11
|
+
export type EmitCtx = Pick<HandlerContext, "unsafeAppendEvent">;
|
|
12
12
|
|
|
13
13
|
// Typed wrapper around ctx.appendEvent. Two wins over the raw call:
|
|
14
14
|
//
|
|
@@ -42,7 +42,7 @@ export async function emitEvent<TPayload>(
|
|
|
42
42
|
type: eventDef.name,
|
|
43
43
|
payload: args.payload,
|
|
44
44
|
};
|
|
45
|
-
await ctx.
|
|
45
|
+
await ctx.unsafeAppendEvent(appendArgs);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// Read-side counterpart: narrow a StoredEvent's `payload` (declared as
|
|
@@ -69,5 +69,5 @@ export function typedPayload<TPayload>(
|
|
|
69
69
|
`Check the projection-apply / reducer mapping — the event was routed to the wrong handler.`,
|
|
70
70
|
);
|
|
71
71
|
}
|
|
72
|
-
return event.payload as TPayload;
|
|
72
|
+
return event.payload as TPayload; // @cast-boundary engine-payload
|
|
73
73
|
}
|