@cosmicdrift/kumiko-framework 0.1.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/README.md +159 -0
- package/package.json +91 -0
- package/src/__tests__/anonymous-access.integration.ts +325 -0
- package/src/__tests__/error-contract.integration.ts +435 -0
- package/src/__tests__/field-access.integration.ts +269 -0
- package/src/__tests__/full-stack.integration.ts +914 -0
- package/src/__tests__/ownership.integration.ts +449 -0
- package/src/__tests__/reference-data.integration.ts +198 -0
- package/src/__tests__/transition-guard.integration.ts +340 -0
- package/src/api/__tests__/api.test.ts +337 -0
- package/src/api/__tests__/auth-middleware-transport.test.ts +80 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +179 -0
- package/src/api/__tests__/batch.integration.ts +404 -0
- package/src/api/__tests__/body-limit.test.ts +88 -0
- package/src/api/__tests__/csrf-middleware.test.ts +97 -0
- package/src/api/__tests__/dispatcher-live.integration.ts +216 -0
- package/src/api/__tests__/metrics-endpoint.test.ts +126 -0
- package/src/api/__tests__/nested-write.integration.ts +213 -0
- package/src/api/__tests__/readiness.test.ts +76 -0
- package/src/api/__tests__/request-id-middleware.test.ts +72 -0
- package/src/api/__tests__/sse-broker.test.ts +58 -0
- package/src/api/__tests__/sse-route.test.ts +112 -0
- package/src/api/anonymous-cookie.ts +60 -0
- package/src/api/api-constants.ts +64 -0
- package/src/api/auth-middleware.ts +418 -0
- package/src/api/auth-routes.ts +982 -0
- package/src/api/csrf-middleware.ts +77 -0
- package/src/api/index.ts +31 -0
- package/src/api/jwt.ts +66 -0
- package/src/api/observability-middleware.ts +89 -0
- package/src/api/readiness.ts +132 -0
- package/src/api/request-context.ts +49 -0
- package/src/api/request-id-middleware.ts +50 -0
- package/src/api/route-registrars.ts +195 -0
- package/src/api/routes.ts +135 -0
- package/src/api/server.ts +640 -0
- package/src/api/sse-broker.ts +71 -0
- package/src/api/sse-route.ts +62 -0
- package/src/api/tokens.ts +16 -0
- package/src/db/__tests__/apply-entity-event-tenant.integration.ts +159 -0
- package/src/db/__tests__/compound-types.test.ts +114 -0
- package/src/db/__tests__/connection-options.test.ts +68 -0
- package/src/db/__tests__/cursor.test.ts +41 -0
- package/src/db/__tests__/db-helpers.test.ts +369 -0
- package/src/db/__tests__/dialect-instant.test.ts +50 -0
- package/src/db/__tests__/drizzle-helpers.integration.ts +186 -0
- package/src/db/__tests__/drizzle-table-types.test.ts +162 -0
- package/src/db/__tests__/encryption.test.ts +39 -0
- package/src/db/__tests__/event-store-executor-list.integration.ts +313 -0
- package/src/db/__tests__/event-store-executor.integration.ts +235 -0
- package/src/db/__tests__/implicit-projection-equivalence.integration.ts +304 -0
- package/src/db/__tests__/located-timestamp.test.ts +184 -0
- package/src/db/__tests__/money.test.ts +199 -0
- package/src/db/__tests__/multi-row-insert.integration.ts +76 -0
- package/src/db/__tests__/parse-auto-verb.test.ts +70 -0
- package/src/db/__tests__/required-not-null-migration-safety.integration.ts +105 -0
- package/src/db/__tests__/row-helpers.test.ts +59 -0
- package/src/db/__tests__/schema-migration.integration.ts +273 -0
- package/src/db/__tests__/table-builder-indexes.test.ts +153 -0
- package/src/db/__tests__/table-builder-required.test.ts +216 -0
- package/src/db/__tests__/tenant-db.integration.ts +606 -0
- package/src/db/__tests__/unique-violation-mapping.integration.ts +166 -0
- package/src/db/apply-entity-event.ts +188 -0
- package/src/db/assert-exists-in.ts +59 -0
- package/src/db/compound-types.ts +47 -0
- package/src/db/connection.ts +104 -0
- package/src/db/cursor.ts +83 -0
- package/src/db/dialect.ts +109 -0
- package/src/db/eagerload.ts +174 -0
- package/src/db/encryption.ts +39 -0
- package/src/db/event-store-executor.ts +906 -0
- package/src/db/index.ts +55 -0
- package/src/db/located-timestamp.ts +114 -0
- package/src/db/money.ts +120 -0
- package/src/db/pg-error.ts +46 -0
- package/src/db/reference-data.ts +77 -0
- package/src/db/row-helpers.ts +53 -0
- package/src/db/schema-inspection.ts +25 -0
- package/src/db/table-builder.ts +475 -0
- package/src/db/tenant-db.ts +434 -0
- package/src/engine/__tests__/auth-claims-registrar.test.ts +74 -0
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +108 -0
- package/src/engine/__tests__/boot-validator.test.ts +1865 -0
- package/src/engine/__tests__/build-app-schema.test.ts +154 -0
- package/src/engine/__tests__/claim-keys.test.ts +274 -0
- package/src/engine/__tests__/config-helpers.test.ts +236 -0
- package/src/engine/__tests__/effective-features.test.ts +86 -0
- package/src/engine/__tests__/engine.test.ts +1461 -0
- package/src/engine/__tests__/entity-handlers.test.ts +274 -0
- package/src/engine/__tests__/event-helpers.test.ts +68 -0
- package/src/engine/__tests__/extends-registrar.test.ts +159 -0
- package/src/engine/__tests__/factories-long-text.test.ts +84 -0
- package/src/engine/__tests__/factories-time.test.ts +158 -0
- package/src/engine/__tests__/field-predicates.test.ts +48 -0
- package/src/engine/__tests__/hook-phases.test.ts +132 -0
- package/src/engine/__tests__/identifiers.test.ts +35 -0
- package/src/engine/__tests__/lifecycle-hooks.test.ts +237 -0
- package/src/engine/__tests__/nav.test.ts +267 -0
- package/src/engine/__tests__/ownership.test.ts +421 -0
- package/src/engine/__tests__/parse-ref-target.test.ts +43 -0
- package/src/engine/__tests__/projection-helpers.test.ts +62 -0
- package/src/engine/__tests__/projection.test.ts +191 -0
- package/src/engine/__tests__/qualified-name.test.ts +264 -0
- package/src/engine/__tests__/resolve-config-or-param.test.ts +315 -0
- package/src/engine/__tests__/run-in.test.ts +38 -0
- package/src/engine/__tests__/schema-builder.test.ts +380 -0
- package/src/engine/__tests__/screen.test.ts +408 -0
- package/src/engine/__tests__/state-machine.test.ts +148 -0
- package/src/engine/__tests__/system-user.test.ts +57 -0
- package/src/engine/__tests__/validation-hooks.test.ts +71 -0
- package/src/engine/access.ts +23 -0
- package/src/engine/boot-validator.ts +1528 -0
- package/src/engine/build-app-schema.ts +125 -0
- package/src/engine/config-helpers.ts +115 -0
- package/src/engine/constants.ts +85 -0
- package/src/engine/create-app.ts +98 -0
- package/src/engine/define-feature.ts +702 -0
- package/src/engine/define-handler.ts +78 -0
- package/src/engine/define-roles.ts +19 -0
- package/src/engine/effective-features.ts +87 -0
- package/src/engine/entity-handlers.ts +364 -0
- package/src/engine/event-helpers.ts +73 -0
- package/src/engine/factories.ts +328 -0
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +416 -0
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +197 -0
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +128 -0
- package/src/engine/feature-ast/__tests__/parse.test.ts +888 -0
- package/src/engine/feature-ast/__tests__/patch.test.ts +360 -0
- package/src/engine/feature-ast/__tests__/patcher.test.ts +469 -0
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +287 -0
- package/src/engine/feature-ast/extractors.ts +2562 -0
- package/src/engine/feature-ast/index.ts +105 -0
- package/src/engine/feature-ast/parse.ts +369 -0
- package/src/engine/feature-ast/patch.ts +525 -0
- package/src/engine/feature-ast/patcher.ts +518 -0
- package/src/engine/feature-ast/patterns.ts +434 -0
- package/src/engine/feature-ast/render.ts +602 -0
- package/src/engine/feature-ast/source-location.ts +45 -0
- package/src/engine/field-access.ts +120 -0
- package/src/engine/index.ts +254 -0
- package/src/engine/ownership.ts +337 -0
- package/src/engine/parse-ref-target.ts +22 -0
- package/src/engine/pattern-library/__tests__/library.test.ts +351 -0
- package/src/engine/pattern-library/index.ts +24 -0
- package/src/engine/pattern-library/library.ts +1117 -0
- package/src/engine/pattern-library/types.ts +255 -0
- package/src/engine/projection-helpers.ts +85 -0
- package/src/engine/qualified-name.ts +122 -0
- package/src/engine/read-claim.ts +31 -0
- package/src/engine/registry.ts +1325 -0
- package/src/engine/resolve-config-or-param.ts +153 -0
- package/src/engine/run-in.ts +29 -0
- package/src/engine/schema-builder.ts +175 -0
- package/src/engine/screen-filter-ops.ts +51 -0
- package/src/engine/state-machine.ts +70 -0
- package/src/engine/system-user.ts +32 -0
- package/src/engine/types/config.ts +306 -0
- package/src/engine/types/event-type-map.ts +37 -0
- package/src/engine/types/feature.ts +574 -0
- package/src/engine/types/fields.ts +422 -0
- package/src/engine/types/handlers.ts +742 -0
- package/src/engine/types/hooks.ts +142 -0
- package/src/engine/types/http-route.ts +54 -0
- package/src/engine/types/identifiers.ts +47 -0
- package/src/engine/types/index.ts +208 -0
- package/src/engine/types/nav.ts +46 -0
- package/src/engine/types/projection.ts +132 -0
- package/src/engine/types/relations.ts +51 -0
- package/src/engine/types/screen.ts +452 -0
- package/src/engine/types/workspace.ts +42 -0
- package/src/engine/validation.ts +33 -0
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.ts +173 -0
- package/src/entrypoint/__tests__/split-deploy.integration.ts +297 -0
- package/src/entrypoint/index.ts +442 -0
- package/src/errors/__tests__/classes.test.ts +371 -0
- package/src/errors/__tests__/write-failures.test.ts +109 -0
- package/src/errors/classes.ts +249 -0
- package/src/errors/i18n/de.yaml +83 -0
- package/src/errors/i18n/en.yaml +80 -0
- package/src/errors/index.ts +41 -0
- package/src/errors/kumiko-error.ts +67 -0
- package/src/errors/reasons.ts +36 -0
- package/src/errors/serialize.ts +136 -0
- package/src/errors/transition-details.ts +30 -0
- package/src/errors/write-error-info.ts +123 -0
- package/src/errors/zod-bridge.ts +49 -0
- package/src/event-store/__tests__/admin-api.integration.ts +361 -0
- package/src/event-store/__tests__/event-store.integration.ts +584 -0
- package/src/event-store/__tests__/get-stream-version-perf.integration.ts +83 -0
- package/src/event-store/__tests__/perf.integration.ts +255 -0
- package/src/event-store/__tests__/snapshot.integration.ts +267 -0
- package/src/event-store/__tests__/upcaster-dead-letter.integration.ts +204 -0
- package/src/event-store/__tests__/upcaster.integration.ts +460 -0
- package/src/event-store/admin-api.ts +257 -0
- package/src/event-store/archive.ts +106 -0
- package/src/event-store/errors.ts +35 -0
- package/src/event-store/event-store.ts +405 -0
- package/src/event-store/events-schema.ts +90 -0
- package/src/event-store/index.ts +50 -0
- package/src/event-store/snapshot.ts +210 -0
- package/src/event-store/upcaster-dead-letter.ts +119 -0
- package/src/event-store/upcaster.ts +147 -0
- package/src/files/__tests__/content-disposition.test.ts +123 -0
- package/src/files/__tests__/file-field-column.integration.ts +103 -0
- package/src/files/__tests__/file-field-pipeline.integration.ts +211 -0
- package/src/files/__tests__/file-handle.test.ts +122 -0
- package/src/files/__tests__/files.integration.ts +830 -0
- package/src/files/__tests__/storage-tracking.integration.ts +153 -0
- package/src/files/content-disposition.ts +55 -0
- package/src/files/file-handle.ts +63 -0
- package/src/files/file-ref-table.ts +22 -0
- package/src/files/file-routes.ts +353 -0
- package/src/files/in-memory-provider.ts +62 -0
- package/src/files/index.ts +29 -0
- package/src/files/local-provider.ts +35 -0
- package/src/files/storage-tracking.ts +60 -0
- package/src/files/types.ts +118 -0
- package/src/i18n/__tests__/i18n.test.ts +72 -0
- package/src/i18n/index.ts +29 -0
- package/src/jobs/__tests__/job-event-trigger.integration.ts +172 -0
- package/src/jobs/__tests__/job-multi-trigger.integration.ts +144 -0
- package/src/jobs/__tests__/jobs.integration.ts +566 -0
- package/src/jobs/index.ts +2 -0
- package/src/jobs/job-runner.ts +574 -0
- package/src/lifecycle/__tests__/create-test-lifecycle.ts +19 -0
- package/src/lifecycle/__tests__/lifecycle-server.integration.ts +108 -0
- package/src/lifecycle/__tests__/lifecycle.test.ts +212 -0
- package/src/lifecycle/__tests__/signal-handlers.test.ts +106 -0
- package/src/lifecycle/index.ts +13 -0
- package/src/lifecycle/lifecycle.ts +160 -0
- package/src/lifecycle/signal-handlers.ts +62 -0
- package/src/logging/__tests__/pino-trace-bridge.test.ts +50 -0
- package/src/logging/index.ts +3 -0
- package/src/logging/pino-logger.ts +64 -0
- package/src/logging/types.ts +7 -0
- package/src/migrations/__tests__/compare-snapshots.test.ts +150 -0
- package/src/migrations/__tests__/detect-drift.integration.ts +320 -0
- package/src/migrations/__tests__/detect-projections-to-rebuild.integration.ts +134 -0
- package/src/migrations/__tests__/rebuild-marker.test.ts +79 -0
- package/src/migrations/index.ts +28 -0
- package/src/migrations/projection-detection.ts +149 -0
- package/src/migrations/rebuild-marker.ts +64 -0
- package/src/migrations/schema-drift.ts +395 -0
- package/src/observability/__tests__/console-provider.test.ts +67 -0
- package/src/observability/__tests__/metric-validator.test.ts +87 -0
- package/src/observability/__tests__/noop-provider.test.ts +82 -0
- package/src/observability/__tests__/observability.integration.ts +559 -0
- package/src/observability/__tests__/prometheus-meter.test.ts +144 -0
- package/src/observability/__tests__/recording-meter.test.ts +101 -0
- package/src/observability/__tests__/recording-tracer.test.ts +110 -0
- package/src/observability/__tests__/sensitive-filter.test.ts +98 -0
- package/src/observability/console-provider.ts +130 -0
- package/src/observability/context.ts +26 -0
- package/src/observability/fallback.ts +34 -0
- package/src/observability/ids.ts +25 -0
- package/src/observability/index.ts +79 -0
- package/src/observability/metric-validator.ts +86 -0
- package/src/observability/metrics-handle.ts +56 -0
- package/src/observability/noop-provider.ts +146 -0
- package/src/observability/prometheus-meter.ts +284 -0
- package/src/observability/recording-meter.ts +156 -0
- package/src/observability/recording-tracer.ts +198 -0
- package/src/observability/redis-wrapper.ts +132 -0
- package/src/observability/sensitive-filter.ts +108 -0
- package/src/observability/standard-metrics.ts +213 -0
- package/src/observability/types/index.ts +29 -0
- package/src/observability/types/metric.ts +56 -0
- package/src/observability/types/provider.ts +32 -0
- package/src/observability/types/span.ts +64 -0
- package/src/pipeline/__tests__/archive-stream.integration.ts +220 -0
- package/src/pipeline/__tests__/auth-claims-resolver.test.ts +279 -0
- package/src/pipeline/__tests__/cascade-handler.integration.ts +419 -0
- package/src/pipeline/__tests__/cascade-handler.test.ts +52 -0
- package/src/pipeline/__tests__/causation-chain.integration.ts +206 -0
- package/src/pipeline/__tests__/ctx-bridge.integration.ts +234 -0
- package/src/pipeline/__tests__/dispatcher.test.ts +379 -0
- package/src/pipeline/__tests__/distributed-lock.integration.ts +67 -0
- package/src/pipeline/__tests__/domain-events-projections.integration.ts +323 -0
- package/src/pipeline/__tests__/event-dedup.integration.ts +153 -0
- package/src/pipeline/__tests__/event-define-event-strict.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-lifecycle.integration.ts +220 -0
- package/src/pipeline/__tests__/event-dispatcher-multi-instance.integration.ts +423 -0
- package/src/pipeline/__tests__/event-dispatcher-pg-listen.integration.ts +123 -0
- package/src/pipeline/__tests__/event-dispatcher-recovery.integration.ts +202 -0
- package/src/pipeline/__tests__/event-dispatcher-second-audit.integration.ts +290 -0
- package/src/pipeline/__tests__/event-dispatcher-strict.test.ts +65 -0
- package/src/pipeline/__tests__/event-dispatcher.integration.ts +287 -0
- package/src/pipeline/__tests__/event-retention.integration.ts +239 -0
- package/src/pipeline/__tests__/fetch-for-writing.integration.ts +281 -0
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +430 -0
- package/src/pipeline/__tests__/load-aggregate-query.integration.ts +266 -0
- package/src/pipeline/__tests__/msp-error-mode.integration.ts +149 -0
- package/src/pipeline/__tests__/msp-multi-hop.integration.ts +228 -0
- package/src/pipeline/__tests__/msp-rebuild.integration.ts +368 -0
- package/src/pipeline/__tests__/multi-stream-projection.integration.ts +341 -0
- package/src/pipeline/__tests__/perf-rebuild.integration.ts +147 -0
- package/src/pipeline/__tests__/projection-rebuild.integration.ts +551 -0
- package/src/pipeline/__tests__/query-projection.integration.ts +201 -0
- package/src/pipeline/__tests__/redis-pipeline.integration.ts +306 -0
- package/src/pipeline/append-event-core.ts +117 -0
- package/src/pipeline/auth-claims-resolver.ts +103 -0
- package/src/pipeline/cascade-handler.ts +113 -0
- package/src/pipeline/dispatcher.ts +1585 -0
- package/src/pipeline/distributed-lock.ts +37 -0
- package/src/pipeline/entity-cache.ts +113 -0
- package/src/pipeline/event-consumer-state.ts +108 -0
- package/src/pipeline/event-dedup.ts +23 -0
- package/src/pipeline/event-dispatcher.ts +1016 -0
- package/src/pipeline/event-retention.ts +154 -0
- package/src/pipeline/idempotency.ts +76 -0
- package/src/pipeline/index.ts +66 -0
- package/src/pipeline/lifecycle-pipeline.ts +409 -0
- package/src/pipeline/msp-rebuild.ts +242 -0
- package/src/pipeline/multi-stream-apply-context.ts +115 -0
- package/src/pipeline/projection-rebuild.ts +334 -0
- package/src/pipeline/projection-state.ts +72 -0
- package/src/pipeline/projections-runner.ts +56 -0
- package/src/pipeline/redis-keys.ts +11 -0
- package/src/pipeline/system-hooks.ts +190 -0
- package/src/random/__tests__/generate.test.ts +149 -0
- package/src/random/generate.ts +141 -0
- package/src/random/index.ts +8 -0
- package/src/random/words.ts +392 -0
- package/src/rate-limit/__tests__/dispatcher-l3.integration.ts +111 -0
- package/src/rate-limit/__tests__/middleware.integration.ts +189 -0
- package/src/rate-limit/__tests__/resolver.integration.ts +189 -0
- package/src/rate-limit/bucket.ts +36 -0
- package/src/rate-limit/index.ts +14 -0
- package/src/rate-limit/middleware.ts +152 -0
- package/src/rate-limit/resolver.ts +267 -0
- package/src/redis/__tests__/redis-options.test.ts +54 -0
- package/src/redis/index.ts +74 -0
- package/src/search/__tests__/meilisearch-adapter.integration.ts +236 -0
- package/src/search/__tests__/search-adapter.test.ts +256 -0
- package/src/search/in-memory-adapter.ts +123 -0
- package/src/search/index.ts +12 -0
- package/src/search/meilisearch-adapter.ts +106 -0
- package/src/search/types.ts +39 -0
- package/src/secrets/__tests__/dek-cache.test.ts +213 -0
- package/src/secrets/__tests__/env-master-key-provider.test.ts +119 -0
- package/src/secrets/__tests__/envelope.test.ts +74 -0
- package/src/secrets/__tests__/leak-guard.test.ts +92 -0
- package/src/secrets/__tests__/rotation.test.ts +149 -0
- package/src/secrets/dek-cache.ts +116 -0
- package/src/secrets/env-master-key-provider.ts +162 -0
- package/src/secrets/envelope.ts +55 -0
- package/src/secrets/index.ts +19 -0
- package/src/secrets/leak-guard.ts +87 -0
- package/src/secrets/rotation.ts +34 -0
- package/src/secrets/types.ts +107 -0
- package/src/stack/db.ts +104 -0
- package/src/stack/event-collector.ts +23 -0
- package/src/stack/index.ts +32 -0
- package/src/stack/redis.ts +44 -0
- package/src/stack/request-helper.ts +168 -0
- package/src/stack/table-helpers.ts +104 -0
- package/src/stack/test-stack.ts +357 -0
- package/src/stack/test-users.ts +37 -0
- package/src/testing/__tests__/e2e-generator.test.ts +230 -0
- package/src/testing/__tests__/ensure-entity-table.integration.ts +54 -0
- package/src/testing/access-assertions.ts +15 -0
- package/src/testing/assertions.ts +35 -0
- package/src/testing/e2e-generator.ts +465 -0
- package/src/testing/expect-error.ts +25 -0
- package/src/testing/handler-context.ts +125 -0
- package/src/testing/http-cookies.ts +52 -0
- package/src/testing/index.ts +41 -0
- package/src/testing/late-bound.ts +39 -0
- package/src/testing/mutable-master-key-provider.ts +31 -0
- package/src/testing/observability-recorder.ts +54 -0
- package/src/testing/shared-entities.ts +49 -0
- package/src/testing/utils.ts +1 -0
- package/src/testing/wait-for.ts +31 -0
- package/src/time/__tests__/polyfill.test.ts +73 -0
- package/src/time/__tests__/tz-context.test.ts +121 -0
- package/src/time/index.ts +21 -0
- package/src/time/polyfill.ts +70 -0
- package/src/time/tz-context.ts +107 -0
- package/src/ui-types/app-schema.ts +57 -0
- package/src/ui-types/index.ts +65 -0
- package/src/utils/__tests__/assert.test.ts +17 -0
- package/src/utils/__tests__/env-parse.test.ts +54 -0
- package/src/utils/assert.ts +18 -0
- package/src/utils/env-parse.ts +16 -0
- package/src/utils/ids.ts +16 -0
- package/src/utils/index.ts +5 -0
- package/src/utils/safe-json.ts +30 -0
- package/src/utils/serialization.ts +7 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { Registry } from "./types";
|
|
2
|
+
import type {
|
|
3
|
+
ConfigAccessor,
|
|
4
|
+
ConfigBounds,
|
|
5
|
+
ConfigKeyHandle,
|
|
6
|
+
ConfigKeyType,
|
|
7
|
+
ConfigValue,
|
|
8
|
+
} from "./types/config";
|
|
9
|
+
|
|
10
|
+
// Per-Request Config-Resolver für Routes.
|
|
11
|
+
//
|
|
12
|
+
// Use-case (aus configuration-layers.md, Ebene 7): der Caller will pro
|
|
13
|
+
// Request einen Wert setzen, den Tenant-Admin aber den Max-Bound festlegen.
|
|
14
|
+
// Beispiel: `GET /files/:id/download-url?expiresSeconds=3600` — Client
|
|
15
|
+
// wählt 3600s, aber wenn Tenant-Admin Max=1800 gesetzt hat, clampt der
|
|
16
|
+
// Helper auf 1800.
|
|
17
|
+
//
|
|
18
|
+
// Clamp-Regel: hard-clamp für number mit bounds. Silent — im Gegensatz zu
|
|
19
|
+
// tenant-admin-SET (dort ist reject richtig). Caller hat oft keine Kontrolle
|
|
20
|
+
// über den genauen Wert (voreingestellt im Client-SDK), und ein 422 pro
|
|
21
|
+
// Download-Klick wäre UX-Gift.
|
|
22
|
+
//
|
|
23
|
+
// Fallback-Cascade:
|
|
24
|
+
// 1. paramValue valide → clamp + return
|
|
25
|
+
// 2. paramValue fehlt / nicht parseable → ctx.config(handle)
|
|
26
|
+
//
|
|
27
|
+
// Bei select: nur valide Options werden akzeptiert, sonst Fallback.
|
|
28
|
+
// Bei boolean: "true"/"1" → true, sonst false.
|
|
29
|
+
//
|
|
30
|
+
// ### text-Keys sind gesperrt
|
|
31
|
+
//
|
|
32
|
+
// Per-Request-Overrides für `type="text"` werden HART ABGELEHNT. Grund:
|
|
33
|
+
// Query-Param-Strings können XSS/SQL/Command-Fragmente enthalten, und
|
|
34
|
+
// dieser Helper ist ein *Parser*, kein *Sanitizer*. Ein silent-pass-through
|
|
35
|
+
// wäre ein Footgun — App-Dev würde denken "param ist aktiv" und der
|
|
36
|
+
// unsanitized Wert landet in HTML/SQL/Shell-Kontext.
|
|
37
|
+
//
|
|
38
|
+
// Die Sperre gilt nur beim tatsächlichen Override-Versuch (paramValue
|
|
39
|
+
// gesetzt). Wenn paramValue undefined/null/"" ist, gibt der Helper den
|
|
40
|
+
// Config-Wert zurück — dann liefert die Funktion für text-Keys einfach
|
|
41
|
+
// denselben Wert wie `ctx.config(handle)`.
|
|
42
|
+
//
|
|
43
|
+
// Wer für text pro Request tatsächlich einen Wert akzeptieren will, baut
|
|
44
|
+
// das explizit in der Route mit eigener Escape-Strategie für den Consumer
|
|
45
|
+
// (HTML-Encoder, SQL-Parameter-Binding, Shell-Quoter).
|
|
46
|
+
|
|
47
|
+
type ResolveCtx = {
|
|
48
|
+
readonly config: ConfigAccessor;
|
|
49
|
+
readonly registry: Registry;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Fires for every number-case clamp with the before/after values. Consumers
|
|
53
|
+
// typically wire this into a structured logger or a metric counter:
|
|
54
|
+
// resolveConfigOrParam(ctx, handle, raw, {
|
|
55
|
+
// onClamp: ({ key, original, clamped, max }) =>
|
|
56
|
+
// ctx.logger?.warn("config.clamp", { key, original, clamped, max }),
|
|
57
|
+
// });
|
|
58
|
+
// Without this hook a clamp is invisible — the caller just sees 1000 instead
|
|
59
|
+
// of the 9999 they sent, and debugging becomes guesswork.
|
|
60
|
+
export type ClampInfo = {
|
|
61
|
+
readonly key: string;
|
|
62
|
+
readonly original: number;
|
|
63
|
+
readonly clamped: number;
|
|
64
|
+
readonly min?: number;
|
|
65
|
+
readonly max?: number;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export type ResolveOptions = {
|
|
69
|
+
readonly onClamp?: (info: ClampInfo) => void;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export async function resolveConfigOrParam<T extends ConfigKeyType>(
|
|
73
|
+
ctx: ResolveCtx,
|
|
74
|
+
handle: ConfigKeyHandle<T>,
|
|
75
|
+
paramValue: unknown,
|
|
76
|
+
options?: ResolveOptions,
|
|
77
|
+
): Promise<ConfigValue<T> | undefined> {
|
|
78
|
+
if (paramValue === undefined || paramValue === null || paramValue === "") {
|
|
79
|
+
return ctx.config(handle);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const keyDef = ctx.registry.getConfigKey(handle.name);
|
|
83
|
+
// skip: key isn't in the registry — unlikely because the caller holds a
|
|
84
|
+
// typed handle, but defence-in-depth for hand-built handles.
|
|
85
|
+
if (!keyDef) return ctx.config(handle);
|
|
86
|
+
|
|
87
|
+
// Explicit opt-in required. A config key without `allowPerRequest: true`
|
|
88
|
+
// on its declaration cannot be overridden via a query-param, even if the
|
|
89
|
+
// route-handler forwards one. This is a deny-by-default policy: without
|
|
90
|
+
// it, a future feature-dev could accidentally route a sensitive key
|
|
91
|
+
// (rate-limits, quotas) through a public query-param without noticing.
|
|
92
|
+
if (!keyDef.allowPerRequest) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`resolveConfigOrParam: per-request override not enabled for config key "${handle.name}". Set allowPerRequest: true on the declaration to opt in — or drop the paramValue if the route-handler forwards it by mistake.`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
switch (keyDef.type) {
|
|
99
|
+
case "number": {
|
|
100
|
+
const parsed = typeof paramValue === "number" ? paramValue : Number(paramValue);
|
|
101
|
+
if (Number.isNaN(parsed) || !Number.isFinite(parsed)) {
|
|
102
|
+
return ctx.config(handle);
|
|
103
|
+
}
|
|
104
|
+
const clamped = clampToBounds(parsed, keyDef.bounds);
|
|
105
|
+
// Fire onClamp only when the value actually moved. No bounds + within
|
|
106
|
+
// bounds = silent; crossing a bound = audit event.
|
|
107
|
+
if (clamped !== parsed && options?.onClamp) {
|
|
108
|
+
const info: ClampInfo = {
|
|
109
|
+
key: handle.name,
|
|
110
|
+
original: parsed,
|
|
111
|
+
clamped,
|
|
112
|
+
...(keyDef.bounds?.min !== undefined && { min: keyDef.bounds.min }),
|
|
113
|
+
...(keyDef.bounds?.max !== undefined && { max: keyDef.bounds.max }),
|
|
114
|
+
};
|
|
115
|
+
options.onClamp(info);
|
|
116
|
+
}
|
|
117
|
+
return clamped as ConfigValue<T>; // @cast-boundary engine-bridge
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
case "boolean": {
|
|
121
|
+
if (typeof paramValue === "boolean") return paramValue as ConfigValue<T>; // @cast-boundary engine-bridge
|
|
122
|
+
const str = String(paramValue).toLowerCase();
|
|
123
|
+
return (str === "true" || str === "1") as ConfigValue<T>; // @cast-boundary engine-bridge
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
case "text": {
|
|
127
|
+
// Hard-reject any attempt to override a text key via query-param.
|
|
128
|
+
// See the module-level comment for why this is strict. App-devs
|
|
129
|
+
// that see this error should either (a) remove the paramValue from
|
|
130
|
+
// their route — the config value still flows through ctx.config —
|
|
131
|
+
// or (b) build their own sanitised parser outside this helper.
|
|
132
|
+
throw new Error(
|
|
133
|
+
`resolveConfigOrParam: per-request override is not allowed for type="text" config key "${handle.name}" — query-params would bypass sanitisation. Remove the paramValue or build a feature-specific sanitiser.`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
case "select": {
|
|
138
|
+
const str = String(paramValue);
|
|
139
|
+
if (keyDef.options?.includes(str)) return str as ConfigValue<T>; // @cast-boundary engine-bridge
|
|
140
|
+
// Invalid option → fall back to the configured value rather than 400.
|
|
141
|
+
// The caller is signalling intent; we honour the constraint instead.
|
|
142
|
+
return ctx.config(handle);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function clampToBounds(value: number, bounds: ConfigBounds | undefined): number {
|
|
148
|
+
if (!bounds) return value;
|
|
149
|
+
let v = value;
|
|
150
|
+
if (bounds.min !== undefined && v < bounds.min) v = bounds.min;
|
|
151
|
+
if (bounds.max !== undefined && v > bounds.max) v = bounds.max;
|
|
152
|
+
return v;
|
|
153
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// runIn lane-routing helpers. Tiny module on purpose: both buildServer
|
|
2
|
+
// (MSP-consumer filter) and the boot-validator (Welle 2.6.c coverage
|
|
3
|
+
// check) need the same resolution rule, and duplicating it in two places
|
|
4
|
+
// invites drift — when the default changes, one site gets updated and
|
|
5
|
+
// the other silently doesn't.
|
|
6
|
+
//
|
|
7
|
+
// Resolution rule:
|
|
8
|
+
// - `runIn: undefined` resolves to "worker" — that's the prod default
|
|
9
|
+
// for async work (API instances stay request-focused, heavy async
|
|
10
|
+
// work lives on the worker fleet).
|
|
11
|
+
// - `runIn: "both"` means "eligible on any lane" — SKIP LOCKED on the
|
|
12
|
+
// consumer cursor handles the race between processes that want the
|
|
13
|
+
// same event. Used for cross-lane load-balancing and for MSPs that
|
|
14
|
+
// have no reason to pin to a specific process shape.
|
|
15
|
+
// - `runIn: "api"` / `runIn: "worker"` pin to one lane.
|
|
16
|
+
//
|
|
17
|
+
// processLane describes the CURRENT process's role:
|
|
18
|
+
// - "api" / "worker" — single-role deploy, filter strictly.
|
|
19
|
+
// - "both" — all-in-one, no filtering (one process does it all).
|
|
20
|
+
|
|
21
|
+
import type { RunIn } from "./types";
|
|
22
|
+
|
|
23
|
+
// Does a consumer with `runIn` want to run on a process of the given lane?
|
|
24
|
+
export function runsInLane(runIn: RunIn | undefined, processLane: RunIn): boolean {
|
|
25
|
+
if (processLane === "both") return true;
|
|
26
|
+
const resolved = runIn ?? "worker";
|
|
27
|
+
if (resolved === "both") return true;
|
|
28
|
+
return resolved === processLane;
|
|
29
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { assertUnreachable } from "../utils";
|
|
3
|
+
import type { EmbeddedSubFieldDef, EntityDefinition, FieldDefinition } from "./types";
|
|
4
|
+
import { DEFAULT_CURRENCIES } from "./types";
|
|
5
|
+
|
|
6
|
+
function embeddedSubFieldToZod(subField: EmbeddedSubFieldDef): z.ZodTypeAny {
|
|
7
|
+
switch (subField.type) {
|
|
8
|
+
case "text":
|
|
9
|
+
return subField.required ? z.string().min(1) : z.string();
|
|
10
|
+
case "number":
|
|
11
|
+
return z.number();
|
|
12
|
+
case "boolean":
|
|
13
|
+
return z.boolean();
|
|
14
|
+
case "date":
|
|
15
|
+
return z.string().date();
|
|
16
|
+
default:
|
|
17
|
+
assertUnreachable(subField.type, "embedded sub-field type");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function fieldToZod(field: FieldDefinition, currencies: readonly string[]): z.ZodTypeAny {
|
|
22
|
+
switch (field.type) {
|
|
23
|
+
case "text": {
|
|
24
|
+
let schema = z.string();
|
|
25
|
+
if (field.maxLength) schema = schema.max(field.maxLength);
|
|
26
|
+
if (field.format === "email") schema = schema.email();
|
|
27
|
+
if (field.format === "url") schema = schema.url();
|
|
28
|
+
if (field.required) schema = schema.min(1);
|
|
29
|
+
return field.default !== undefined ? schema.default(field.default) : schema;
|
|
30
|
+
}
|
|
31
|
+
case "longText": {
|
|
32
|
+
// longText hat keine `format`-Variante (per type-design). Nur
|
|
33
|
+
// optional maxLength + required, sonst ein offener z.string().
|
|
34
|
+
let schema = z.string();
|
|
35
|
+
if (field.maxLength) schema = schema.max(field.maxLength);
|
|
36
|
+
if (field.required) schema = schema.min(1);
|
|
37
|
+
return field.default !== undefined ? schema.default(field.default) : schema;
|
|
38
|
+
}
|
|
39
|
+
case "boolean": {
|
|
40
|
+
const schema = z.boolean();
|
|
41
|
+
return field.default !== undefined ? schema.default(field.default) : schema;
|
|
42
|
+
}
|
|
43
|
+
case "select": {
|
|
44
|
+
const [first, ...rest] = field.options;
|
|
45
|
+
if (!first) return z.string();
|
|
46
|
+
const schema = z.enum([first, ...rest]);
|
|
47
|
+
return field.default !== undefined ? schema.default(field.default) : schema;
|
|
48
|
+
}
|
|
49
|
+
case "multiSelect": {
|
|
50
|
+
const [first, ...rest] = field.options;
|
|
51
|
+
if (!first) return z.array(z.string());
|
|
52
|
+
// `required: true` heißt non-empty — Analogie zu `text`-Field.
|
|
53
|
+
// Leeres Array wird rejected; das globale `.optional()`-Wrapping
|
|
54
|
+
// in buildInsertSchema kümmert sich um „darf fehlen".
|
|
55
|
+
let schema = z.array(z.enum([first, ...rest]));
|
|
56
|
+
if (field.required) schema = schema.min(1);
|
|
57
|
+
return field.default !== undefined ? schema.default([...field.default]) : schema;
|
|
58
|
+
}
|
|
59
|
+
case "number": {
|
|
60
|
+
const schema = z.number();
|
|
61
|
+
return field.default !== undefined ? schema.default(field.default) : schema;
|
|
62
|
+
}
|
|
63
|
+
case "money": {
|
|
64
|
+
const [first, ...rest] = currencies;
|
|
65
|
+
if (!first) throw new Error("No currencies configured");
|
|
66
|
+
return z.object({
|
|
67
|
+
amount: z.number(),
|
|
68
|
+
currency: z.enum([first, ...rest]),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
case "embedded": {
|
|
72
|
+
const shape: Record<string, z.ZodTypeAny> = {};
|
|
73
|
+
for (const [subName, subField] of Object.entries(field.schema)) {
|
|
74
|
+
const zodSub = embeddedSubFieldToZod(subField);
|
|
75
|
+
shape[subName] = subField.required ? zodSub : zodSub.optional();
|
|
76
|
+
}
|
|
77
|
+
return z.object(shape);
|
|
78
|
+
}
|
|
79
|
+
case "date": {
|
|
80
|
+
return z.string().date();
|
|
81
|
+
}
|
|
82
|
+
case "timestamp": {
|
|
83
|
+
// Wenn locatedBy gesetzt: Wall-Clock OHNE Offset (ISO-Datetime ohne `Z`).
|
|
84
|
+
// Sonst: ISO-UTC-Datetime (mit `Z`). Beide werden über z.iso.datetime
|
|
85
|
+
// gegen das ISO-8601-Schema validiert; die Präzision (mit/ohne Offset)
|
|
86
|
+
// hängt von locatedBy ab.
|
|
87
|
+
return field.locatedBy !== undefined ? z.iso.datetime({ local: true }) : z.iso.datetime();
|
|
88
|
+
}
|
|
89
|
+
case "tz": {
|
|
90
|
+
// IANA-Zonenname. Validierung gegen Intl.supportedValuesOf("timeZone")
|
|
91
|
+
// ist genau aber teuer (~600 Strings) — wir akzeptieren den freien
|
|
92
|
+
// String und prüfen via try/catch im Boot-Validator (kommt in
|
|
93
|
+
// späterer Iteration).
|
|
94
|
+
return z.string().min(1);
|
|
95
|
+
}
|
|
96
|
+
case "locatedTimestamp": {
|
|
97
|
+
// Combined Wall-Clock+TZ Object. Beim Write akzeptieren wir entweder
|
|
98
|
+
// { at, tz } (typisch UI-Form, utc wird berechnet) oder { utc, tz }
|
|
99
|
+
// (typisch Server-zu-Server, at wird berechnet). Beim Read liefert
|
|
100
|
+
// der Read-Wrapper alle drei Felder (siehe Phase D in MIGRATION.md).
|
|
101
|
+
//
|
|
102
|
+
// Hier nur die Schema-Garantie: mindestens tz + (at ODER utc).
|
|
103
|
+
const at = z.iso.datetime({ local: true });
|
|
104
|
+
const tz = z.string().min(1);
|
|
105
|
+
const utc = z.iso.datetime();
|
|
106
|
+
return z.union([
|
|
107
|
+
z.object({ at, tz, utc: utc.optional() }),
|
|
108
|
+
z.object({ utc, tz, at: at.optional() }),
|
|
109
|
+
]);
|
|
110
|
+
}
|
|
111
|
+
case "file":
|
|
112
|
+
case "image": {
|
|
113
|
+
// Single file: stores a fileRef UUID — must match fileRefsTable.id
|
|
114
|
+
// (uuid column). Pre-fix this was z.number() from an era when the
|
|
115
|
+
// column was (wrongly) integer; the table-builder fix to uuid needs
|
|
116
|
+
// a matching validation-layer fix here or the CRUD pipeline rejects
|
|
117
|
+
// every valid UUID with "expected number".
|
|
118
|
+
return z.uuid();
|
|
119
|
+
}
|
|
120
|
+
case "files":
|
|
121
|
+
case "images": {
|
|
122
|
+
// Multi file: array of fileRef UUIDs. Same story as the singular
|
|
123
|
+
// variant — the element type has to match the UUID column on
|
|
124
|
+
// fileRefsTable.id.
|
|
125
|
+
return z.array(z.uuid());
|
|
126
|
+
}
|
|
127
|
+
case "reference":
|
|
128
|
+
// Tier 2.7e-3: Validiert UUID-shape. Existenz-Check der Reference
|
|
129
|
+
// (Row im referenced Table existiert + Tenant-Scope) ist Server-
|
|
130
|
+
// side-Verantwortung im Handler / Foreign-Key-Constraint, nicht
|
|
131
|
+
// im Schema-Validator (würde sonst Round-trip zur DB beim Parse).
|
|
132
|
+
// Multi-Mode (Tier 2.7e-Multi): Array von UUIDs.
|
|
133
|
+
return field.multiple === true ? z.array(z.uuid()) : z.uuid();
|
|
134
|
+
default:
|
|
135
|
+
assertUnreachable(field, "field type");
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function buildInsertSchema(
|
|
140
|
+
entity: EntityDefinition,
|
|
141
|
+
currencies: readonly string[] = [...DEFAULT_CURRENCIES],
|
|
142
|
+
): z.ZodObject<Record<string, z.ZodTypeAny>> {
|
|
143
|
+
const shape: Record<string, z.ZodTypeAny> = {};
|
|
144
|
+
|
|
145
|
+
for (const [name, field] of Object.entries(entity.fields)) {
|
|
146
|
+
const zodField = fieldToZod(field, currencies);
|
|
147
|
+
const hasDefault = "default" in field && field.default !== undefined;
|
|
148
|
+
const isRequired = "required" in field && field.required === true;
|
|
149
|
+
shape[name] = isRequired || hasDefault ? zodField : zodField.optional();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return z.object(shape);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function buildUpdateSchema(
|
|
156
|
+
entity: EntityDefinition,
|
|
157
|
+
currencies: readonly string[] = [...DEFAULT_CURRENCIES],
|
|
158
|
+
): z.ZodObject<Record<string, z.ZodTypeAny>> {
|
|
159
|
+
const shape: Record<string, z.ZodTypeAny> = {};
|
|
160
|
+
|
|
161
|
+
for (const [name, field] of Object.entries(entity.fields)) {
|
|
162
|
+
// Update schemas never apply defaults — a user that sends only
|
|
163
|
+
// `{ title }` means "only change title"; zod defaults would silently
|
|
164
|
+
// inject default values for every omitted field and clobber existing
|
|
165
|
+
// data via the event-store-executor's `changes` payload.
|
|
166
|
+
// Cast widens the discriminated union so destructure works for variants
|
|
167
|
+
// without a `default` field; remainder is structurally a FieldDefinition.
|
|
168
|
+
const { default: _default, ...stripped } = field as FieldDefinition & {
|
|
169
|
+
default?: unknown;
|
|
170
|
+
};
|
|
171
|
+
shape[name] = fieldToZod(stripped as FieldDefinition, currencies).optional();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return z.object(shape);
|
|
175
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Screen-Filter (Tier 2.7c) — Op-vs-Field-Type-Compatibility.
|
|
2
|
+
//
|
|
3
|
+
// Wer darf was filtern?
|
|
4
|
+
// - text/select/multiSelect: nur equality-Ops (eq, ne, in). lt/gt
|
|
5
|
+
// auf Strings ist semantisch fast immer Tippfehler (lexicographic
|
|
6
|
+
// compare nutzt der Author selten bewusst); Author kann den
|
|
7
|
+
// Filter dann einfach nicht setzen.
|
|
8
|
+
// - boolean: eq, ne (in/lt/gt sinnlos für 2-Werte-Type).
|
|
9
|
+
// - number/money/date/timestamp/locatedTimestamp: alle 5 Ops —
|
|
10
|
+
// die Felder sind natürlich vergleichbar.
|
|
11
|
+
//
|
|
12
|
+
// Boot-Validator nutzt diese Map um Author-Fehler früh zu fangen
|
|
13
|
+
// ("filter mit op:lt auf einem text-Feld" → Boot-Fail). Erweitert sich
|
|
14
|
+
// transparent: neuer Field-Type → hier eintragen + sortable/filterable-
|
|
15
|
+
// Flag im Type-Def, sonst lehnt der Validator das Field generell ab.
|
|
16
|
+
|
|
17
|
+
import type { FieldDefinition, ScreenFilterOp } from "./types";
|
|
18
|
+
|
|
19
|
+
const EQUALITY_ONLY = ["eq", "ne", "in"] as const satisfies readonly ScreenFilterOp[];
|
|
20
|
+
const COMPARABLE = ["eq", "ne", "lt", "gt", "in"] as const satisfies readonly ScreenFilterOp[];
|
|
21
|
+
const BOOL_OPS = ["eq", "ne"] as const satisfies readonly ScreenFilterOp[];
|
|
22
|
+
|
|
23
|
+
export function getAllowedFilterOps(field: FieldDefinition): readonly ScreenFilterOp[] {
|
|
24
|
+
switch (field.type) {
|
|
25
|
+
case "text":
|
|
26
|
+
case "select":
|
|
27
|
+
case "multiSelect":
|
|
28
|
+
return EQUALITY_ONLY;
|
|
29
|
+
case "boolean":
|
|
30
|
+
return BOOL_OPS;
|
|
31
|
+
case "number":
|
|
32
|
+
case "money":
|
|
33
|
+
case "date":
|
|
34
|
+
case "timestamp":
|
|
35
|
+
case "locatedTimestamp":
|
|
36
|
+
return COMPARABLE;
|
|
37
|
+
// tz/embedded/file/image/files/images: nicht filterbar — Author
|
|
38
|
+
// kann das Feld nicht als filterable: true markieren (Boot-
|
|
39
|
+
// Validator weist `filterable: true` auf den Types ohnehin schon
|
|
40
|
+
// ab, weil das Flag dort gar nicht im Type-Def steht).
|
|
41
|
+
default:
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Author hat das Feld als filterable markiert? Tz/Embedded/File-Types
|
|
47
|
+
// haben das Flag gar nicht im Type-Def, also fangen wir das per
|
|
48
|
+
// `"filterable" in field`-Narrow ab.
|
|
49
|
+
export function isFieldFilterable(field: FieldDefinition): boolean {
|
|
50
|
+
return "filterable" in field && field.filterable === true;
|
|
51
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { buildInvalidTransitionDetails, FrameworkReasons, UnprocessableError } from "../errors";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Type-safe transition graph. Wraps the underlying Map so callers don't
|
|
5
|
+
* accidentally fall into the `transitions[from]?.includes(to)` footgun
|
|
6
|
+
* (Object-Index-Access auf einer Map returnt undefined und der ganze
|
|
7
|
+
* Check kollabiert silently). Stattdessen forciert die API explizite
|
|
8
|
+
* Methoden — kein Map-shape mehr extern sichtbar.
|
|
9
|
+
*
|
|
10
|
+
* - `canTransition(from, to)` → boolean
|
|
11
|
+
* - `allowedFrom(from)` → readonly string[] (leer wenn `from` unbekannt
|
|
12
|
+
* oder terminaler Zustand)
|
|
13
|
+
* - `assertTransition(from, to)` → wirft UnprocessableError(invalid_transition)
|
|
14
|
+
*/
|
|
15
|
+
export type TransitionGraph<TStates extends string = string> = {
|
|
16
|
+
readonly canTransition: (from: TStates, to: TStates) => boolean;
|
|
17
|
+
readonly allowedFrom: (from: TStates) => readonly TStates[];
|
|
18
|
+
readonly assertTransition: (from: TStates, to: TStates) => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Defines allowed state transitions. Returns a typed graph (see
|
|
23
|
+
* TransitionGraph) — nicht eine Map. Damit ist `transitions[x]`-Zugriff
|
|
24
|
+
* type-error statt silent undefined.
|
|
25
|
+
*/
|
|
26
|
+
export function defineTransitions<const TMap extends Record<string, readonly string[]>>(
|
|
27
|
+
map: TMap,
|
|
28
|
+
): TransitionGraph<keyof TMap & string> {
|
|
29
|
+
type TStates = keyof TMap & string;
|
|
30
|
+
const internal = new Map<string, ReadonlySet<string>>();
|
|
31
|
+
for (const [from, targets] of Object.entries(map)) {
|
|
32
|
+
internal.set(from, new Set(targets));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
canTransition: (from, to) => internal.get(from)?.has(to) === true,
|
|
37
|
+
allowedFrom: (from) => {
|
|
38
|
+
const set = internal.get(from);
|
|
39
|
+
return set ? ([...set] as TStates[]) : [];
|
|
40
|
+
},
|
|
41
|
+
assertTransition: (from, to) => {
|
|
42
|
+
const set = internal.get(from);
|
|
43
|
+
// skip: erlaubter Übergang — kein Throw, fall through.
|
|
44
|
+
if (set?.has(to) === true) return;
|
|
45
|
+
const allowed = set ? [...set] : [];
|
|
46
|
+
throw new UnprocessableError(FrameworkReasons.invalidTransition, {
|
|
47
|
+
i18nKey: "errors.invalidTransition",
|
|
48
|
+
details: buildInvalidTransitionDetails(from, to, allowed),
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Asserts a state transition is allowed. Throws UnprocessableError with
|
|
56
|
+
* reason="invalid_transition" if not — the 422 status lets the client
|
|
57
|
+
* distinguish a logical rejection from a validation or auth failure.
|
|
58
|
+
*
|
|
59
|
+
* Convenience-Wrapper um `transitions.assertTransition(from, to)` —
|
|
60
|
+
* existiert weil bestehende Aufrufer `guardTransition(graph, ...)`-Form
|
|
61
|
+
* nutzen. Beides ist erlaubt; die Method-Form auf dem Graph ist die
|
|
62
|
+
* idiomatische API für neuen Code.
|
|
63
|
+
*/
|
|
64
|
+
export function guardTransition<TStates extends string>(
|
|
65
|
+
transitions: TransitionGraph<TStates>,
|
|
66
|
+
from: TStates,
|
|
67
|
+
to: TStates,
|
|
68
|
+
): void {
|
|
69
|
+
transitions.assertTransition(from, to);
|
|
70
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import type { SessionUser } from "./types";
|
|
3
|
+
|
|
4
|
+
// Stringified so it round-trips through SessionUser.id (string UUID-shape).
|
|
5
|
+
// Not a real UUID — SYSTEM acts as an alias for "no human caller" and event-
|
|
6
|
+
// store createdBy is text, so the literal suffices.
|
|
7
|
+
export const SYSTEM_USER_ID = "00000000-0000-0000-0000-000000000000";
|
|
8
|
+
export const SYSTEM_ROLE = "system" as const;
|
|
9
|
+
|
|
10
|
+
export function createSystemUser(tenantId: TenantId): SessionUser {
|
|
11
|
+
return {
|
|
12
|
+
id: SYSTEM_USER_ID,
|
|
13
|
+
tenantId,
|
|
14
|
+
roles: [SYSTEM_ROLE],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Anonymous = unauthenticated caller on a public endpoint. id is a stable
|
|
19
|
+
// literal (not a UUID) so audit-trails and event-store rows stay readable —
|
|
20
|
+
// `actor: "anonymous"` is more useful than a random UUID-bucket. Reserved
|
|
21
|
+
// like SYSTEM_ROLE: the boot-validator rejects apps that declare these as
|
|
22
|
+
// custom roles.
|
|
23
|
+
export const ANONYMOUS_USER_ID = "anonymous";
|
|
24
|
+
export const ANONYMOUS_ROLE = "anonymous" as const;
|
|
25
|
+
|
|
26
|
+
export function createAnonymousUser(tenantId: TenantId): SessionUser {
|
|
27
|
+
return {
|
|
28
|
+
id: ANONYMOUS_USER_ID,
|
|
29
|
+
tenantId,
|
|
30
|
+
roles: [ANONYMOUS_ROLE],
|
|
31
|
+
};
|
|
32
|
+
}
|