@cosmicdrift/kumiko-framework 0.306.0 → 0.308.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/package.json +4 -4
- package/src/__tests__/anonymous-access.integration.test.ts +1 -1
- package/src/__tests__/error-contract.integration.test.ts +1 -1
- package/src/__tests__/field-access.integration.test.ts +1 -1
- package/src/__tests__/fixtures/migrate-open-to-all/manual-only.expected.ts +1 -1
- package/src/__tests__/fixtures/migrate-open-to-all/manual-only.input.ts +1 -1
- package/src/__tests__/fixtures/migrate-open-to-all/test-simple.expected.ts +1 -1
- package/src/__tests__/fixtures/migrate-open-to-all/test-simple.input.ts +1 -1
- package/src/__tests__/full-stack.integration.test.ts +1 -1
- package/src/__tests__/transition-guard.integration.test.ts +1 -1
- package/src/api/__tests__/api.test.ts +1 -1
- package/src/api/__tests__/batch.integration.test.ts +1 -1
- package/src/api/__tests__/body-limit.test.ts +1 -1
- package/src/api/__tests__/dispatcher-live.integration.test.ts +1 -1
- package/src/api/__tests__/extra-routes.integration.test.ts +1 -1
- package/src/api/__tests__/http-route-rate-limit.integration.test.ts +1 -1
- package/src/api/__tests__/nested-write-ownership.integration.test.ts +1 -1
- package/src/api/__tests__/nested-write.integration.test.ts +1 -1
- package/src/api/__tests__/pii-leak-guard.integration.test.ts +1 -1
- package/src/api/__tests__/redis-sse-broker.integration.test.ts +133 -0
- package/src/api/__tests__/request-locale.integration.test.ts +1 -1
- package/src/api/__tests__/server-boot-guards.test.ts +1 -1
- package/src/api/__tests__/server-error-logging.test.ts +1 -1
- package/src/api/__tests__/sse-broker.test.ts +144 -1
- package/src/api/__tests__/stream-escape-hatch.integration.test.ts +1 -1
- package/src/api/auth-routes.ts +1 -1
- package/src/api/redis-sse-broker.ts +57 -3
- package/src/api/request-context.ts +24 -0
- package/src/api/server.ts +11 -0
- package/src/api/sse-broker.ts +69 -12
- package/src/changes.json +63 -0
- package/src/compliance/override-schema.ts +1 -1
- package/src/crypto/__tests__/event-pii.test.ts +1 -1
- package/src/crypto/kms-adapter.ts +1 -1
- package/src/db/__tests__/tenant-db-where-merge.integration.test.ts +1 -1
- package/src/db/queries/event-consumer.ts +155 -9
- package/src/db/queries/event-store.ts +69 -0
- package/src/db/tenant-db.ts +43 -5
- package/src/derivatives/__tests__/derivatives-context.integration.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-action-wiring.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-dashboard.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-nav-area.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-projection-list.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-query-output-schema.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-query-refs.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-ref-entity.test.ts +1 -1
- package/src/engine/__tests__/boot-validator-s0-integration.test.ts +1 -1
- package/src/engine/__tests__/boot-validator.test.ts +35 -1
- package/src/engine/__tests__/build-app-schema.test.ts +1 -1
- package/src/engine/__tests__/build-config-feature-schema-secrets.test.ts +1 -1
- package/src/engine/__tests__/define-feature-entity-mapping.test.ts +1 -1
- package/src/engine/__tests__/engine.test.ts +1 -1
- package/src/engine/__tests__/event-helpers.test.ts +1 -1
- package/src/engine/__tests__/event-migration-declarative.test.ts +1 -1
- package/src/engine/__tests__/extend-entity-projection.test.ts +1 -1
- package/src/engine/__tests__/extends-registrar.test.ts +1 -1
- package/src/engine/__tests__/hook-phases.test.ts +1 -1
- package/src/engine/__tests__/lifecycle-hooks.test.ts +1 -1
- package/src/engine/__tests__/pipeline-engine.test.ts +1 -1
- package/src/engine/__tests__/pipeline-handler.integration.test.ts +1 -1
- package/src/engine/__tests__/pipeline-observability.integration.test.ts +1 -1
- package/src/engine/__tests__/pipeline-performance.integration.test.ts +1 -1
- package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +1 -1
- package/src/engine/__tests__/post-query-hook.test.ts +1 -1
- package/src/engine/__tests__/projection-detail-actions.test.ts +1 -1
- package/src/engine/__tests__/projection-detail-singleton.test.ts +1 -1
- package/src/engine/__tests__/projection-detail-tabs.test.ts +1 -1
- package/src/engine/__tests__/read-steps-tenant-filter.integration.test.ts +1 -1
- package/src/engine/__tests__/registry.test.ts +1 -1
- package/src/engine/__tests__/screen.test.ts +1 -1
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +1 -1
- package/src/engine/boot-validator/__tests__/access-declarations.test.ts +1 -1
- package/src/engine/boot-validator/__tests__/action-icon.test.ts +81 -0
- package/src/engine/boot-validator/__tests__/anonymous-rate-limit-required.test.ts +1 -1
- package/src/engine/boot-validator/__tests__/no-all-role-in-handler-access.test.ts +1 -1
- package/src/engine/boot-validator/__tests__/parent-ref.test.ts +1 -1
- package/src/engine/boot-validator/__tests__/rate-limit-disabled-reason.test.ts +1 -1
- package/src/engine/boot-validator/screens.ts +83 -0
- package/src/engine/define-handler.ts +2 -1
- package/src/engine/entity-handlers.ts +2 -1
- package/src/engine/feature-ast/__tests__/pattern-change-schema.test.ts +1 -1
- package/src/engine/feature-ast/pattern-change-schema.ts +1 -1
- package/src/engine/feature-builder-state.ts +2 -1
- package/src/engine/feature-config-events-jobs.ts +2 -1
- package/src/engine/feature-entity-handlers.ts +2 -1
- package/src/engine/schema-builder.ts +1 -1
- package/src/entrypoint/__tests__/entrypoint-attach-dispatcher.integration.test.ts +1 -1
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.test.ts +1 -1
- package/src/entrypoint/__tests__/split-deploy.integration.test.ts +1 -1
- package/src/env/__tests__/compose-env-schema.test.ts +1 -1
- package/src/env/__tests__/dry-run.test.ts +1 -1
- package/src/env/__tests__/parse-env-dry-run.test.ts +1 -1
- package/src/env/_zod-introspect.ts +1 -1
- package/src/env/dry-run.ts +1 -1
- package/src/env/index.ts +1 -1
- package/src/errors/__tests__/classes.test.ts +1 -1
- package/src/es-ops/__tests__/runner.integration.test.ts +1 -1
- package/src/event-store/__tests__/backfill-pii.integration.test.ts +1 -1
- package/src/event-store/__tests__/event-attribution.integration.test.ts +54 -4
- package/src/event-store/__tests__/upcaster.integration.test.ts +1 -1
- package/src/event-store/admin-api.ts +5 -0
- package/src/event-store/event-store.ts +16 -7
- package/src/jobs/__tests__/job-backoff.integration.test.ts +1 -1
- package/src/jobs/__tests__/job-event-trigger.integration.test.ts +1 -1
- package/src/jobs/__tests__/job-multi-trigger.integration.test.ts +1 -1
- package/src/jobs/__tests__/job-public-intake-origin.integration.test.ts +536 -0
- package/src/jobs/__tests__/job-retention.integration.test.ts +1 -1
- package/src/jobs/__tests__/job-write-as.integration.test.ts +1 -1
- package/src/jobs/__tests__/jobs.integration.test.ts +1 -1
- package/src/jobs/job-runner.ts +61 -6
- package/src/observability/__tests__/metrics-wiring.test.ts +1 -1
- package/src/observability/__tests__/observability.integration.test.ts +1 -1
- package/src/observability/metrics-wiring.ts +1 -1
- package/src/pipeline/__tests__/archive-stream.integration.test.ts +1 -1
- package/src/pipeline/__tests__/causation-chain.integration.test.ts +1 -1
- package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +1 -1
- package/src/pipeline/__tests__/ctx-systemdb.integration.test.ts +1 -1
- package/src/pipeline/__tests__/dispatcher-utils.test.ts +8 -0
- package/src/pipeline/__tests__/dispatcher.test.ts +1 -1
- package/src/pipeline/__tests__/domain-events-projections.integration.test.ts +1 -1
- package/src/pipeline/__tests__/escape-hatch-audit.integration.test.ts +1 -1
- package/src/pipeline/__tests__/escape-hatch.integration.test.ts +1 -1
- package/src/pipeline/__tests__/event-dedup.integration.test.ts +1 -1
- package/src/pipeline/__tests__/event-define-event-strict.integration.test.ts +1 -1
- package/src/pipeline/__tests__/event-dispatcher-batch-delivery.test.ts +178 -0
- package/src/pipeline/__tests__/event-dispatcher-batch-handler.integration.test.ts +143 -0
- package/src/pipeline/__tests__/event-dispatcher-commit-order.integration.test.ts +278 -0
- package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +1 -0
- package/src/pipeline/__tests__/event-dispatcher-idle.integration.test.ts +94 -0
- package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.test.ts +6 -6
- package/src/pipeline/__tests__/event-dispatcher-per-consumer-turns.integration.test.ts +126 -0
- package/src/pipeline/__tests__/event-dispatcher-start-from.integration.test.ts +103 -3
- package/src/pipeline/__tests__/fetch-for-writing.integration.test.ts +1 -1
- package/src/pipeline/__tests__/idempotency-transient-failure.integration.test.ts +1 -1
- package/src/pipeline/__tests__/job-trigger-consumer.integration.test.ts +1 -1
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +1 -1
- package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +1 -1
- package/src/pipeline/__tests__/msp-multi-hop.integration.test.ts +1 -1
- package/src/pipeline/__tests__/msp-rebuild.integration.test.ts +116 -1
- package/src/pipeline/__tests__/multi-stream-projection.integration.test.ts +1 -1
- package/src/pipeline/__tests__/pending-gap-ranges.test.ts +94 -0
- package/src/pipeline/__tests__/post-query-hook.integration.test.ts +1 -1
- package/src/pipeline/__tests__/public-intake-runtime-gate.integration.test.ts +262 -3
- package/src/pipeline/__tests__/query-projection.integration.test.ts +1 -1
- package/src/pipeline/__tests__/rate-limit-default.integration.test.ts +1 -1
- package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +1 -1
- package/src/pipeline/__tests__/search-consumer-named-events.integration.test.ts +1 -1
- package/src/pipeline/__tests__/system-identity-switch.integration.test.ts +1 -1
- package/src/pipeline/__tests__/try-append-event.integration.test.ts +1 -1
- package/src/pipeline/__tests__/unsafe-raw-escape-hatch.integration.test.ts +1 -1
- package/src/pipeline/dispatch-batch.ts +49 -19
- package/src/pipeline/dispatch-stream.ts +12 -3
- package/src/pipeline/dispatcher-utils.ts +21 -2
- package/src/pipeline/dispatcher.ts +71 -6
- package/src/pipeline/event-consumer-state.ts +26 -0
- package/src/pipeline/event-dispatcher-admin.ts +32 -5
- package/src/pipeline/event-dispatcher-delivery.ts +212 -65
- package/src/pipeline/event-dispatcher.ts +253 -49
- package/src/pipeline/index.ts +1 -0
- package/src/pipeline/msp-rebuild.ts +39 -1
- package/src/pipeline/pending-gap-ranges.ts +120 -0
- package/src/pipeline/system-hooks.ts +226 -64
- package/src/pipeline/write-origin.ts +31 -10
- package/src/rate-limit/__tests__/dispatcher-l3.integration.test.ts +1 -1
- package/src/search/__tests__/derive-search-adapter-config.test.ts +65 -0
- package/src/search/__tests__/meilisearch-adapter.integration.test.ts +91 -0
- package/src/search/__tests__/search-consumer-batch.integration.test.ts +306 -0
- package/src/search/derive-search-adapter-config.ts +27 -0
- package/src/search/index.ts +1 -0
- package/src/search/meilisearch-adapter.ts +96 -14
- package/src/secrets/__tests__/contains-secret.test.ts +1 -1
- package/src/stack/__tests__/request-helper.integration.test.ts +1 -1
- package/src/stack/__tests__/setup-test-stack-jobs.integration.test.ts +1 -1
- package/src/stack/__tests__/setup-test-stack-metrics.integration.test.ts +1 -1
- package/src/stack/test-stack.ts +1 -1
- package/src/testing/__tests__/e2e-generator.test.ts +1 -1
- package/src/testing/e2e-generator.ts +1 -1
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
// into validateColumnRendererForm, splitting them would create a
|
|
5
5
|
// same-folder require cycle.
|
|
6
6
|
|
|
7
|
+
import { resolveActionIcon } from "@cosmicdrift/kumiko-types/action-icon";
|
|
7
8
|
import { NO_WIDGET_FIELD_TYPES } from "@cosmicdrift/kumiko-types/fields";
|
|
9
|
+
import type { IconKey } from "@cosmicdrift/kumiko-types/nav-icon";
|
|
10
|
+
import { NAV_ICON_KEYS } from "@cosmicdrift/kumiko-types/nav-icon";
|
|
8
11
|
import { rowMetaFieldNames } from "../../db/table-builder";
|
|
9
12
|
import { LIST_ROW_META_COLUMNS } from "../../ui-types/list-row-meta";
|
|
10
13
|
import { parseRefTarget } from "../parse-ref-target";
|
|
@@ -319,6 +322,41 @@ function validateWizardLayout(
|
|
|
319
322
|
// are NOT validated here — the r.uiComponent registry that would resolve
|
|
320
323
|
// them ships in M4/M5. Until then those are kept opaque on purpose.
|
|
321
324
|
|
|
325
|
+
// every action rendered in a Card title row (screen-level
|
|
326
|
+
// `actions`, section-level `actions`, `emptyState.action`) must draw an
|
|
327
|
+
// icon — declared or id-derived (resolveActionIcon, same resolver the
|
|
328
|
+
// renderer uses). Uncovered here: entityList/projectionList rowActions —
|
|
329
|
+
// those render inside a DataTable row, not a Card title row, so they keep
|
|
330
|
+
// today's "icon optional" behavior.
|
|
331
|
+
//
|
|
332
|
+
// Registration check (not just "resolved is not undefined"): action.icon is
|
|
333
|
+
// typed IconKey, a closed union — but that only protects call sites this
|
|
334
|
+
// repo's own `tsc --build` actually covers (samples and external consumers
|
|
335
|
+
// aren't in that project graph). The renderer's actionIconFor
|
|
336
|
+
// (renderer-web/src/primitives/index.tsx) already falls back to "no icon"
|
|
337
|
+
// for an unregistered key instead of crashing — this check makes the same
|
|
338
|
+
// gap fail loudly at boot instead of silently at render.
|
|
339
|
+
function isRegisteredIcon(icon: IconKey): boolean {
|
|
340
|
+
return (NAV_ICON_KEYS as readonly string[]).includes(icon);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export function validateActionHasIcon(
|
|
344
|
+
featureName: string,
|
|
345
|
+
screenId: string,
|
|
346
|
+
screenType: "projectionDetail" | "entityEdit",
|
|
347
|
+
context: string,
|
|
348
|
+
action: { readonly id: string; readonly icon?: IconKey },
|
|
349
|
+
): void {
|
|
350
|
+
const resolved = resolveActionIcon(action.id, action.icon);
|
|
351
|
+
if (resolved === undefined || !isRegisteredIcon(resolved)) {
|
|
352
|
+
throw new Error(
|
|
353
|
+
`[Feature ${featureName}] Screen "${screenId}" (${screenType}) ${context} "${action.id}" has no ` +
|
|
354
|
+
`resolvable icon — set icon to a key registered in NAV_ICONS, or use an id that resolves one of ` +
|
|
355
|
+
`its kebab segments via the default icon map (resolveActionIcon).`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
322
360
|
// Tier 2.7e-3: deklarative Feld-Referenzen einer Action gegen die Entity-
|
|
323
361
|
// Felder pinnen — ein Tippfehler in pick/map-Quellfeldern oder
|
|
324
362
|
// visible.field erzeugte sonst still `undefined` im Payload bzw. dauerhaft
|
|
@@ -1137,6 +1175,17 @@ export function validateScreens(
|
|
|
1137
1175
|
}
|
|
1138
1176
|
if (screen.layout.mode === "tabs") {
|
|
1139
1177
|
validateTabSections(feature.name, screenId, "projectionDetail", screen.layout.sections);
|
|
1178
|
+
for (const section of screen.layout.sections) {
|
|
1179
|
+
const isFieldsSection = section.kind === undefined || section.kind === "fields";
|
|
1180
|
+
if (isFieldsSection && section.groups !== undefined && section.actions !== undefined) {
|
|
1181
|
+
throw new Error(
|
|
1182
|
+
`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) section "${section.id}" ` +
|
|
1183
|
+
`declares both groups and actions in tabs mode — actions has no single group's Card to sit ` +
|
|
1184
|
+
`in, and an extra title-less Card around the group cards would nest a Card inside a Card. ` +
|
|
1185
|
+
`Move the actions onto one group's title, or drop groups for a flat fields grid.`,
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1140
1189
|
}
|
|
1141
1190
|
if (screen.metrics !== undefined) {
|
|
1142
1191
|
for (const metric of screen.metrics) {
|
|
@@ -1166,6 +1215,27 @@ export function validateScreens(
|
|
|
1166
1215
|
}
|
|
1167
1216
|
}
|
|
1168
1217
|
for (const section of screen.layout.sections) {
|
|
1218
|
+
const sectionLabel = section.title ?? section.id ?? "(untitled)";
|
|
1219
|
+
if (section.actions !== undefined) {
|
|
1220
|
+
for (const action of section.actions) {
|
|
1221
|
+
validateActionHasIcon(
|
|
1222
|
+
feature.name,
|
|
1223
|
+
screenId,
|
|
1224
|
+
"projectionDetail",
|
|
1225
|
+
`section "${sectionLabel}" action`,
|
|
1226
|
+
action,
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
if (section.kind === "relatedList" && section.emptyState?.action !== undefined) {
|
|
1231
|
+
validateActionHasIcon(
|
|
1232
|
+
feature.name,
|
|
1233
|
+
screenId,
|
|
1234
|
+
"projectionDetail",
|
|
1235
|
+
`section "${sectionLabel}" emptyState action`,
|
|
1236
|
+
section.emptyState.action,
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1169
1239
|
if (isExtensionEditSection(section)) {
|
|
1170
1240
|
// projectionDetail is read-only (no composed form submit) — an
|
|
1171
1241
|
// extension that persists through the host's Save button has
|
|
@@ -1402,6 +1472,7 @@ export function validateScreens(
|
|
|
1402
1472
|
// a detail screen has no row to click.
|
|
1403
1473
|
if (screen.actions !== undefined) {
|
|
1404
1474
|
for (const action of screen.actions) {
|
|
1475
|
+
validateActionHasIcon(feature.name, screenId, "projectionDetail", "action", action);
|
|
1405
1476
|
if (action.kind === "navigate" && action.rowClick === true) {
|
|
1406
1477
|
throw new Error(
|
|
1407
1478
|
`[Feature ${feature.name}] Screen "${qualifyEntityName(feature.name, "screen", screenId)}" ` +
|
|
@@ -1902,6 +1973,18 @@ export function validateScreens(
|
|
|
1902
1973
|
);
|
|
1903
1974
|
}
|
|
1904
1975
|
for (const section of screen.layout.sections) {
|
|
1976
|
+
if (section.actions !== undefined) {
|
|
1977
|
+
const sectionLabel = section.title ?? section.id ?? "(untitled)";
|
|
1978
|
+
for (const action of section.actions) {
|
|
1979
|
+
validateActionHasIcon(
|
|
1980
|
+
feature.name,
|
|
1981
|
+
screenId,
|
|
1982
|
+
"entityEdit",
|
|
1983
|
+
`section "${sectionLabel}" action`,
|
|
1984
|
+
action,
|
|
1985
|
+
);
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1905
1988
|
if (isExtensionEditSection(section)) {
|
|
1906
1989
|
if (section.component?.react === undefined && section.component?.native === undefined) {
|
|
1907
1990
|
throw new Error(
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CursorResult } from "@cosmicdrift/kumiko-types/cursor-types";
|
|
2
2
|
import { ENTITY_CONVENTION_QUERY_BRAND } from "@cosmicdrift/kumiko-types/handlers";
|
|
3
|
-
import type
|
|
3
|
+
import type * as z from "zod";
|
|
4
|
+
import type { ZodType } from "zod";
|
|
4
5
|
import type { ContainsSecret } from "../secrets/types";
|
|
5
6
|
import { runPipeline } from "./run-pipeline";
|
|
6
7
|
import type { HandlerContext, KumikoEventTypeMap, WriteEvent, WriteResult } from "./types";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ENTITY_CONVENTION_QUERY_BRAND } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
|
-
import {
|
|
2
|
+
import type { ZodType } from "zod";
|
|
3
|
+
import * as z from "zod";
|
|
3
4
|
import type { DbRow } from "../db/connection";
|
|
4
5
|
import {
|
|
5
6
|
collectReferenceFields,
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { describe, expect, test } from "bun:test";
|
|
7
7
|
import { resolve } from "node:path";
|
|
8
8
|
import { Project, type SourceFile } from "ts-morph";
|
|
9
|
-
import type
|
|
9
|
+
import type * as z from "zod";
|
|
10
10
|
import { parseFeatureFile, parseSourceFile } from "../parse";
|
|
11
11
|
import { applyChanges, type PatternId } from "../patch";
|
|
12
12
|
import {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// additionally checked for field-type membership via the shared
|
|
23
23
|
// `findUnknownEntityFieldTypes` helper (kept in sync with `extractEntity`).
|
|
24
24
|
|
|
25
|
-
import
|
|
25
|
+
import * as z from "zod";
|
|
26
26
|
import { type LifecycleHookType, LifecycleHookTypes } from "../constants";
|
|
27
27
|
import type {
|
|
28
28
|
ConfigKeyDefinition,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalizeEventPiiSubject } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
|
-
import
|
|
2
|
+
import type * as z from "zod";
|
|
3
|
+
import { ZodObject, type ZodType } from "zod";
|
|
3
4
|
import type { FeatureBuilderState } from "./feature-builder-state";
|
|
4
5
|
import { resolveName } from "./handler-helpers";
|
|
5
6
|
import { splitNamedDefinition, unwrapArrayForm } from "./object-form";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ENTITY_CONVENTION_QUERY_BRAND } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
|
-
import type
|
|
2
|
+
import type * as z from "zod";
|
|
3
|
+
import type { ZodType } from "zod";
|
|
3
4
|
import { toTableName } from "../db/table-builder";
|
|
4
5
|
import {
|
|
5
6
|
isEntityConventionQueryHandler,
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// throws the #2043 stub).
|
|
11
11
|
|
|
12
12
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
13
|
-
import
|
|
13
|
+
import * as z from "zod";
|
|
14
14
|
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
15
15
|
import { asRawClient } from "../../db/query";
|
|
16
16
|
import { createRegistry, defineFeature } from "../../engine";
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// regressing to the Welle-2.5 state.
|
|
14
14
|
|
|
15
15
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
16
|
-
import
|
|
16
|
+
import * as z from "zod";
|
|
17
17
|
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
18
18
|
import { createRegistry, defineFeature } from "../../engine";
|
|
19
19
|
import { createArchivedStreamsTable } from "../../event-store";
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// fires with systemConsumers explicitly disabled).
|
|
14
14
|
|
|
15
15
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
16
|
-
import
|
|
16
|
+
import * as z from "zod";
|
|
17
17
|
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
18
18
|
import { asRawClient } from "../../db/query";
|
|
19
19
|
import { createRegistry, defineFeature } from "../../engine";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import
|
|
2
|
+
import * as z from "zod";
|
|
3
3
|
import { defineFeature } from "../../engine/define-feature";
|
|
4
4
|
import { prometheusMetricsEnvSchema } from "../../observability/metrics-wiring";
|
|
5
5
|
import { renderDryRun } from "../dry-run";
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// All helpers accept the *runtime* Zod instance — TypeScript wrapper
|
|
7
7
|
// (`z.ZodType`) and core (`$ZodType`) are the same object at runtime.
|
|
8
8
|
|
|
9
|
-
import type
|
|
9
|
+
import type * as z from "zod";
|
|
10
10
|
|
|
11
11
|
export type ZodDef = {
|
|
12
12
|
readonly innerType?: z.ZodType;
|
package/src/env/dry-run.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// The same schema introspection (`classifyField`, `readKumikoMeta`) used
|
|
9
9
|
// by parseEnv drives the output here — single source of truth.
|
|
10
10
|
|
|
11
|
-
import type
|
|
11
|
+
import type * as z from "zod";
|
|
12
12
|
import { zodShape } from "./_zod-introspect";
|
|
13
13
|
import {
|
|
14
14
|
type ComposedEnvSchema,
|
package/src/env/index.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// Without meta, defaults: `camelCase(envVarName)` for the Pulumi key,
|
|
15
15
|
// `<set-me>` placeholder for the value, no `--secret` flag.
|
|
16
16
|
|
|
17
|
-
import
|
|
17
|
+
import * as z from "zod";
|
|
18
18
|
import type { FeatureDefinition } from "../engine/types/feature";
|
|
19
19
|
import { zodDef, zodDescription, zodMeta, zodShape, zodShapeField } from "./_zod-introspect";
|
|
20
20
|
|
|
@@ -17,7 +17,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:tes
|
|
|
17
17
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
18
18
|
import { tmpdir } from "node:os";
|
|
19
19
|
import { join } from "node:path";
|
|
20
|
-
import
|
|
20
|
+
import * as z from "zod";
|
|
21
21
|
import { createEventStoreExecutor } from "../../db/event-store-executor";
|
|
22
22
|
import { asRawClient, insertOne, selectMany } from "../../db/query";
|
|
23
23
|
import { buildEntityTable } from "../../db/table-builder";
|
|
@@ -14,12 +14,13 @@
|
|
|
14
14
|
// and stay readable without the new fields.
|
|
15
15
|
|
|
16
16
|
import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
|
|
17
|
-
import { UNATTRIBUTED_ORIGIN } from "@cosmicdrift/kumiko-types/event-store-types";
|
|
18
|
-
import
|
|
17
|
+
import { UNATTRIBUTED_ORIGIN, type WriteOrigin } from "@cosmicdrift/kumiko-types/event-store-types";
|
|
18
|
+
import * as z from "zod";
|
|
19
19
|
import { createEventStoreExecutor } from "../../db/event-store-executor";
|
|
20
20
|
import { selectMany } from "../../db/query";
|
|
21
21
|
import { buildEntityTable } from "../../db/table-builder";
|
|
22
22
|
import { createEntity, createTextField, defineFeature } from "../../engine";
|
|
23
|
+
import type { TenantId } from "../../engine/types";
|
|
23
24
|
import {
|
|
24
25
|
resetEventStore,
|
|
25
26
|
setupTestStack,
|
|
@@ -43,8 +44,11 @@ const orderTable = buildEntityTable("attr-order", orderEntity);
|
|
|
43
44
|
|
|
44
45
|
const PLACED = "attribution:event:placed";
|
|
45
46
|
const CONFIRMED = "attribution:event:confirmed";
|
|
47
|
+
const PROBED = "attribution:event:probed";
|
|
46
48
|
const PLACE_HANDLER = "attribution:write:order:place";
|
|
49
|
+
const PROBE_HANDLER = "attribution:write:order:probe";
|
|
47
50
|
const CONFIRMER_MSP = "attribution:projection:confirmer";
|
|
51
|
+
const TENANT_ID = "00000000-0000-4000-8000-000000000002" as TenantId;
|
|
48
52
|
|
|
49
53
|
const attributionFeature = defineFeature("attribution", (r) => {
|
|
50
54
|
r.entity("attr-order", orderEntity);
|
|
@@ -75,6 +79,26 @@ const attributionFeature = defineFeature("attribution", (r) => {
|
|
|
75
79
|
{ access: { roles: ["Admin"] } },
|
|
76
80
|
);
|
|
77
81
|
|
|
82
|
+
const probed = r.defineEvent("probed", z.object({ orderId: z.uuid() }), { piiFields: "none" });
|
|
83
|
+
|
|
84
|
+
// Anonymous, no public-intake declared, no PII field written (nothing to gate).
|
|
85
|
+
r.writeHandler(
|
|
86
|
+
"order:probe",
|
|
87
|
+
z.object({ item: z.string() }),
|
|
88
|
+
async (event, ctx) => {
|
|
89
|
+
const created = await orderExecutor.create({ item: event.payload.item }, event.user, ctx.db);
|
|
90
|
+
if (!created.isSuccess) return created;
|
|
91
|
+
await ctx.unsafeAppendEvent({
|
|
92
|
+
aggregateId: String(created.data.id),
|
|
93
|
+
aggregateType: "attr-order",
|
|
94
|
+
type: probed.name,
|
|
95
|
+
payload: { orderId: String(created.data.id) },
|
|
96
|
+
});
|
|
97
|
+
return created;
|
|
98
|
+
},
|
|
99
|
+
{ access: { roles: ["anonymous"] } },
|
|
100
|
+
);
|
|
101
|
+
|
|
78
102
|
r.multiStreamProjection({
|
|
79
103
|
name: "confirmer",
|
|
80
104
|
apply: {
|
|
@@ -95,7 +119,11 @@ let stack: TestStack;
|
|
|
95
119
|
const admin = TestUsers.admin;
|
|
96
120
|
|
|
97
121
|
beforeAll(async () => {
|
|
98
|
-
stack = await setupTestStack({
|
|
122
|
+
stack = await setupTestStack({
|
|
123
|
+
features: [attributionFeature],
|
|
124
|
+
systemHooks: [],
|
|
125
|
+
anonymousAccess: { defaultTenantId: TENANT_ID },
|
|
126
|
+
});
|
|
99
127
|
await unsafeCreateEntityTable(stack.db, orderEntity, "attr-order");
|
|
100
128
|
});
|
|
101
129
|
|
|
@@ -107,7 +135,7 @@ afterEach(async () => {
|
|
|
107
135
|
await resetEventStore(stack, ["read_attribution_orders"]);
|
|
108
136
|
});
|
|
109
137
|
|
|
110
|
-
type Origin = { feature?: string; handler?: string };
|
|
138
|
+
type Origin = { feature?: string; handler?: string; writeOrigin?: WriteOrigin };
|
|
111
139
|
|
|
112
140
|
async function originOf(type: string): Promise<Origin> {
|
|
113
141
|
const rows = await selectMany(stack.db, eventsTable);
|
|
@@ -165,6 +193,28 @@ describe("#3043 — event attribution from the execution scope", () => {
|
|
|
165
193
|
});
|
|
166
194
|
});
|
|
167
195
|
|
|
196
|
+
test("authenticated write: the event carries no writeOrigin at all", async () => {
|
|
197
|
+
await stack.http.writeOk(PLACE_HANDLER, { item: "cog" }, admin);
|
|
198
|
+
|
|
199
|
+
const origin = await originOf(PLACED);
|
|
200
|
+
expect(origin.writeOrigin).toBeUndefined();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("anonymous non-intake write: the event is stamped with the gated origin", async () => {
|
|
204
|
+
const res = await stack.http.raw("POST", "/api/write", {
|
|
205
|
+
type: PROBE_HANDLER,
|
|
206
|
+
payload: { item: "washer" },
|
|
207
|
+
});
|
|
208
|
+
expect(res.status).toBe(200);
|
|
209
|
+
|
|
210
|
+
const origin = await originOf(PROBED);
|
|
211
|
+
expect(origin.writeOrigin).toMatchObject({
|
|
212
|
+
rootHandler: PROBE_HANDLER,
|
|
213
|
+
anonymousRoot: true,
|
|
214
|
+
publicIntake: false,
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
168
218
|
test("appendRaw keeps historical metadata verbatim and stays readable", async () => {
|
|
169
219
|
const aggregateId = uuid();
|
|
170
220
|
await appendRaw(stack.db, {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// can never silently hand half-migrated data to consumers.
|
|
10
10
|
|
|
11
11
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
12
|
-
import
|
|
12
|
+
import * as z from "zod";
|
|
13
13
|
import { integer as pgInteger, table as pgTable, text as pgText } from "../../db/dialect";
|
|
14
14
|
import { createEventStoreExecutor } from "../../db/event-store-executor";
|
|
15
15
|
import { asRawClient, insertOne, selectMany } from "../../db/query";
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type { DbRunner } from "../db";
|
|
11
11
|
import { constraintOf, isUniqueViolation } from "../db/pg-error";
|
|
12
|
+
import { claimXactId } from "../db/queries/event-store";
|
|
12
13
|
import {
|
|
13
14
|
eventPredecessorExists,
|
|
14
15
|
findExistingEventVersion,
|
|
@@ -54,6 +55,9 @@ export async function appendRaw(runner: DbRunner, event: RawEventToAppend): Prom
|
|
|
54
55
|
const eventVersion = event.eventVersion ?? 1;
|
|
55
56
|
|
|
56
57
|
try {
|
|
58
|
+
// See db/queries/event-store.ts's claimXactId — gap-finality needs a
|
|
59
|
+
// real xact id assigned before this holder's insert.
|
|
60
|
+
await claimXactId(runner);
|
|
57
61
|
if (event.expectedVersion === 0) {
|
|
58
62
|
await insertRawFirst(runner, event, newVersion, eventVersion);
|
|
59
63
|
} else {
|
|
@@ -145,6 +149,7 @@ export async function appendRawBatch(
|
|
|
145
149
|
});
|
|
146
150
|
|
|
147
151
|
try {
|
|
152
|
+
await claimXactId(runner);
|
|
148
153
|
await insertRawEventBatch(runner, valuesClauses.join(", "), params);
|
|
149
154
|
} catch (e) {
|
|
150
155
|
if (isUniqueViolation(e)) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
type EventMetadata,
|
|
3
|
+
isPersonalDataGated,
|
|
3
4
|
type StoredEvent,
|
|
4
5
|
UNATTRIBUTED_ORIGIN,
|
|
5
6
|
} from "@cosmicdrift/kumiko-types/event-store-types";
|
|
@@ -15,6 +16,7 @@ import { encryptEventPayloadPii } from "../crypto/event-pii";
|
|
|
15
16
|
import type { DbRunner } from "../db";
|
|
16
17
|
import { constraintOf, isUniqueViolation } from "../db/pg-error";
|
|
17
18
|
import {
|
|
19
|
+
claimXactId,
|
|
18
20
|
insertSubsequentEventRow,
|
|
19
21
|
notifyPgChannel,
|
|
20
22
|
selectAggregateMaxVersion,
|
|
@@ -93,14 +95,16 @@ export async function append(db: DbRunner, event: EventToAppend): Promise<Stored
|
|
|
93
95
|
const eventVersion = toStore.eventVersion ?? 1;
|
|
94
96
|
|
|
95
97
|
try {
|
|
98
|
+
await claimXactId(db);
|
|
99
|
+
|
|
96
100
|
const row =
|
|
97
101
|
toStore.expectedVersion === 0
|
|
98
102
|
? await insertFirstEvent(db, toStore, newVersion, eventVersion)
|
|
99
103
|
: await insertSubsequentEvent(db, toStore, newVersion, eventVersion);
|
|
100
104
|
|
|
101
|
-
// NOTIFY
|
|
102
|
-
//
|
|
103
|
-
//
|
|
105
|
+
// NOTIFY after the INSERT: outside a transaction each statement commits
|
|
106
|
+
// on its own, so a NOTIFY sent first would wake the dispatcher before
|
|
107
|
+
// the row exists.
|
|
104
108
|
await notifyPgChannel(db, EVENTS_PUBSUB_CHANNEL);
|
|
105
109
|
|
|
106
110
|
return buildStoredEvent(toStore, newVersion, eventVersion, row);
|
|
@@ -127,12 +131,15 @@ export async function append(db: DbRunner, event: EventToAppend): Promise<Stored
|
|
|
127
131
|
// bypass this deliberately — they replay historical rows verbatim.
|
|
128
132
|
function stampOrigin(event: EventToAppend): EventToAppend {
|
|
129
133
|
const origin = requestContext.get();
|
|
134
|
+
const { writeOrigin: _droppedCallerWriteOrigin, ...restMetadata } = event.metadata;
|
|
130
135
|
return {
|
|
131
136
|
...event,
|
|
132
137
|
metadata: {
|
|
133
|
-
...
|
|
138
|
+
...restMetadata,
|
|
134
139
|
feature: origin?.feature ?? UNATTRIBUTED_ORIGIN,
|
|
135
140
|
handler: origin?.handler ?? UNATTRIBUTED_ORIGIN,
|
|
141
|
+
...(origin?.writeOrigin &&
|
|
142
|
+
isPersonalDataGated(origin.writeOrigin) && { writeOrigin: origin.writeOrigin }),
|
|
136
143
|
},
|
|
137
144
|
};
|
|
138
145
|
}
|
|
@@ -380,9 +387,11 @@ export async function loadAllEventsByType(
|
|
|
380
387
|
|
|
381
388
|
// Stream every event for an aggregate_type across all tenants, batchwise
|
|
382
389
|
// instead of buffered. Memory-bounded: never more than `batchSize` rows
|
|
383
|
-
// resident. Cursor walks `events.id` (bigserial monotonic
|
|
384
|
-
//
|
|
385
|
-
//
|
|
390
|
+
// resident. Cursor walks `events.id` (bigserial monotonic, but ids are
|
|
391
|
+
// assigned at INSERT time, not commit time — a sweep can overtake a still-
|
|
392
|
+
// open transaction and its lower id, same gap the event-dispatcher's
|
|
393
|
+
// pending_gaps tracks). Fine for this generator's use case: history sweeps
|
|
394
|
+
// (projection-rebuild, tests) that don't need live, just-committed rows.
|
|
386
395
|
//
|
|
387
396
|
// Use case: projection-rebuild on a large event log (>100k events per
|
|
388
397
|
// aggregate-type). loadAllEventsByType would OOM; this iterator yields
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// so a >= assertion is flake-free without inflating waitFor's budget.
|
|
11
11
|
|
|
12
12
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
13
|
-
import
|
|
13
|
+
import * as z from "zod";
|
|
14
14
|
import { defineFeature } from "../../engine";
|
|
15
15
|
import { setupTestStack, type TestStack, TestUsers } from "../../stack";
|
|
16
16
|
import { waitFor } from "../../testing";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
2
|
import type { Hono } from "hono";
|
|
3
|
-
import
|
|
3
|
+
import * as z from "zod";
|
|
4
4
|
import { buildServer, type JwtHelper } from "../../api";
|
|
5
5
|
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
6
6
|
import { createRegistry, defineFeature, type SessionUser } from "../../engine";
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
10
10
|
import type { Hono } from "hono";
|
|
11
|
-
import
|
|
11
|
+
import * as z from "zod";
|
|
12
12
|
import { buildServer, type JwtHelper } from "../../api";
|
|
13
13
|
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
14
14
|
import { createRegistry, defineFeature, type SessionUser } from "../../engine";
|