@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,78 @@
|
|
|
1
|
+
import type { ZodType, z } from "zod";
|
|
2
|
+
import type {
|
|
3
|
+
AccessRule,
|
|
4
|
+
HandlerContext,
|
|
5
|
+
KumikoEventTypeMap,
|
|
6
|
+
QueryEvent,
|
|
7
|
+
RateLimitOption,
|
|
8
|
+
WriteEvent,
|
|
9
|
+
WriteResult,
|
|
10
|
+
} from "./types";
|
|
11
|
+
|
|
12
|
+
// --- Write Handler Definition ---
|
|
13
|
+
//
|
|
14
|
+
// TMap propagates the strict event-type-map through the handler's
|
|
15
|
+
// HandlerContext. CRITICAL: TMap is declared as a generic parameter on the
|
|
16
|
+
// FUNCTION (defineWriteHandler), not just on the type. Generic-functions
|
|
17
|
+
// substitute TMap at the USE-site (the caller's compile context, where
|
|
18
|
+
// the augmentation is visible); generic-type-aliases substitute at the
|
|
19
|
+
// definition-site (framework's compile, where the augmentation isn't
|
|
20
|
+
// visible) and collapse `keyof TMap` to `never`. See the spike-findings
|
|
21
|
+
// memory for the empirical proof.
|
|
22
|
+
|
|
23
|
+
export type WriteHandlerDefinition<
|
|
24
|
+
TName extends string = string,
|
|
25
|
+
TSchema extends ZodType = ZodType,
|
|
26
|
+
TData = unknown,
|
|
27
|
+
TMap extends object = KumikoEventTypeMap,
|
|
28
|
+
> = {
|
|
29
|
+
readonly name: TName;
|
|
30
|
+
readonly schema: TSchema;
|
|
31
|
+
readonly access?: AccessRule;
|
|
32
|
+
readonly skipTransitionGuard?: boolean;
|
|
33
|
+
readonly rateLimit?: RateLimitOption;
|
|
34
|
+
readonly handler: (
|
|
35
|
+
event: WriteEvent<z.infer<TSchema>>,
|
|
36
|
+
context: HandlerContext<TMap>,
|
|
37
|
+
) => Promise<WriteResult<TData>>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export function defineWriteHandler<
|
|
41
|
+
const TName extends string,
|
|
42
|
+
TSchema extends ZodType,
|
|
43
|
+
TData = unknown,
|
|
44
|
+
TMap extends object = KumikoEventTypeMap,
|
|
45
|
+
>(
|
|
46
|
+
def: WriteHandlerDefinition<TName, TSchema, TData, TMap>,
|
|
47
|
+
): WriteHandlerDefinition<TName, TSchema, TData, TMap> {
|
|
48
|
+
return def;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// --- Query Handler Definition ---
|
|
52
|
+
|
|
53
|
+
export type QueryHandlerDefinition<
|
|
54
|
+
TName extends string = string,
|
|
55
|
+
TSchema extends ZodType = ZodType,
|
|
56
|
+
TResult = unknown,
|
|
57
|
+
TMap extends object = KumikoEventTypeMap,
|
|
58
|
+
> = {
|
|
59
|
+
readonly name: TName;
|
|
60
|
+
readonly schema: TSchema;
|
|
61
|
+
readonly access?: AccessRule;
|
|
62
|
+
readonly rateLimit?: RateLimitOption;
|
|
63
|
+
readonly handler: (
|
|
64
|
+
query: QueryEvent<z.infer<TSchema>>,
|
|
65
|
+
context: HandlerContext<TMap>,
|
|
66
|
+
) => Promise<TResult>;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export function defineQueryHandler<
|
|
70
|
+
const TName extends string,
|
|
71
|
+
TSchema extends ZodType,
|
|
72
|
+
TResult = unknown,
|
|
73
|
+
TMap extends object = KumikoEventTypeMap,
|
|
74
|
+
>(
|
|
75
|
+
def: QueryHandlerDefinition<TName, TSchema, TResult, TMap>,
|
|
76
|
+
): QueryHandlerDefinition<TName, TSchema, TResult, TMap> {
|
|
77
|
+
return def;
|
|
78
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Identity function for type-safe role definitions.
|
|
2
|
+
// App defines roles once, all features reference them via the typed object.
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// const roles = defineRoles(["Admin", "SystemAdmin", "Driver"] as const);
|
|
6
|
+
// roles.Admin // "Admin" — autocomplete + type-checked
|
|
7
|
+
// roles.Admni // TS error
|
|
8
|
+
|
|
9
|
+
type RoleMap<T extends readonly string[]> = {
|
|
10
|
+
readonly [K in T[number]]: K;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function defineRoles<const T extends readonly string[]>(roles: T): RoleMap<T> {
|
|
14
|
+
const map = {} as Record<string, string>;
|
|
15
|
+
for (const role of roles) {
|
|
16
|
+
map[role] = role;
|
|
17
|
+
}
|
|
18
|
+
return map as RoleMap<T>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { Registry } from "./types";
|
|
2
|
+
|
|
3
|
+
// Callback that returns the current global-toggle override for a feature.
|
|
4
|
+
// `true` = explicit global row says enabled.
|
|
5
|
+
// `false` = explicit global row says disabled.
|
|
6
|
+
// `undefined` = no row — fall back to the feature's declared toggleableDefault.
|
|
7
|
+
//
|
|
8
|
+
// The feature-toggles bundled feature provides this reader; tests can inject
|
|
9
|
+
// a map-backed stub. The reader is called once per feature during compute;
|
|
10
|
+
// callers that fetch toggles from a DB should batch-load upfront and expose
|
|
11
|
+
// a Map lookup to keep compute() allocation-light.
|
|
12
|
+
export type ToggleReader = (featureName: string) => boolean | undefined;
|
|
13
|
+
|
|
14
|
+
// Compute the set of effectively-enabled features for the current call.
|
|
15
|
+
//
|
|
16
|
+
// Rules (AND-combined, any false wins):
|
|
17
|
+
// 1. Always-on: feature without r.toggleable() → enabled, ignores overrides.
|
|
18
|
+
// 2. Toggleable: enabled = (globalOverride ?? toggleableDefault).
|
|
19
|
+
// 3. Cascade: a feature is only effectively enabled if ALL its r.requires()
|
|
20
|
+
// targets are effectively enabled. Applied transitively.
|
|
21
|
+
//
|
|
22
|
+
// Cascade semantics note: a NON-toggleable feature A that requires a
|
|
23
|
+
// toggleable feature B becomes effectively disabled when B is off. This is
|
|
24
|
+
// intentional — running A's handlers/hooks without its declared dependency
|
|
25
|
+
// would be a worse failure mode than gating A. Ops documentation must call
|
|
26
|
+
// this out so disabling "leaf" features doesn't surprise anyone.
|
|
27
|
+
//
|
|
28
|
+
// The result is a plain Set for O(1) `has(name)` checks in the dispatcher
|
|
29
|
+
// gate, hook filter, and MSP runner. Cycle-safety is delegated to the
|
|
30
|
+
// registry's existing boot-validation (cycles are rejected there).
|
|
31
|
+
export function computeEffectiveFeatures(
|
|
32
|
+
registry: Registry,
|
|
33
|
+
readToggle: ToggleReader,
|
|
34
|
+
): ReadonlySet<string> {
|
|
35
|
+
// Raw enablement, before cascade.
|
|
36
|
+
const raw = new Map<string, boolean>();
|
|
37
|
+
for (const feature of registry.features.values()) {
|
|
38
|
+
if (feature.toggleableDefault === undefined) {
|
|
39
|
+
raw.set(feature.name, true);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const override = readToggle(feature.name);
|
|
43
|
+
raw.set(feature.name, override ?? feature.toggleableDefault);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Transitive cascade via DFS with memoization. Cycles are already rejected
|
|
47
|
+
// at boot, so no cycle-breaking is needed here.
|
|
48
|
+
const effective = new Map<string, boolean>();
|
|
49
|
+
|
|
50
|
+
function resolve(name: string): boolean {
|
|
51
|
+
const cached = effective.get(name);
|
|
52
|
+
if (cached !== undefined) return cached;
|
|
53
|
+
|
|
54
|
+
const rawEnabled = raw.get(name) ?? true;
|
|
55
|
+
if (!rawEnabled) {
|
|
56
|
+
effective.set(name, false);
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const feature = registry.getFeature(name);
|
|
61
|
+
// Feature referenced by requires() but not loaded — registry boot should
|
|
62
|
+
// have caught this, but be defensive: treat missing deps as disabled
|
|
63
|
+
// (surfaces the same behaviour as "dep is off", not a silent pass).
|
|
64
|
+
if (!feature) {
|
|
65
|
+
effective.set(name, false);
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const dep of feature.requires) {
|
|
70
|
+
if (!resolve(dep)) {
|
|
71
|
+
effective.set(name, false);
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
effective.set(name, true);
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
for (const feature of registry.features.values()) resolve(feature.name);
|
|
81
|
+
|
|
82
|
+
const result = new Set<string>();
|
|
83
|
+
for (const [name, enabled] of effective) {
|
|
84
|
+
if (enabled) result.add(name);
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
import { type ZodType, z } from "zod";
|
|
2
|
+
import type { DbRow } from "../db/connection";
|
|
3
|
+
import type { TableColumns } from "../db/dialect";
|
|
4
|
+
import {
|
|
5
|
+
collectReferenceFields,
|
|
6
|
+
enrichRowWithReferences,
|
|
7
|
+
enrichWithReferences,
|
|
8
|
+
} from "../db/eagerload";
|
|
9
|
+
import { createEventStoreExecutor, type EventStoreExecutor } from "../db/event-store-executor";
|
|
10
|
+
import { buildDrizzleTable } from "../db/table-builder";
|
|
11
|
+
import { assertUnreachable } from "../utils";
|
|
12
|
+
import { buildInsertSchema, buildUpdateSchema } from "./schema-builder";
|
|
13
|
+
import type { AccessRule, EntityDefinition, QueryHandlerDef, WriteHandlerDef } from "./types";
|
|
14
|
+
|
|
15
|
+
// Convention-based handler factories for event-sourced aggregates.
|
|
16
|
+
//
|
|
17
|
+
// You register one handler per call (no auto-generation of a whole CRUD set),
|
|
18
|
+
// but the Schema and the executor body are inferred from the entity + verb.
|
|
19
|
+
// Pick the verbs you need — leave out the ones you don't.
|
|
20
|
+
//
|
|
21
|
+
// Two API shapes — pick one per project, don't mix:
|
|
22
|
+
//
|
|
23
|
+
// PREFERRED — one function per verb, type-safe, no magic strings:
|
|
24
|
+
// r.writeHandler(defineEntityCreateHandler("note", noteEntity, { access }))
|
|
25
|
+
// r.writeHandler(defineEntityUpdateHandler("note", noteEntity, { access }))
|
|
26
|
+
// r.writeHandler(defineEntityDeleteHandler("note", noteEntity, { access }))
|
|
27
|
+
// r.queryHandler(defineEntityListHandler("note", noteEntity, { access }))
|
|
28
|
+
// r.queryHandler(defineEntityDetailHandler("note", noteEntity, { access }))
|
|
29
|
+
//
|
|
30
|
+
// LEGACY — single function with verb in the name-string. Kept for
|
|
31
|
+
// backwards-compat; existing apps work as before. New code should use
|
|
32
|
+
// the verb-specific factories above:
|
|
33
|
+
// r.writeHandler(defineEntityCreateHandler("note", noteEntity, { access }))
|
|
34
|
+
// r.queryHandler(defineEntityDetailHandler("note", noteEntity, { access }))
|
|
35
|
+
//
|
|
36
|
+
// For custom logic (default values, business rules, side effects, custom
|
|
37
|
+
// executors with ctx.searchAdapter, ...) write the handler explicitly with
|
|
38
|
+
// r.writeHandler / r.queryHandler — these helpers cover the standard path only.
|
|
39
|
+
//
|
|
40
|
+
// Note on the `as` casts in the handler bodies: WriteHandlerDef.handler's
|
|
41
|
+
// payload type is `unknown` because the dispatcher hands the parsed payload
|
|
42
|
+
// through a runtime-only boundary. Each verb knows its post-parse shape (the
|
|
43
|
+
// schema we just built two lines up enforces it), so the casts are a
|
|
44
|
+
// localised re-declaration of that shape rather than a narrowing escape.
|
|
45
|
+
|
|
46
|
+
const WRITE_VERBS = ["create", "update", "delete", "restore"] as const;
|
|
47
|
+
const QUERY_VERBS = ["list", "detail"] as const;
|
|
48
|
+
|
|
49
|
+
type UpdatePayload = { id: string; version: number; changes: Record<string, unknown> };
|
|
50
|
+
type IdPayload = { id: string };
|
|
51
|
+
type ListPayload = {
|
|
52
|
+
cursor?: string;
|
|
53
|
+
limit?: number;
|
|
54
|
+
search?: string;
|
|
55
|
+
sort?: string;
|
|
56
|
+
sortDirection?: "asc" | "desc";
|
|
57
|
+
// Page-based Pagination: offset 0-basiert, mit limit zusammen statt
|
|
58
|
+
// cursor genutzt. Cursor ist für infinite-scroll / live-tailing
|
|
59
|
+
// präziser; offset ist für klassische Pager (← 1 2 ... N →) wo der
|
|
60
|
+
// User direkt zu "page 7" springen will. Nur EINE Variante pro
|
|
61
|
+
// Request — wenn beide gesetzt sind, gewinnt cursor (DB-stabil).
|
|
62
|
+
offset?: number;
|
|
63
|
+
// Wenn true, liefert der executor zusätzlich eine `total`-Zahl im
|
|
64
|
+
// Response. Extra-Roundtrip auf der DB (COUNT(*)), nur dann sinnvoll
|
|
65
|
+
// wenn der Pager "Page X of Y" rendern muss. Infinite-Scroll oder
|
|
66
|
+
// unbedingte Lists lassen das weg um die COUNT-Kosten zu sparen.
|
|
67
|
+
totalCount?: boolean;
|
|
68
|
+
// Screen-Level Filter (Tier 2.7c) — Author-deklarierter, server-side
|
|
69
|
+
// applizierter WHERE-Clause. Drei Buckets der selben Entity ohne
|
|
70
|
+
// Custom-Pages: jedes Screen hat sein eigenes filter, alle nutzen
|
|
71
|
+
// den gleichen Query-Handler.
|
|
72
|
+
filter?: {
|
|
73
|
+
readonly field: string;
|
|
74
|
+
readonly op: "eq" | "ne" | "lt" | "gt" | "in";
|
|
75
|
+
readonly value: unknown;
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const idSchema = z.object({ id: z.uuid() });
|
|
80
|
+
const listSchema = z.object({
|
|
81
|
+
cursor: z.string().optional(),
|
|
82
|
+
limit: z.number().optional(),
|
|
83
|
+
search: z.string().optional(),
|
|
84
|
+
sort: z.string().optional(),
|
|
85
|
+
sortDirection: z.enum(["asc", "desc"]).optional(),
|
|
86
|
+
offset: z.number().int().nonnegative().optional(),
|
|
87
|
+
totalCount: z.boolean().optional(),
|
|
88
|
+
filter: z
|
|
89
|
+
.object({
|
|
90
|
+
field: z.string(),
|
|
91
|
+
op: z.enum(["eq", "ne", "lt", "gt", "in"]),
|
|
92
|
+
// Value ist `unknown` zur Compile-Zeit; Server-Side prüft beim
|
|
93
|
+
// Build der WHERE-Clause ob der Type zum Field passt. z.unknown()
|
|
94
|
+
// lässt alles durch; Type-Check kommt im executor.list.
|
|
95
|
+
value: z.unknown(),
|
|
96
|
+
})
|
|
97
|
+
.optional(),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
function parseHandlerName<TVerb extends string>(
|
|
101
|
+
name: string,
|
|
102
|
+
validVerbs: readonly TVerb[],
|
|
103
|
+
): { entityName: string; verb: TVerb } {
|
|
104
|
+
const colonIdx = name.indexOf(":");
|
|
105
|
+
if (colonIdx < 0) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`Handler name "${name}" must use the "<entity>:<verb>" pattern (e.g. "note:create").`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const entityName = name.slice(0, colonIdx);
|
|
111
|
+
const verbCandidate = name.slice(colonIdx + 1);
|
|
112
|
+
if (!entityName) {
|
|
113
|
+
throw new Error(`Handler name "${name}" is missing the entity part before the colon.`);
|
|
114
|
+
}
|
|
115
|
+
// @cast-boundary engine-bridge — verbCandidate validated against validVerbs union
|
|
116
|
+
if (!(validVerbs as readonly string[]).includes(verbCandidate)) {
|
|
117
|
+
throw new Error(
|
|
118
|
+
`Unknown verb "${verbCandidate}" in handler name "${name}". Standard verbs: ${validVerbs.join("/")}. For custom verbs use the explicit r.writeHandler / r.queryHandler form.`,
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return { entityName, verb: verbCandidate as TVerb }; // @cast-boundary engine-bridge
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function defineEntityWriteHandler(
|
|
125
|
+
name: string,
|
|
126
|
+
entity: EntityDefinition,
|
|
127
|
+
options?: { access?: AccessRule },
|
|
128
|
+
): WriteHandlerDef {
|
|
129
|
+
const { entityName, verb } = parseHandlerName(name, WRITE_VERBS);
|
|
130
|
+
if (verb === "restore" && !entity.softDelete) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`"${name}": restore is only valid for entities declared with softDelete: true.`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const table = buildDrizzleTable(entityName, entity);
|
|
137
|
+
const executor = createEventStoreExecutor(table, entity, { entityName });
|
|
138
|
+
|
|
139
|
+
let schema: ZodType;
|
|
140
|
+
let handler: WriteHandlerDef["handler"];
|
|
141
|
+
|
|
142
|
+
switch (verb) {
|
|
143
|
+
case "create":
|
|
144
|
+
schema = buildInsertSchema(entity);
|
|
145
|
+
handler = async (event, ctx) => executor.create(event.payload as DbRow, event.user, ctx.db);
|
|
146
|
+
break;
|
|
147
|
+
case "update":
|
|
148
|
+
schema = z.object({
|
|
149
|
+
id: z.uuid(),
|
|
150
|
+
version: z.number(),
|
|
151
|
+
changes: buildUpdateSchema(entity),
|
|
152
|
+
});
|
|
153
|
+
handler = async (event, ctx) =>
|
|
154
|
+
executor.update(event.payload as UpdatePayload, event.user, ctx.db);
|
|
155
|
+
break;
|
|
156
|
+
case "delete":
|
|
157
|
+
schema = idSchema;
|
|
158
|
+
handler = async (event, ctx) =>
|
|
159
|
+
executor.delete(event.payload as IdPayload, event.user, ctx.db);
|
|
160
|
+
break;
|
|
161
|
+
case "restore":
|
|
162
|
+
schema = idSchema;
|
|
163
|
+
handler = async (event, ctx) =>
|
|
164
|
+
executor.restore(event.payload as IdPayload, event.user, ctx.db);
|
|
165
|
+
break;
|
|
166
|
+
default:
|
|
167
|
+
assertUnreachable(verb, "write verb");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
name,
|
|
172
|
+
schema,
|
|
173
|
+
handler,
|
|
174
|
+
...(options?.access && { access: options.access }),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function defineEntityQueryHandler(
|
|
179
|
+
name: string,
|
|
180
|
+
entity: EntityDefinition,
|
|
181
|
+
options?: { access?: AccessRule },
|
|
182
|
+
): QueryHandlerDef {
|
|
183
|
+
const { entityName, verb } = parseHandlerName(name, QUERY_VERBS);
|
|
184
|
+
|
|
185
|
+
const table = buildDrizzleTable(entityName, entity);
|
|
186
|
+
const executor = createEventStoreExecutor(table, entity, { entityName });
|
|
187
|
+
|
|
188
|
+
let schema: ZodType;
|
|
189
|
+
let handler: QueryHandlerDef["handler"];
|
|
190
|
+
|
|
191
|
+
// Tier 2.7e Server-Eagerload: wenn die entity reference-Felder hat,
|
|
192
|
+
// resolved der handler nach dem Haupt-Query die UUIDs gegen die
|
|
193
|
+
// referenced entities. Das `_refs`-Property landet auf jeder Row;
|
|
194
|
+
// Renderer-Side useReferenceLookup bleibt als Fallback bestehen
|
|
195
|
+
// (für Apps die manuell Custom-Handler schreiben ohne diesen
|
|
196
|
+
// Wrapper zu nutzen).
|
|
197
|
+
const hasRefFields = collectReferenceFields(entity).length > 0;
|
|
198
|
+
|
|
199
|
+
switch (verb) {
|
|
200
|
+
case "list":
|
|
201
|
+
schema = listSchema;
|
|
202
|
+
handler = async (query, ctx) => {
|
|
203
|
+
// Tier 2.7e Audit-Fix: SearchAdapter aus ctx durchreichen,
|
|
204
|
+
// damit payload.search zur Laufzeit gegen Meilisearch/InMem
|
|
205
|
+
// läuft (Remote-Combobox-Search). Der executor wird beim
|
|
206
|
+
// Definition-Time gebaut, kennt den Adapter also nicht —
|
|
207
|
+
// Runtime-Override holt das.
|
|
208
|
+
const result = await executor.list(query.payload as ListPayload, query.user, ctx.db, {
|
|
209
|
+
...(ctx.searchAdapter !== undefined && { searchAdapter: ctx.searchAdapter }),
|
|
210
|
+
});
|
|
211
|
+
if (!hasRefFields) return result;
|
|
212
|
+
const enrichedRows = await enrichWithReferences(
|
|
213
|
+
result.rows,
|
|
214
|
+
entity,
|
|
215
|
+
(name) => ctx.registry.getEntity(name),
|
|
216
|
+
ctx.db,
|
|
217
|
+
);
|
|
218
|
+
return { ...result, rows: enrichedRows };
|
|
219
|
+
};
|
|
220
|
+
break;
|
|
221
|
+
case "detail":
|
|
222
|
+
schema = idSchema;
|
|
223
|
+
handler = async (query, ctx) => {
|
|
224
|
+
const row = await executor.detail(query.payload as IdPayload, query.user, ctx.db);
|
|
225
|
+
if (row === null || !hasRefFields) return row;
|
|
226
|
+
return enrichRowWithReferences(row, entity, (name) => ctx.registry.getEntity(name), ctx.db);
|
|
227
|
+
};
|
|
228
|
+
break;
|
|
229
|
+
default:
|
|
230
|
+
assertUnreachable(verb, "query verb");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
name,
|
|
235
|
+
schema,
|
|
236
|
+
handler,
|
|
237
|
+
...(options?.access && { access: options.access }),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ─── Verb-specific factories (preferred) ──────────────────────────────
|
|
242
|
+
//
|
|
243
|
+
// One function per verb — verb is the function name, no magic-string
|
|
244
|
+
// parsing. Each delegates to the legacy `defineEntityWriteHandler` /
|
|
245
|
+
// `defineEntityQueryHandler` with a fixed verb-suffix; the schema +
|
|
246
|
+
// handler-body logic is unchanged. Migration from the legacy API is a
|
|
247
|
+
// 1:1 rename — same arguments minus the verb-prefix in the name-string.
|
|
248
|
+
//
|
|
249
|
+
// Why prefer these over the legacy form:
|
|
250
|
+
// - Verb is checked at compile-time (no runtime "Unknown verb" throw)
|
|
251
|
+
// - IDE auto-completes the four/two verbs after typing `defineEntity`
|
|
252
|
+
// - Function name is self-documenting; no comment needed to explain
|
|
253
|
+
// - Entity-name appears once (used to be doubled: once in the string,
|
|
254
|
+
// once as the entity-arg)
|
|
255
|
+
//
|
|
256
|
+
// Restore-specific note: defineEntityRestoreHandler still validates at
|
|
257
|
+
// runtime that entity.softDelete === true. A compile-time-only check
|
|
258
|
+
// would need a Branded-EntityDefinition with `softDelete: true` literal
|
|
259
|
+
// — feasible but not yet wired; the runtime guard catches misuse.
|
|
260
|
+
|
|
261
|
+
type EntityHandlerOptions = { readonly access?: AccessRule };
|
|
262
|
+
|
|
263
|
+
export function defineEntityCreateHandler(
|
|
264
|
+
entityName: string,
|
|
265
|
+
entity: EntityDefinition,
|
|
266
|
+
options?: EntityHandlerOptions,
|
|
267
|
+
): WriteHandlerDef {
|
|
268
|
+
return defineEntityWriteHandler(`${entityName}:create`, entity, options);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function defineEntityUpdateHandler(
|
|
272
|
+
entityName: string,
|
|
273
|
+
entity: EntityDefinition,
|
|
274
|
+
options?: EntityHandlerOptions,
|
|
275
|
+
): WriteHandlerDef {
|
|
276
|
+
return defineEntityWriteHandler(`${entityName}:update`, entity, options);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function defineEntityDeleteHandler(
|
|
280
|
+
entityName: string,
|
|
281
|
+
entity: EntityDefinition,
|
|
282
|
+
options?: EntityHandlerOptions,
|
|
283
|
+
): WriteHandlerDef {
|
|
284
|
+
return defineEntityWriteHandler(`${entityName}:delete`, entity, options);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function defineEntityRestoreHandler(
|
|
288
|
+
entityName: string,
|
|
289
|
+
entity: EntityDefinition,
|
|
290
|
+
options?: EntityHandlerOptions,
|
|
291
|
+
): WriteHandlerDef {
|
|
292
|
+
return defineEntityWriteHandler(`${entityName}:restore`, entity, options);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function defineEntityListHandler(
|
|
296
|
+
entityName: string,
|
|
297
|
+
entity: EntityDefinition,
|
|
298
|
+
options?: EntityHandlerOptions,
|
|
299
|
+
): QueryHandlerDef {
|
|
300
|
+
return defineEntityQueryHandler(`${entityName}:list`, entity, options);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export function defineEntityDetailHandler(
|
|
304
|
+
entityName: string,
|
|
305
|
+
entity: EntityDefinition,
|
|
306
|
+
options?: EntityHandlerOptions,
|
|
307
|
+
): QueryHandlerDef {
|
|
308
|
+
return defineEntityQueryHandler(`${entityName}:detail`, entity, options);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// biome-ignore lint/suspicious/noExplicitAny: Drizzle dynamic table — erased on purpose, same as db/event-store-executor.ts does.
|
|
312
|
+
type AnyTable = TableColumns<any>;
|
|
313
|
+
|
|
314
|
+
// Bundle the two calls every custom write-handler opens with: build the
|
|
315
|
+
// Drizzle table from the entity, then wire an event-store executor onto it.
|
|
316
|
+
// The pair is identical in every sample that hand-writes handlers, so the
|
|
317
|
+
// helper collapses 3-4 lines + the { entityName } bookkeeping into one.
|
|
318
|
+
//
|
|
319
|
+
// const { table, executor } = createEntityExecutor("counter", counterEntity);
|
|
320
|
+
//
|
|
321
|
+
// Keep using the explicit buildDrizzleTable / createEventStoreExecutor duo
|
|
322
|
+
// when you need search-adapter / entity-cache options on the executor — this
|
|
323
|
+
// helper covers the zero-config case.
|
|
324
|
+
export function createEntityExecutor(
|
|
325
|
+
entityName: string,
|
|
326
|
+
entity: EntityDefinition,
|
|
327
|
+
): { readonly table: AnyTable; readonly executor: EventStoreExecutor } {
|
|
328
|
+
const table = buildDrizzleTable(entityName, entity);
|
|
329
|
+
const executor = createEventStoreExecutor(table, entity, { entityName });
|
|
330
|
+
return { table, executor };
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Wrap a projection read into a zero-argument query handler. Use when the
|
|
334
|
+
// read is "give me all rows from projection X, tenant-scoped" — the common
|
|
335
|
+
// shape for list-views backed by an MSP/projection table.
|
|
336
|
+
//
|
|
337
|
+
// r.queryHandler(
|
|
338
|
+
// defineProjectionQueryHandler("revenue:list", "showcase:projection:customer-revenue", {
|
|
339
|
+
// access: { openToAll: true },
|
|
340
|
+
// }),
|
|
341
|
+
// );
|
|
342
|
+
//
|
|
343
|
+
// For anything more involved (filters, joins, custom shaping), write the
|
|
344
|
+
// query-handler explicitly with ctx.queryProjection or a raw select.
|
|
345
|
+
export function defineProjectionQueryHandler(
|
|
346
|
+
name: string,
|
|
347
|
+
projectionQualifiedName: string,
|
|
348
|
+
options?: { access?: AccessRule; allTenants?: boolean },
|
|
349
|
+
): QueryHandlerDef {
|
|
350
|
+
return {
|
|
351
|
+
name,
|
|
352
|
+
schema: z.object({}),
|
|
353
|
+
// Returns the raw row array — matches ctx.queryProjection's shape so the
|
|
354
|
+
// helper is a drop-in for the inline `async (_q, ctx) => ctx.queryProjection(...)`
|
|
355
|
+
// handler. Wrap the result in the caller's handler when you need
|
|
356
|
+
// pagination envelopes or added metadata.
|
|
357
|
+
handler: async (_query, ctx) =>
|
|
358
|
+
ctx.queryProjection(
|
|
359
|
+
projectionQualifiedName,
|
|
360
|
+
options?.allTenants ? { allTenants: true } : undefined,
|
|
361
|
+
),
|
|
362
|
+
...(options?.access && { access: options.access }),
|
|
363
|
+
};
|
|
364
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { AppendEventArgs, EventDef, HandlerContext } from "./types/handlers";
|
|
2
|
+
|
|
3
|
+
// The ctx-surface emitEvent needs. Accepting the narrow shape lets tests or
|
|
4
|
+
// MultiStreamApplyContext-style callers pass their own appendEvent without
|
|
5
|
+
// a full HandlerContext. Real handlers just pass `ctx`.
|
|
6
|
+
//
|
|
7
|
+
// Uses appendEventUnsafe internally because EventDef's TPayload comes from
|
|
8
|
+
// a runtime-defined zod-schema — emitEvent does the type-check itself via
|
|
9
|
+
// the EventDef generic, so it doesn't need the strict KumikoEventTypeMap
|
|
10
|
+
// path. The strict appendEvent is for direct in-handler callsites.
|
|
11
|
+
export type EmitCtx = Pick<HandlerContext, "appendEventUnsafe">;
|
|
12
|
+
|
|
13
|
+
// Typed wrapper around ctx.appendEvent. Two wins over the raw call:
|
|
14
|
+
//
|
|
15
|
+
// 1. The payload is checked against the EventDef's inferred TPayload,
|
|
16
|
+
// so a Zod-schema mismatch becomes a compile error at the emit site
|
|
17
|
+
// rather than a runtime reject from the event-store append.
|
|
18
|
+
// 2. The event name is carried by the def — no hand-typed
|
|
19
|
+
// "<feature>:event:<short>" string, no typos.
|
|
20
|
+
//
|
|
21
|
+
// await emitEvent(ctx, orderPlaced, {
|
|
22
|
+
// aggregateId: String(result.data.id),
|
|
23
|
+
// aggregateType: "pubsub-order",
|
|
24
|
+
// payload: { id, customer, product },
|
|
25
|
+
// });
|
|
26
|
+
//
|
|
27
|
+
// aggregateType stays explicit on purpose — the EventDef doesn't know which
|
|
28
|
+
// aggregate owns an event (cross-feature reuse is legal). Use the raw
|
|
29
|
+
// ctx.appendEvent when the event name is computed at runtime.
|
|
30
|
+
export async function emitEvent<TPayload>(
|
|
31
|
+
ctx: EmitCtx,
|
|
32
|
+
eventDef: EventDef<TPayload>,
|
|
33
|
+
args: {
|
|
34
|
+
readonly aggregateId: string;
|
|
35
|
+
readonly aggregateType: string;
|
|
36
|
+
readonly payload: TPayload;
|
|
37
|
+
},
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
const appendArgs: AppendEventArgs = {
|
|
40
|
+
aggregateId: args.aggregateId,
|
|
41
|
+
aggregateType: args.aggregateType,
|
|
42
|
+
type: eventDef.name,
|
|
43
|
+
payload: args.payload,
|
|
44
|
+
};
|
|
45
|
+
await ctx.appendEventUnsafe(appendArgs);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Read-side counterpart: narrow a StoredEvent's `payload` (declared as
|
|
49
|
+
// `Record<string, unknown>` because a stream carries many event types) to
|
|
50
|
+
// the EventDef's inferred TPayload. Replaces the scattered
|
|
51
|
+
// `event.payload as { ... }` casts inside projection-apply handlers and
|
|
52
|
+
// reducers — the cast is named, the shape comes from a single source
|
|
53
|
+
// (the defineEvent() schema), and a mismatched event-type throws loudly.
|
|
54
|
+
//
|
|
55
|
+
// const p = typedPayload(event, approved);
|
|
56
|
+
// // p is typed as { amountCents: number; approvedBy: string }
|
|
57
|
+
//
|
|
58
|
+
// The runtime type-check guards against projection-apply maps that hand-
|
|
59
|
+
// build their key → handler mapping and accidentally route events to the
|
|
60
|
+
// wrong typedPayload call. No Zod-parse — validation happened at
|
|
61
|
+
// appendEvent time; this is a read-path helper.
|
|
62
|
+
export function typedPayload<TPayload>(
|
|
63
|
+
event: { readonly type: string; readonly payload: Record<string, unknown> },
|
|
64
|
+
eventDef: EventDef<TPayload>,
|
|
65
|
+
): TPayload {
|
|
66
|
+
if (event.type !== eventDef.name) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`[Kumiko] typedPayload: event type "${event.type}" does not match EventDef "${eventDef.name}". ` +
|
|
69
|
+
`Check the projection-apply / reducer mapping — the event was routed to the wrong handler.`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return event.payload as TPayload;
|
|
73
|
+
}
|