@cosmicdrift/kumiko-framework 0.386.0 → 0.387.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/LICENSE +21 -57
- package/README.md +2 -2
- package/agents/widgets.md +39 -39
- package/dist/api/api-constants.js +2 -2
- package/dist/api/auth-middleware.js +7 -7
- package/dist/api/auth-rate-limiters.js +7 -3
- package/dist/api/auth-routes-mfa.js +1 -1
- package/dist/api/auth-routes-session.js +1 -1
- package/dist/api/auth-routes-shared.js +7 -3
- package/dist/api/auth-routes-signup.js +1 -1
- package/dist/api/client-ip.js +3 -1
- package/dist/api/extra-route.js +1 -1
- package/dist/api/pii-leak-guard.js +4 -2
- package/dist/api/readiness.js +5 -5
- package/dist/api/redis-sse-broker.js +8 -6
- package/dist/api/routes.js +2 -2
- package/dist/api/server.js +34 -33
- package/dist/api/sse-broker.js +1 -1
- package/dist/api/sse-route.d.ts +13 -13
- package/dist/api/sse-route.js +15 -15
- package/dist/auth/index.js +4 -4
- package/dist/auth/roles.d.ts +4 -4
- package/dist/auth/roles.js +24 -24
- package/dist/compliance/duration-spec.js +18 -20
- package/dist/compliance/index.js +2 -3
- package/dist/compliance/override-schema.d.ts +4 -4
- package/dist/compliance/override-schema.js +19 -22
- package/dist/compliance/profiles.d.ts +26 -29
- package/dist/compliance/profiles.js +62 -69
- package/dist/compliance/sub-processors.d.ts +28 -30
- package/dist/compliance/sub-processors.js +17 -21
- package/dist/consumer-cli.js +4 -4
- package/dist/crypto/blind-index.js +18 -17
- package/dist/crypto/ciphertext-pattern.js +1 -1
- package/dist/crypto/event-pii.js +3 -3
- package/dist/crypto/kek-source.d.ts +1 -1
- package/dist/crypto/kek-source.js +3 -2
- package/dist/crypto/kms-adapter.d.ts +1 -1
- package/dist/crypto/kms-adapter.js +4 -6
- package/dist/crypto/kms-wiring.d.ts +1 -2
- package/dist/crypto/kms-wiring.js +2 -2
- package/dist/crypto/pg-kms-adapter.js +2 -6
- package/dist/crypto/pii-field-encryption.js +4 -4
- package/dist/crypto/subject-resolver.d.ts +1 -1
- package/dist/crypto/subject-resolver.js +2 -2
- package/dist/db/apply-entity-event.d.ts +6 -6
- package/dist/db/apply-entity-event.js +55 -55
- package/dist/db/blind-index-cleanup.js +7 -7
- package/dist/db/collect-table-metas.js +10 -11
- package/dist/db/compound-types.d.ts +1 -1
- package/dist/db/compound-types.js +12 -12
- package/dist/db/cursor.js +3 -4
- package/dist/db/dialect.d.ts +2 -2
- package/dist/db/dialect.js +2 -2
- package/dist/db/eagerload.d.ts +3 -3
- package/dist/db/eagerload.js +21 -12
- package/dist/db/entity-table-meta.d.ts +2 -2
- package/dist/db/entity-table-meta.js +4 -4
- package/dist/db/event-store-executor-context.d.ts +1 -1
- package/dist/db/event-store-executor-context.js +13 -13
- package/dist/db/event-store-executor-read.js +27 -28
- package/dist/db/event-store-executor-write.js +65 -70
- package/dist/db/located-timestamp.d.ts +11 -11
- package/dist/db/located-timestamp.js +29 -30
- package/dist/db/migrate-generator.d.ts +1 -1
- package/dist/db/migrate-generator.js +26 -19
- package/dist/db/migrate-runner.js +25 -25
- package/dist/db/money.d.ts +6 -6
- package/dist/db/money.js +29 -27
- package/dist/db/queries/backfill-pii.js +5 -5
- package/dist/db/queries/ddl.js +1 -1
- package/dist/db/queries/es-ops.d.ts +1 -1
- package/dist/db/queries/es-ops.js +1 -1
- package/dist/db/queries/event-consumer.js +3 -3
- package/dist/db/queries/event-store.js +4 -2
- package/dist/db/queries/projection-rebuild.js +3 -3
- package/dist/db/queries/raw-sql.d.ts +2 -2
- package/dist/db/queries/raw-sql.js +2 -2
- package/dist/db/queries/shadow-swap.js +19 -19
- package/dist/db/query.d.ts +1 -1
- package/dist/db/query.js +6 -6
- package/dist/db/rebuild-marker.js +20 -21
- package/dist/db/replay-migration-sql.js +2 -2
- package/dist/db/schema-inspection.js +4 -5
- package/dist/db/table-builder.d.ts +1 -2
- package/dist/db/table-builder.js +82 -82
- package/dist/db/tenant-db.d.ts +2 -2
- package/dist/db/tenant-db.js +9 -2
- package/dist/derivatives/delete-stored-file.js +5 -2
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/engine/boot-validator/access-roles.js +4 -3
- package/dist/engine/boot-validator/api-ext.js +14 -14
- package/dist/engine/boot-validator/config-deps.js +7 -6
- package/dist/engine/boot-validator/detail-screens.js +3 -3
- package/dist/engine/boot-validator/entity-handler.js +60 -61
- package/dist/engine/boot-validator/gdpr-storage.js +4 -4
- package/dist/engine/boot-validator/i18n-keys.js +1 -1
- package/dist/engine/boot-validator/index.d.ts +2 -2
- package/dist/engine/boot-validator/index.js +20 -20
- package/dist/engine/boot-validator/nav.js +5 -5
- package/dist/engine/boot-validator/ownership.js +12 -9
- package/dist/engine/boot-validator/parent-ref.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +50 -57
- package/dist/engine/boot-validator/projection-list-screens.js +7 -7
- package/dist/engine/boot-validator/query-refs.js +1 -1
- package/dist/engine/boot-validator/screens-actions.js +15 -16
- package/dist/engine/boot-validator/screens-forms.js +10 -11
- package/dist/engine/boot-validator/screens-lists.js +14 -15
- package/dist/engine/boot-validator/screens.js +34 -40
- package/dist/engine/boot-validator/security-baseline.js +3 -2
- package/dist/engine/boot-validator/transfer-graph.js +2 -2
- package/dist/engine/boot-validator/workspaces.js +3 -3
- package/dist/engine/bridge-stub.js +3 -4
- package/dist/engine/build-app-schema.js +75 -75
- package/dist/engine/build-config-feature-schema.js +50 -50
- package/dist/engine/build-target.js +20 -23
- package/dist/engine/codemod/pipeline-codemod.js +20 -13
- package/dist/engine/config-helpers.js +4 -4
- package/dist/engine/constants.js +2 -2
- package/dist/engine/create-app.js +2 -2
- package/dist/engine/define-feature.js +12 -11
- package/dist/engine/define-handler.js +12 -11
- package/dist/engine/define-step.js +1 -1
- package/dist/engine/define-workflow.d.ts +5 -5
- package/dist/engine/define-workflow.js +6 -6
- package/dist/engine/entity-handlers.js +13 -14
- package/dist/engine/event-def.js +2 -2
- package/dist/engine/extension-names.d.ts +16 -17
- package/dist/engine/extension-names.js +37 -38
- package/dist/engine/extensions/escape-hatch-usage.js +1 -1
- package/dist/engine/extensions/tenant-data.js +2 -2
- package/dist/engine/extensions/user-data.d.ts +23 -24
- package/dist/engine/factories.d.ts +14 -13
- package/dist/engine/factories.js +38 -37
- package/dist/engine/feature-ast/entity-field-types.js +1 -1
- package/dist/engine/feature-ast/extractors/handlers.js +1 -1
- package/dist/engine/feature-ast/extractors/hooks.js +1 -1
- package/dist/engine/feature-ast/extractors/shared.d.ts +2 -3
- package/dist/engine/feature-ast/extractors/shared.js +2 -3
- package/dist/engine/feature-ast/parse.js +13 -13
- package/dist/engine/feature-ast/patch.js +1 -1
- package/dist/engine/feature-ast/pattern-change-schema.js +1 -1
- package/dist/engine/feature-ast/patterns.d.ts +2 -2
- package/dist/engine/feature-ast/render.js +2 -2
- package/dist/engine/feature-config-events-jobs.js +15 -12
- package/dist/engine/feature-entity-handlers.js +9 -5
- package/dist/engine/feature-manifest.d.ts +9 -9
- package/dist/engine/feature-manifest.js +6 -7
- package/dist/engine/feature-ui-extensions.d.ts +3 -3
- package/dist/engine/feature-ui-extensions.js +30 -22
- package/dist/engine/field-access.js +5 -6
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +4 -4
- package/dist/engine/object-form.d.ts +1 -0
- package/dist/engine/object-form.js +14 -4
- package/dist/engine/ownership.js +3 -3
- package/dist/engine/parse-ref-target.js +5 -7
- package/dist/engine/pattern-library/index.js +2 -2
- package/dist/engine/pattern-library/library.js +2 -2
- package/dist/engine/pattern-library/types.js +3 -3
- package/dist/engine/pipeline.js +2 -2
- package/dist/engine/project-app-schema-for-roles.js +1 -1
- package/dist/engine/projection-helpers.js +9 -9
- package/dist/engine/qualified-name.js +2 -2
- package/dist/engine/registry-facade.js +1 -1
- package/dist/engine/registry-ingest.js +12 -12
- package/dist/engine/registry-state.js +17 -19
- package/dist/engine/registry-validate.js +12 -12
- package/dist/engine/run-in.js +2 -2
- package/dist/engine/run-pipeline.js +7 -0
- package/dist/engine/schema-builder.js +40 -38
- package/dist/engine/screen-filter-ops.js +17 -20
- package/dist/engine/screen-helpers.js +4 -3
- package/dist/engine/state-machine.d.ts +12 -12
- package/dist/engine/state-machine.js +7 -7
- package/dist/engine/steps/_resolver-utils.js +4 -3
- package/dist/engine/steps/for-each.js +4 -4
- package/dist/engine/steps/unsafe-projection-delete.js +1 -1
- package/dist/engine/steps/unsafe-projection-upsert.js +4 -4
- package/dist/engine/steps/wait-for-event.js +4 -4
- package/dist/engine/steps/workflow-system-events.js +3 -3
- package/dist/engine/tier-resolver-extension.d.ts +29 -27
- package/dist/engine/tier-resolver-extension.js +26 -26
- package/dist/engine/transfer-adjacency.js +2 -2
- package/dist/engine/validate-projection-allowlist.js +7 -27
- package/dist/engine/walk-step-instances.d.ts +2 -0
- package/dist/engine/walk-step-instances.js +38 -0
- package/dist/engine/where-rule-lint.js +1 -1
- package/dist/engine/write-handler-qn-extract.js +1 -1
- package/dist/entrypoint/index.js +3 -3
- package/dist/env/index.d.ts +7 -6
- package/dist/env/index.js +10 -9
- package/dist/errors/transition-details.js +7 -7
- package/dist/errors/write-error-info.d.ts +6 -6
- package/dist/errors/write-error-info.js +6 -6
- package/dist/es-ops/context.d.ts +9 -9
- package/dist/es-ops/context.js +28 -28
- package/dist/es-ops/index.js +7 -9
- package/dist/es-ops/operations-schema.js +4 -10
- package/dist/es-ops/runner.d.ts +11 -11
- package/dist/es-ops/runner.js +49 -53
- package/dist/es-ops/types.d.ts +55 -55
- package/dist/es-ops/types.js +11 -11
- package/dist/event-store/admin-api.js +6 -7
- package/dist/event-store/errors.js +1 -1
- package/dist/event-store/event-store.js +2 -2
- package/dist/event-store/rebuild-dead-letter.js +1 -1
- package/dist/files/feature.js +9 -10
- package/dist/files/file-handle.js +5 -6
- package/dist/files/file-ref-entity.js +15 -15
- package/dist/files/file-ref-table.js +7 -8
- package/dist/files/file-routes.d.ts +7 -0
- package/dist/files/file-routes.js +11 -5
- package/dist/files/in-memory-provider.js +8 -9
- package/dist/files/local-provider.js +18 -25
- package/dist/files/provider-resolver.d.ts +7 -7
- package/dist/files/storage-tracking.js +2 -2
- package/dist/files/zip-stream.d.ts +12 -19
- package/dist/files/zip-stream.js +44 -63
- package/dist/i18n/required-surface-keys.d.ts +1 -1
- package/dist/i18n/required-surface-keys.js +8 -8
- package/dist/jobs/job-runner-helpers.js +8 -2
- package/dist/jobs/job-runner-schedulers.d.ts +1 -1
- package/dist/jobs/job-runner-schedulers.js +3 -4
- package/dist/jobs/job-runner.js +13 -15
- package/dist/lifecycle/lifecycle.js +3 -5
- package/dist/lifecycle/signal-handlers.js +5 -4
- package/dist/logging/utils.d.ts +2 -1
- package/dist/logging/utils.js +38 -10
- package/dist/migrations/index.js +1 -1
- package/dist/migrations/kumiko-drift.js +14 -13
- package/dist/migrations/pending-rebuilds.d.ts +21 -20
- package/dist/migrations/pending-rebuilds.js +27 -27
- package/dist/migrations/projection-table-index.d.ts +3 -3
- package/dist/migrations/projection-table-index.js +8 -8
- package/dist/observability/metric-validator.js +1 -1
- package/dist/observability/metrics-handle.js +3 -3
- package/dist/pipeline/append-event-core.js +11 -2
- package/dist/pipeline/cascade-handler.d.ts +1 -1
- package/dist/pipeline/dispatch-batch.js +2 -2
- package/dist/pipeline/dispatch-query.js +10 -10
- package/dist/pipeline/dispatch-shared.d.ts +1 -0
- package/dist/pipeline/dispatch-shared.js +15 -17
- package/dist/pipeline/dispatch-stream.js +1 -1
- package/dist/pipeline/dispatch-write.js +3 -3
- package/dist/pipeline/dispatcher-utils.d.ts +1 -1
- package/dist/pipeline/dispatcher-utils.js +1 -1
- package/dist/pipeline/event-consumer-state.js +2 -2
- package/dist/pipeline/event-dispatcher-delivery.js +1 -1
- package/dist/pipeline/event-dispatcher.js +17 -28
- package/dist/pipeline/irreversible-operation-gate.js +1 -1
- package/dist/pipeline/lifecycle-pipeline.js +9 -9
- package/dist/pipeline/member-read-only-transaction.js +1 -3
- package/dist/pipeline/msp-rebuild.js +1 -1
- package/dist/pipeline/projection-rebuild.js +13 -12
- package/dist/pipeline/system-hooks.js +39 -41
- package/dist/pipeline/system-identity-switch.js +1 -3
- package/dist/pipeline/tenant-timezone-cache.js +1 -1
- package/dist/random/generate.d.ts +29 -29
- package/dist/random/generate.js +30 -30
- package/dist/random/words.js +20 -20
- package/dist/rate-limit/middleware.js +9 -5
- package/dist/redis/client.js +2 -2
- package/dist/redis/pubsub-signal.js +14 -17
- package/dist/schema-cli.js +116 -22
- package/dist/search/index.js +4 -4
- package/dist/search/purge-subject.js +4 -5
- package/dist/search/reindex-entity.js +6 -9
- package/dist/time/boot-tz-warning.d.ts +2 -2
- package/dist/time/boot-tz-warning.js +8 -8
- package/dist/time/iana.js +10 -10
- package/dist/time/index.js +9 -9
- package/dist/time/legacy-date.d.ts +2 -2
- package/dist/time/legacy-date.js +6 -8
- package/dist/time/polyfill.js +1 -1
- package/dist/time/tz-context.d.ts +1 -1
- package/dist/time/tz-context.js +1 -1
- package/dist/ui-types/app-schema.js +7 -7
- package/dist/ui-types/list-row-meta.js +4 -4
- package/dist/upgrade-cli.js +10 -9
- package/dist/utils/compare.js +1 -1
- package/package.json +4 -4
- package/src/changes.json +48 -0
- package/src/scripts/codemod/crypto-shredding-testing-move.ts +2 -2
- package/src/scripts/codemod/migrate-cross-tenant.ts +1 -1
- package/src/scripts/codemod/migrate-open-to-all.ts +1 -1
- package/src/scripts/codemod/pii-personal-migration.ts +17 -8
- package/src/scripts/codemod/unprocessable-error-details-reason.ts +2 -2
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
// Public API of the pattern-library — Designer
|
|
2
|
-
// MCP-Server
|
|
1
|
+
// Public API of the pattern-library — Designer, AI-Builder,
|
|
2
|
+
// MCP-Server consume from here.
|
|
3
3
|
export { getPatternSchema, groupByCategory, PATTERN_LIBRARY } from "./library.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Pattern-Library — concrete FormSchema entries for every FeaturePattern
|
|
2
|
-
// kind. Centralised here so the Designer
|
|
3
|
-
// and the MCP-Server
|
|
2
|
+
// kind. Centralised here so the Designer, the AI-Builder,
|
|
3
|
+
// and the MCP-Server share one source-of-truth for "how does this
|
|
4
4
|
// pattern look as a form?".
|
|
5
5
|
//
|
|
6
6
|
// **Updating contract:** when a new pattern-kind gets a parser/renderer
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Forms-Schema metadata — describes how a FeaturePattern is rendered as
|
|
2
2
|
// a Designer form, fed to an LLM as a JSON-Schema, or surfaced in the
|
|
3
3
|
// MCP-Server tool list. One schema, three consumers — that's the whole
|
|
4
|
-
// point of factoring this out of the hand-rolled
|
|
4
|
+
// point of factoring this out of the hand-rolled skeleton.
|
|
5
5
|
//
|
|
6
6
|
// **NOT a DSL.** The Source-of-Truth for a feature stays
|
|
7
7
|
// `defineFeature.ts` — the AST-visitor (feature-ast/parse.ts) reads it
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// already-parsed FeaturePattern in a form. No alternative parser, no
|
|
11
11
|
// alternative syntax, no second canonical representation.
|
|
12
12
|
//
|
|
13
|
-
// **
|
|
13
|
+
// **Vocabulary** (12 input-types):
|
|
14
14
|
//
|
|
15
15
|
// text — single-line string (name / label / id)
|
|
16
16
|
// textarea — multi-line string (description / SQL / readme)
|
|
@@ -31,6 +31,6 @@
|
|
|
31
31
|
// FeaturePattern object using dot-separated keys (`definition.fields`,
|
|
32
32
|
// `access.roles`, `templates.email`). Numeric indices use the bracket
|
|
33
33
|
// form (`columns.0`). The Designer/LLM uses paths for read & write —
|
|
34
|
-
// the patcher
|
|
34
|
+
// the patcher consumes pattern-level updates only, so paths are
|
|
35
35
|
// purely a UI concern at this stage.
|
|
36
36
|
export {};
|
package/dist/engine/pipeline.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// the resolver-side PipelineCtx (run-pipeline.ts). Resolvers that need
|
|
9
9
|
// prior step results destructure them from the resolver's ctx.
|
|
10
10
|
//
|
|
11
|
-
// Renamed from `pipeline()`
|
|
11
|
+
// Renamed from `pipeline()` — it shared its name with the
|
|
12
12
|
// internal `packages/framework/src/pipeline/` directory (dispatcher,
|
|
13
13
|
// lifecycle, outbox-poller), which made repo-wide grep for `pipeline`
|
|
14
14
|
// return mixed results.
|
|
@@ -76,7 +76,7 @@ export function stepsPipeline(closure) {
|
|
|
76
76
|
// awaits map wired into the closure. The cast below is the one place a
|
|
77
77
|
// workflow's raw declared event-type strings become AwaitedEventType — safe
|
|
78
78
|
// because pipelineDef.awaits only ever holds what defineWorkflow's `awaits`
|
|
79
|
-
// carried in (
|
|
79
|
+
// carried in (workflow-resume-loop.md), never an unchecked external
|
|
80
80
|
// value.
|
|
81
81
|
export function buildPipelineSteps(pipelineDef, event) {
|
|
82
82
|
const steps = pipelineDef.build({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Per-role projection of a fully-built AppSchema
|
|
1
|
+
// Per-role projection of a fully-built AppSchema. GET /api/schema
|
|
2
2
|
// used to ship the same full schema — screens, navs, workspaces, content
|
|
3
3
|
// collections — to every authenticated user regardless of role, relying on
|
|
4
4
|
// client-side UI checks (isUiAccessGranted) alone to hide what a role
|
|
@@ -1,27 +1,27 @@
|
|
|
1
1
|
import { updateMany } from "../db/query.js";
|
|
2
|
-
// Typed
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// Typed apply helper for r.projection.apply: enables typed event.payload
|
|
3
|
+
// access per event type without propagating a SingleStreamApplyFn generic
|
|
4
|
+
// through the entire ProjectionDefinition.
|
|
5
5
|
//
|
|
6
|
-
//
|
|
6
|
+
// The helper is purely a type vehicle — an identity function at runtime:
|
|
7
7
|
//
|
|
8
8
|
// apply: {
|
|
9
9
|
// "user.created": defineApply<UserCreatedPayload>(async (event, tx, table) => {
|
|
10
|
-
// // event.payload
|
|
10
|
+
// // event.payload is UserCreatedPayload, not Record<string, unknown>.
|
|
11
11
|
// // Write through the passed `table` (ES-blessed inside apply), not a
|
|
12
12
|
// // closed-over branded constant.
|
|
13
13
|
// await insertOne(tx, table, { id: event.aggregateId, ...event.payload });
|
|
14
14
|
// }),
|
|
15
15
|
// }
|
|
16
16
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
17
|
+
// The default generic, Record<string, unknown>, preserves backward-compatible
|
|
18
|
+
// behavior for apply handlers written without a type argument. The `table`
|
|
19
|
+
// parameter is optional to use, so 2-argument applies remain valid.
|
|
20
20
|
export function defineApply(fn) {
|
|
21
21
|
// @cast-boundary engine-bridge — typed user-fn → erased internal storage
|
|
22
22
|
return fn;
|
|
23
23
|
}
|
|
24
|
-
//
|
|
24
|
+
// Counterpart for r.multiStreamProjection.apply — also receives ctx.
|
|
25
25
|
export function defineMspApply(fn) {
|
|
26
26
|
// @cast-boundary engine-bridge — typed user-fn → erased internal storage
|
|
27
27
|
return fn;
|
|
@@ -32,7 +32,7 @@ function validateSegment(value, label, context) {
|
|
|
32
32
|
}
|
|
33
33
|
// registry-ingest.ts also qualifies handlers/projections with these two —
|
|
34
34
|
// not in the public QnTypes enum, but reachable as a type segment the same
|
|
35
|
-
// way
|
|
35
|
+
// way.
|
|
36
36
|
const NON_ENUM_QN_TYPES = ["stream", "projection", "workspace"];
|
|
37
37
|
// Every segment word that a fully-qualified QN can carry as its type.
|
|
38
38
|
const RESERVED_QN_SEGMENTS = new Set([
|
|
@@ -44,7 +44,7 @@ const RESERVED_QN_SEGMENTS = new Set([
|
|
|
44
44
|
// the trailing part. Both signals must hold together: the scope alone
|
|
45
45
|
// repeating (e.g. entity "user:create" on feature "user") or a reserved word
|
|
46
46
|
// alone appearing (e.g. "compliance:query:for-tenant") are both common,
|
|
47
|
-
// legitimate sub-structure on their own
|
|
47
|
+
// legitimate sub-structure on their own.
|
|
48
48
|
function isDoubleQualified(scope, segments) {
|
|
49
49
|
const [first, second] = segments;
|
|
50
50
|
return first === scope && second !== undefined && RESERVED_QN_SEGMENTS.has(second);
|
|
@@ -85,7 +85,7 @@ export function buildRegistryFacade(state) {
|
|
|
85
85
|
getEntityPostQueryHooks(entityName, effectiveFeatures) {
|
|
86
86
|
return filterOwned(state.entityPostQueryHooks.get(entityName), effectiveFeatures);
|
|
87
87
|
},
|
|
88
|
-
//
|
|
88
|
+
// Search-Payload-Extension contributors for an entity. Used by
|
|
89
89
|
// `buildSearchDocument` in system-hooks.ts to enrich the indexed payload.
|
|
90
90
|
// `effectiveFeatures` filters out contributors owned by feature-toggle-
|
|
91
91
|
// disabled features (parallel to getEntityPostQueryHooks etc.).
|
|
@@ -23,9 +23,9 @@ export function populateFeatureCore(state, feature) {
|
|
|
23
23
|
`collides with r.storeTable("${physical}") (feature "${clash.featureName}"). ` +
|
|
24
24
|
`Pick a different tableName — both would emit CREATE TABLE "${physical}".`);
|
|
25
25
|
}
|
|
26
|
-
// Entity-vs-entity
|
|
27
|
-
//
|
|
28
|
-
//
|
|
26
|
+
// Entity-vs-entity collisions are equally fatal: two entities with an explicit,
|
|
27
|
+
// identical tableName previously overwrote each other silently here—
|
|
28
|
+
// causing duplicate CREATE TABLE statements or one projection to replace the other.
|
|
29
29
|
if (clash?.kind === "entity") {
|
|
30
30
|
throw new Error(`Entity "${name}" (feature "${feature.name}") has physical table "${physical}" which ` +
|
|
31
31
|
`collides with entity "${clash.owner}" (feature "${clash.featureName}"). ` +
|
|
@@ -232,7 +232,7 @@ export function populateTranslations(state, feature) {
|
|
|
232
232
|
// Translations prefixed with featureName: (i18next namespace convention).
|
|
233
233
|
// Keys that already carry the feature's own namespace prefix (e.g. a nav
|
|
234
234
|
// label referencing "cap-counter:nav.cap-list" verbatim) must NOT be
|
|
235
|
-
// re-prefixed, else server-side t() can never resolve them
|
|
235
|
+
// re-prefixed, else server-side t() can never resolve them. Both
|
|
236
236
|
// bare and qualified spellings for the same key are accepted; if a
|
|
237
237
|
// feature defines both, whichever is declared last wins (Object.entries
|
|
238
238
|
// = insertion order, not "qualified always wins").
|
|
@@ -263,7 +263,7 @@ export function populateHooks(state, feature) {
|
|
|
263
263
|
mergeHookList(state.entityPreDeleteHooks, feature.entityHooks?.preDelete);
|
|
264
264
|
mergeHookList(state.entityPostDeleteHooks, feature.entityHooks?.postDelete);
|
|
265
265
|
mergeHookList(state.entityPostQueryHooks, feature.entityHooks?.postQuery);
|
|
266
|
-
//
|
|
266
|
+
// Search-payload-extensions: per-entity contributors merged additively
|
|
267
267
|
for (const [entityName, contributors] of Object.entries(feature.searchPayloadExtensions ?? {})) {
|
|
268
268
|
const existing = state.searchPayloadExtensions.get(entityName) ?? [];
|
|
269
269
|
for (const c of contributors)
|
|
@@ -460,10 +460,10 @@ export function populateScreensNavWorkspaces(state, feature) {
|
|
|
460
460
|
const stored = { ...screenDef, id: qualified };
|
|
461
461
|
state.screenMap.set(qualified, stored);
|
|
462
462
|
state.screenFeatureMap.set(qualified, feature.name);
|
|
463
|
-
//
|
|
464
|
-
// entityList/entityEdit
|
|
465
|
-
//
|
|
466
|
-
//
|
|
463
|
+
// Index only screens that are directly attached to an entity.
|
|
464
|
+
// entityList/entityEdit have `entity`; custom + actionForm do not reference
|
|
465
|
+
// an entity (custom is opaque, and actionForm has inline fields without
|
|
466
|
+
// an entity reference).
|
|
467
467
|
if (stored.type === "entityList" || stored.type === "entityEdit") {
|
|
468
468
|
const existing = state.screensByEntity.get(stored.entity) ?? [];
|
|
469
469
|
existing.push(stored);
|
|
@@ -515,9 +515,9 @@ export function populateScreensNavWorkspaces(state, feature) {
|
|
|
515
515
|
}
|
|
516
516
|
}
|
|
517
517
|
}
|
|
518
|
-
// Tree-Actions slot
|
|
519
|
-
// registrar).
|
|
520
|
-
//
|
|
518
|
+
// Tree-Actions slot—at most one per feature (the only-once guard runs in
|
|
519
|
+
// the registrar). The Map is erased for runtime lookup; the compile-time-typed
|
|
520
|
+
// surface is exposed through FeatureDefinition.exports (TreeActionsHandle).
|
|
521
521
|
if (feature.treeActions !== undefined) {
|
|
522
522
|
state.treeActionsMap.set(feature.name, feature.treeActions);
|
|
523
523
|
}
|
|
@@ -5,25 +5,25 @@ import { asEntityTableMeta } from "../db/query.js";
|
|
|
5
5
|
import { buildEntityTable } from "../db/table-builder.js";
|
|
6
6
|
import { qualifyEntityName } from "./qualified-name.js";
|
|
7
7
|
const IMPLICIT_PROJECTION_SUFFIX = "-entity";
|
|
8
|
-
//
|
|
9
|
-
// apply
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
8
|
+
// Each `r.entity` registration gets an implicit projection with generated
|
|
9
|
+
// apply handlers for the four automatic verbs. The live path writes directly
|
|
10
|
+
// to the table through EventStoreExecutor, while rebuildProjection replays
|
|
11
|
+
// events through this definition. Both call applyEntityEvent, ensuring live
|
|
12
|
+
// and rebuild use the same logic (also covered by
|
|
13
|
+
// implicit-projection-equivalence.integration.ts).
|
|
14
14
|
export function buildImplicitProjection(featureName, entityName, entity, qualify, backingTable, extensions = []) {
|
|
15
15
|
const name = qualify(featureName, "projection", `${entityName}${IMPLICIT_PROJECTION_SUFFIX}`);
|
|
16
16
|
// Backing table (r.entity({ name, ...def, backingTable })) is the one physical table
|
|
17
17
|
// object shared by executor-writes, rebuild-replay, test-push and
|
|
18
|
-
// collectTableMetas — restoring the
|
|
18
|
+
// collectTableMetas — restoring the invariant (test-push == generate).
|
|
19
19
|
// Validated as a superset of the field-derived columns so a field/table
|
|
20
20
|
// disagreement fails at boot, not as a silent thin-vs-rich row.
|
|
21
21
|
const drizzleTable = backingTable !== undefined
|
|
22
22
|
? resolveBackingTable(entityName, entity, backingTable)
|
|
23
23
|
: buildEntityTable(entityName, entity);
|
|
24
24
|
markExecutorManagedTable(drizzleTable);
|
|
25
|
-
// applyEntityEvent
|
|
26
|
-
// Promise<void>.
|
|
25
|
+
// applyEntityEvent returns ApplyResult, but SingleStreamApplyFn expects
|
|
26
|
+
// Promise<void>. The row is irrelevant during rebuild, so we discard it.
|
|
27
27
|
const handler = async (event, tx) => {
|
|
28
28
|
await applyEntityEvent(event, drizzleTable, entity, tx);
|
|
29
29
|
};
|
|
@@ -35,10 +35,9 @@ export function buildImplicitProjection(featureName, entityName, entity, qualify
|
|
|
35
35
|
// Registered for every entity so the erasure replays on rebuild.
|
|
36
36
|
[`${entityName}.forgotten`]: handler,
|
|
37
37
|
};
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
// registrieren den Handler gar nicht erst.
|
|
38
|
+
// The restore verb exists only for softDelete entities. Hard-delete entities
|
|
39
|
+
// should not produce restored events; if they did, applyEntityEvent would
|
|
40
|
+
// treat them as a no-op, so the handler is not registered.
|
|
42
41
|
if (entity.softDelete) {
|
|
43
42
|
apply[`${entityName}.restored`] = handler;
|
|
44
43
|
}
|
|
@@ -230,8 +229,8 @@ function ownerEnabled(owner, effectiveFeatures) {
|
|
|
230
229
|
// every hook entry (set by defineFeature), so there's no parallel
|
|
231
230
|
// bookkeeping — just append.
|
|
232
231
|
export function mergeHookList(map, source) {
|
|
233
|
-
// skip:
|
|
234
|
-
// postDelete
|
|
232
|
+
// skip: the optional entity-hook slot may be undefined when a feature has no
|
|
233
|
+
// postSave, preDelete, postDelete, or postQuery hooks.
|
|
235
234
|
if (!source)
|
|
236
235
|
return;
|
|
237
236
|
for (const [name, fns] of Object.entries(source)) {
|
|
@@ -244,10 +243,9 @@ export function mergeHookList(map, source) {
|
|
|
244
243
|
// Hook keys are handler QNs — hooks don't get their own QN, they're keyed by the handler they target.
|
|
245
244
|
// The hookQnType indicates whether the targeted handler is a write or query handler.
|
|
246
245
|
export function mergeHookListQualified(map, source, featureName, hookQnType) {
|
|
247
|
-
// skip:
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
// statt Object.entries(undefined)-Crash.
|
|
246
|
+
// skip: the optional hook slot may be omitted by hand-built definitions at
|
|
247
|
+
// system boundaries (fixtures and partial boots), even though defineFeature
|
|
248
|
+
// materializes every slot. This avoids Object.entries(undefined) crashing.
|
|
251
249
|
if (!source)
|
|
252
250
|
return;
|
|
253
251
|
for (const [name, fns] of Object.entries(source)) {
|
|
@@ -144,7 +144,7 @@ export function applyExtensionUsages(state) {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
-
//
|
|
147
|
+
// labelField defaults to "id" (a UUID column) elsewhere, but a
|
|
148
148
|
// searchable reference ILIKEs the label column — an implicit or explicit
|
|
149
149
|
// "id" default would 500 on every search request. Fail at boot.
|
|
150
150
|
function buildSearchableReferenceField(entityName, fieldName, field, entityMap) {
|
|
@@ -176,7 +176,7 @@ function buildSearchableReferenceField(entityName, fieldName, field, entityMap)
|
|
|
176
176
|
labelField: field.labelField,
|
|
177
177
|
};
|
|
178
178
|
}
|
|
179
|
-
//
|
|
179
|
+
// ORDER BY runs against the referenced row's label column, so the
|
|
180
180
|
// same explicit-labelField requirement as searchable applies — ordering by the
|
|
181
181
|
// "id" default would be a UUID order that looks deliberate to the user.
|
|
182
182
|
function buildSortableReferenceField(entityName, fieldName, field) {
|
|
@@ -230,12 +230,12 @@ export function buildSearchableSortableCaches(state) {
|
|
|
230
230
|
}
|
|
231
231
|
// Implicit-Projection pro r.entity — see buildImplicitProjection doc above.
|
|
232
232
|
export function buildImplicitProjections(state, features) {
|
|
233
|
-
// Implicit
|
|
234
|
-
//
|
|
235
|
-
// Naming
|
|
236
|
-
// "-entity"
|
|
237
|
-
//
|
|
238
|
-
//
|
|
233
|
+
// Implicit projection for r.entity. Makes the entity table rebuildable
|
|
234
|
+
// without requiring apps to declare an explicit r.projection.
|
|
235
|
+
// Naming convention: `<feature>:projection:<entityName>-entity` — the
|
|
236
|
+
// "-entity" suffix distinguishes implicit from explicit projections and
|
|
237
|
+
// prevents collisions when someone registers, for example, a
|
|
238
|
+
// cross-aggregate projection using the entity name.
|
|
239
239
|
for (const feature of features) {
|
|
240
240
|
// extendEntityProjection targets must exist as r.entity in the SAME
|
|
241
241
|
// feature — a typo'd entity name would otherwise vanish silently and the
|
|
@@ -651,7 +651,7 @@ export function validateLifecycleHookTargets(state) {
|
|
|
651
651
|
}
|
|
652
652
|
export function validateEntityHookTargets(state, features) {
|
|
653
653
|
// Same logic for entity-keyed hooks — targets must reference existing entities.
|
|
654
|
-
//
|
|
654
|
+
// A hook whose target has a typo silently registers
|
|
655
655
|
// and never fires. Validates all four entity-hook types (postSave/preDelete/
|
|
656
656
|
// postDelete/postQuery) — net cleanup of an existing antipattern, not a
|
|
657
657
|
// postQuery-special.
|
|
@@ -810,7 +810,7 @@ export function computeHasRateLimitedHandler(state) {
|
|
|
810
810
|
// Lua-script registration on Redis) when nobody opted in. Deliberately
|
|
811
811
|
// stays limited to explicit, non-disabled `rateLimit` declarations —
|
|
812
812
|
// `{ disabled: true }` must not auto-wire a resolver for an app that only
|
|
813
|
-
// opted OUT, and
|
|
813
|
+
// opted OUT, and the systemScope default only fires once a resolver
|
|
814
814
|
// already exists, so it must not force wantsL3 either.
|
|
815
815
|
state.hasRateLimitedHandlerCached = (() => {
|
|
816
816
|
for (const h of state.writeHandlerMap.values()) {
|
|
@@ -827,12 +827,12 @@ export function computeHasRateLimitedHandler(state) {
|
|
|
827
827
|
})();
|
|
828
828
|
}
|
|
829
829
|
export function publishEventPiiCatalog(state) {
|
|
830
|
-
// Publish the event-PII catalog
|
|
830
|
+
// Publish the event-PII catalog: append() — the single write funnel
|
|
831
831
|
// into kumiko_events — encrypts catalogued payload fields regardless of
|
|
832
832
|
// which path produced the event (ctx.appendEvent, MSP-apply, low-level
|
|
833
833
|
// append in delivery/jobs loggers).
|
|
834
834
|
//
|
|
835
|
-
// Defense-in-depth
|
|
835
|
+
// Defense-in-depth: defineEvent is the only producer of
|
|
836
836
|
// EventDef.piiFields and already fails closed without a stance, but this
|
|
837
837
|
// boot gate re-checks every registered event — a future producer that
|
|
838
838
|
// forgets to run through defineEvent would otherwise leave a hole where
|
package/dist/engine/run-in.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// runIn lane-routing helpers. Tiny module on purpose: both buildServer
|
|
2
|
-
// (MSP-consumer filter) and the boot-validator (
|
|
3
|
-
//
|
|
2
|
+
// (MSP-consumer filter) and the boot-validator (coverage check)
|
|
3
|
+
// need the same resolution rule, and duplicating it in two places
|
|
4
4
|
// invites drift — when the default changes, one site gets updated and
|
|
5
5
|
// the other silently doesn't.
|
|
6
6
|
//
|
|
@@ -21,6 +21,8 @@
|
|
|
21
21
|
// errors propagate to the dispatcher's catch which maps them to
|
|
22
22
|
// WriteFailure / HTTP. "return" / "skip" / fallback strategies land in
|
|
23
23
|
// later slices together with their own integration tests.
|
|
24
|
+
import { InternalError } from "../errors/index.js";
|
|
25
|
+
import { describeShape, isWriteResultShape } from "../pipeline/dispatcher-utils.js";
|
|
24
26
|
import { getStep } from "./define-step.js";
|
|
25
27
|
import { buildPipelineSteps } from "./pipeline.js";
|
|
26
28
|
import { SUSPEND_SENTINEL } from "./steps/_step-dispatch-constants.js";
|
|
@@ -112,6 +114,11 @@ export async function runStepList(steps, event, handlerCtx, stepsAcc, scopeAcc,
|
|
|
112
114
|
}
|
|
113
115
|
const key = stepDef.resultKey?.(instance.args);
|
|
114
116
|
if (key === RETURN_RESULT_KEY) {
|
|
117
|
+
if (!isWriteResultShape(value)) {
|
|
118
|
+
throw new InternalError({
|
|
119
|
+
message: `Step "${instance.kind}" at index ${i} must return a WriteResult, got ${describeShape(value)}`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
115
122
|
return { kind: "return", result: value };
|
|
116
123
|
}
|
|
117
124
|
if (key !== undefined) {
|
|
@@ -16,10 +16,11 @@ export function isRepresentableAtScale(n, scale) {
|
|
|
16
16
|
const tolerance = Math.abs(scaled) * 8 * Number.EPSILON + Number.EPSILON;
|
|
17
17
|
return Math.abs(scaled - Math.round(scaled)) <= tolerance;
|
|
18
18
|
}
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
19
|
+
// Lexicographic ISO comparison — exact for `yyyy-mm-dd` (date) and correct
|
|
20
|
+
// for ISO datetimes in a consistent representation (same offset/precision
|
|
21
|
+
// form). Deliberately avoids the Date API (no-date-api-Guard); day-level
|
|
22
|
+
// comparison is sufficient for min/max use cases (e.g. a birth date not in
|
|
23
|
+
// the future).
|
|
23
24
|
function withDateBounds(schema, min, max) {
|
|
24
25
|
if (min === undefined && max === undefined)
|
|
25
26
|
return schema;
|
|
@@ -127,8 +128,9 @@ export function fieldToZod(field, currencies, opts = {}) {
|
|
|
127
128
|
return field.default !== undefined && applyDefaults ? schema.default(field.default) : schema;
|
|
128
129
|
}
|
|
129
130
|
case "longText": {
|
|
130
|
-
// longText
|
|
131
|
-
//
|
|
131
|
+
// longText has no `format` variant (by type design). Only optional
|
|
132
|
+
// maxLength and required are supported; otherwise it is an unrestricted
|
|
133
|
+
// z.string().
|
|
132
134
|
let schema = z.string();
|
|
133
135
|
if (field.maxLength)
|
|
134
136
|
schema = schema.max(field.maxLength);
|
|
@@ -148,13 +150,13 @@ export function fieldToZod(field, currencies, opts = {}) {
|
|
|
148
150
|
if (field.default !== undefined) {
|
|
149
151
|
// Untouched <select> sends "" too; with a default that maps to the
|
|
150
152
|
// default (same semantics as undefined) instead of the invalid-value
|
|
151
|
-
// rejection
|
|
153
|
+
// rejection. A field with a default is never "unset" —
|
|
152
154
|
// true on both insert AND update, so this branch (and its "" → default
|
|
153
155
|
// mapping) fires regardless of applyDefaults; only the `.default(...)`
|
|
154
156
|
// schema-level fallback for OMITTED input is update-gated below.
|
|
155
157
|
// `null` maps the same way: the no-default branch below normalizes
|
|
156
158
|
// an untouched select to null, and a client that reuses that value
|
|
157
|
-
// against a
|
|
159
|
+
// against a defaulted field must not get rejected either.
|
|
158
160
|
const mapped = z.preprocess((value) => (value === "" || value === null ? field.default : value), enumSchema);
|
|
159
161
|
return applyDefaults ? mapped.default(field.default) : mapped;
|
|
160
162
|
}
|
|
@@ -171,9 +173,9 @@ export function fieldToZod(field, currencies, opts = {}) {
|
|
|
171
173
|
const [first, ...rest] = field.options;
|
|
172
174
|
if (!first)
|
|
173
175
|
return z.array(z.string());
|
|
174
|
-
// `required: true`
|
|
175
|
-
//
|
|
176
|
-
// in buildInsertSchema
|
|
176
|
+
// `required: true` means non-empty, as with a `text` field.
|
|
177
|
+
// An empty array is rejected; the global `.optional()` wrapping
|
|
178
|
+
// in buildInsertSchema determines whether the field may be omitted.
|
|
177
179
|
let schema = z.array(z.enum([first, ...rest]));
|
|
178
180
|
if (field.required)
|
|
179
181
|
schema = schema.min(1);
|
|
@@ -209,10 +211,10 @@ export function fieldToZod(field, currencies, opts = {}) {
|
|
|
209
211
|
return field.default !== undefined && applyDefaults ? schema.default(field.default) : schema;
|
|
210
212
|
}
|
|
211
213
|
case "bigInt": {
|
|
212
|
-
// JS-`number
|
|
213
|
-
// safe-integer
|
|
214
|
-
//
|
|
215
|
-
//
|
|
214
|
+
// JS-`number` round-trip via mode:"number"; exact up to 2^53.
|
|
215
|
+
// The safe-integer cap ensures that a caller passing a float (for example,
|
|
216
|
+
// due to a parseFloat bug) fails immediately at insert instead of being
|
|
217
|
+
// silently truncated.
|
|
216
218
|
const schema = z.number().int().safe();
|
|
217
219
|
return field.default !== undefined && applyDefaults ? schema.default(field.default) : schema;
|
|
218
220
|
}
|
|
@@ -267,8 +269,8 @@ export function fieldToZod(field, currencies, opts = {}) {
|
|
|
267
269
|
});
|
|
268
270
|
}
|
|
269
271
|
case "jsonb": {
|
|
270
|
-
// Free-form jsonb — keys
|
|
271
|
-
//
|
|
272
|
+
// Free-form jsonb — keys are tenant-/runtime-defined. Validation
|
|
273
|
+
// passes through any plain object.
|
|
272
274
|
return z.record(z.string(), z.unknown());
|
|
273
275
|
}
|
|
274
276
|
case "date": {
|
|
@@ -276,26 +278,26 @@ export function fieldToZod(field, currencies, opts = {}) {
|
|
|
276
278
|
return withDateBounds(schema, field.min, field.max);
|
|
277
279
|
}
|
|
278
280
|
case "timestamp": {
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
281
|
+
// When locatedBy is set: wall-clock time without an offset (ISO datetime
|
|
282
|
+
// without `Z`). Otherwise: ISO UTC datetime (with `Z`). Both are validated
|
|
283
|
+
// against the ISO-8601 schema through z.iso.datetime; precision (with or
|
|
284
|
+
// without an offset) depends on locatedBy.
|
|
283
285
|
const schema = field.locatedBy !== undefined ? z.iso.datetime({ local: true }) : z.iso.datetime();
|
|
284
286
|
return withDateBounds(schema, field.min, field.max);
|
|
285
287
|
}
|
|
286
288
|
case "tz": {
|
|
287
|
-
// IANA-
|
|
288
|
-
// (isValidIanaTimeZone).
|
|
289
|
-
//
|
|
289
|
+
// IANA time-zone name, validated against the runtime's zone list
|
|
290
|
+
// (isValidIanaTimeZone). An invalid name fails at the write boundary
|
|
291
|
+
// instead of later in ctx.tz.parse / Temporal.
|
|
290
292
|
return z.string().refine(isValidIanaTimeZone, { message: "invalid IANA time zone" });
|
|
291
293
|
}
|
|
292
294
|
case "locatedTimestamp": {
|
|
293
|
-
// Combined
|
|
294
|
-
// { at, tz } (
|
|
295
|
-
// (
|
|
296
|
-
//
|
|
295
|
+
// Combined wall-clock and time-zone object. On write, either
|
|
296
|
+
// { at, tz } (typical for a UI form, with utc calculated) or { utc, tz }
|
|
297
|
+
// (typical for server-to-server communication, with at calculated) is
|
|
298
|
+
// accepted. On read, the read wrapper provides all three fields.
|
|
297
299
|
//
|
|
298
|
-
//
|
|
300
|
+
// The schema guarantees only tz plus at least one of at or utc.
|
|
299
301
|
const at = z.iso.datetime({ local: true });
|
|
300
302
|
const tz = z.string().refine(isValidIanaTimeZone, { message: "invalid IANA time zone" });
|
|
301
303
|
const utc = z.iso.datetime();
|
|
@@ -322,11 +324,12 @@ export function fieldToZod(field, currencies, opts = {}) {
|
|
|
322
324
|
return field.maxCount === undefined ? ids : ids.max(field.maxCount);
|
|
323
325
|
}
|
|
324
326
|
case "reference":
|
|
325
|
-
//
|
|
326
|
-
// (
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
327
|
+
// Validates the UUID shape. Checking that the reference exists
|
|
328
|
+
// (the row exists in the referenced table and belongs to the tenant) is
|
|
329
|
+
// the handler / foreign-key constraint's responsibility, not the schema
|
|
330
|
+
// validator's; checking it here would require a database round trip during
|
|
331
|
+
// parsing.
|
|
332
|
+
// Multi-mode accepts an array of UUIDs.
|
|
330
333
|
return field.multiple === true ? z.array(z.uuid()) : z.uuid();
|
|
331
334
|
default:
|
|
332
335
|
assertUnreachable(field, "field type");
|
|
@@ -342,8 +345,7 @@ export function fieldToZod(field, currencies, opts = {}) {
|
|
|
342
345
|
// fails the check even if the raw minor-unit amounts happen to match.
|
|
343
346
|
//
|
|
344
347
|
// Known limitation: compares against rounded `derived` cells, i.e.
|
|
345
|
-
// "sum-of-rounded" not "round-of-sum"
|
|
346
|
-
// for a computed, read-only sibling total: kumiko-framework#1873.
|
|
348
|
+
// "sum-of-rounded" not "round-of-sum".
|
|
347
349
|
// kumiko-lint-ignore complexity-budget currency-equality check on sibling money payloads
|
|
348
350
|
function applyTotalsMatchRefinements(entity, schema) {
|
|
349
351
|
let result = schema;
|
|
@@ -360,7 +362,7 @@ function applyTotalsMatchRefinements(entity, schema) {
|
|
|
360
362
|
continue;
|
|
361
363
|
// One side of a totalsMatch pair without the other → reject. Update
|
|
362
364
|
// payloads only carry `changes`, so omitting the sibling would otherwise
|
|
363
|
-
// silently leave sum ≠ total
|
|
365
|
+
// silently leave sum ≠ total.
|
|
364
366
|
if (rawRows === undefined || siblingRaw === undefined) {
|
|
365
367
|
ctx.addIssue({
|
|
366
368
|
code: "custom",
|
|
@@ -478,7 +480,7 @@ export function buildUpdateSchema(entity, currencies = [...DEFAULT_CURRENCIES],
|
|
|
478
480
|
// sends only `{ title }` means "only change title"; zod defaults would
|
|
479
481
|
// silently inject default values for every omitted field and clobber
|
|
480
482
|
// existing data via the event-store-executor's `changes` payload.
|
|
481
|
-
// The field is passed through un-stripped
|
|
483
|
+
// The field is passed through un-stripped so
|
|
482
484
|
// fieldToZod still knows the default for its "" → default mapping
|
|
483
485
|
// (e.g. select) — applyDefaults: false only suppresses the schema-level
|
|
484
486
|
// `.default(...)` fallback for a genuinely omitted key.
|
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
// Screen-Filter
|
|
1
|
+
// Screen-Filter — operator-to-field-type compatibility.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// - text/select/multiSelect:
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// - number/money/decimal/date/timestamp/locatedTimestamp: alle 7
|
|
10
|
-
// Ops — die Felder sind natürlich vergleichbar.
|
|
3
|
+
// Allowed operators depend on the field type:
|
|
4
|
+
// - text/select/multiSelect: equality operators only (eq, ne, in). lt/gt
|
|
5
|
+
// on strings are usually accidental; authors can simply omit the filter.
|
|
6
|
+
// - boolean: eq, ne (in/lt/gt are meaningless for a two-value type).
|
|
7
|
+
// - number/money/decimal/date/timestamp/locatedTimestamp: all 7 operators,
|
|
8
|
+
// because these fields are naturally comparable.
|
|
11
9
|
//
|
|
12
|
-
//
|
|
13
|
-
// ("filter
|
|
14
|
-
//
|
|
15
|
-
//
|
|
10
|
+
// The boot validator uses this map to catch authoring errors early
|
|
11
|
+
// ("filter with op:lt on a text field" → boot failure). When adding a field
|
|
12
|
+
// type, add it here and set its sortable/filterable flag in the type definition;
|
|
13
|
+
// otherwise the validator rejects the field entirely.
|
|
16
14
|
const EQUALITY_ONLY = ["eq", "ne", "in"];
|
|
17
15
|
const COMPARABLE = [
|
|
18
16
|
"eq",
|
|
@@ -39,17 +37,16 @@ export function getAllowedFilterOps(field) {
|
|
|
39
37
|
case "timestamp":
|
|
40
38
|
case "locatedTimestamp":
|
|
41
39
|
return COMPARABLE;
|
|
42
|
-
// tz/embedded/file/image/files/images:
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
40
|
+
// tz/embedded/file/image/files/images: not filterable — authors cannot
|
|
41
|
+
// mark the field as filterable: true because the boot validator already
|
|
42
|
+
// rejects `filterable: true` for these types; the flag is not present in
|
|
43
|
+
// their type definitions.
|
|
46
44
|
default:
|
|
47
45
|
return [];
|
|
48
46
|
}
|
|
49
47
|
}
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
// `"filterable" in field`-Narrow ab.
|
|
48
|
+
// If an author marked the field as filterable, catch types without that
|
|
49
|
+
// flag in their type definition with the `"filterable" in field` narrowing.
|
|
53
50
|
export function isFieldFilterable(field) {
|
|
54
51
|
return "filterable" in field && field.filterable === true;
|
|
55
52
|
}
|