@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,742 @@
|
|
|
1
|
+
import type { Redis } from "ioredis";
|
|
2
|
+
import type { ZodType } from "zod";
|
|
3
|
+
import type { DbConnection } from "../../db/connection";
|
|
4
|
+
import type { TenantDb } from "../../db/tenant-db";
|
|
5
|
+
import type { FileContext } from "../../files/file-handle";
|
|
6
|
+
import type { Logger } from "../../logging/types";
|
|
7
|
+
import type { Meter, MetricsHandle, Tracer } from "../../observability/types";
|
|
8
|
+
import type { EntityCache } from "../../pipeline/entity-cache";
|
|
9
|
+
import type { SearchAdapter } from "../../search/types";
|
|
10
|
+
import type { TzContext } from "../../time";
|
|
11
|
+
import type { ConfigAccessor, ConfigAccessorFactory, ConfigResolver } from "./config";
|
|
12
|
+
import type { KumikoEventTypeMap } from "./event-type-map";
|
|
13
|
+
|
|
14
|
+
// --- Access ---
|
|
15
|
+
|
|
16
|
+
// AccessRule is DEFAULT-DENY: a handler without an access rule is not reachable.
|
|
17
|
+
// To grant access, set one of:
|
|
18
|
+
// - { roles: ["Admin", ...] } — role-based allowlist (empty array denies everyone)
|
|
19
|
+
// - { openToAll: true } — any authenticated user may call (still requires a valid JWT)
|
|
20
|
+
export type AccessRule = { readonly roles: readonly string[] } | { readonly openToAll: true };
|
|
21
|
+
|
|
22
|
+
// --- Pipeline User ---
|
|
23
|
+
|
|
24
|
+
export type SessionUser = {
|
|
25
|
+
// UUID-string so user.id threads through the event-store (aggregate-id) and
|
|
26
|
+
// the projection tables (uuid PK) without casts. Auth middleware reads the
|
|
27
|
+
// JWT `sub` claim as a string; legacy integer ids were a pre-ES artefact.
|
|
28
|
+
readonly id: string;
|
|
29
|
+
readonly tenantId: TenantId;
|
|
30
|
+
readonly roles: readonly string[];
|
|
31
|
+
// App-specific identity facts baked into the JWT at login time.
|
|
32
|
+
// Populated by `r.authClaims()` hooks (not yet implemented — see the
|
|
33
|
+
// auth-claims design note in docs/plans). Reserved here so the type shape
|
|
34
|
+
// is stable when the hook system lands.
|
|
35
|
+
readonly claims?: Readonly<Record<string, unknown>>;
|
|
36
|
+
// Session-ID — transported via the JWT `jti` standard claim. Present when
|
|
37
|
+
// an app has wired a `sessionCreator` callback on the auth-routes config
|
|
38
|
+
// (e.g. via the `sessions` feature). Absent for stateless-JWT deployments.
|
|
39
|
+
// When present, middleware can validate that the sid is still alive before
|
|
40
|
+
// accepting the request (session revocation).
|
|
41
|
+
readonly sid?: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// --- Claim Keys (r.claimKey declarations) ---
|
|
45
|
+
|
|
46
|
+
// Declared claim shape. Features call r.claimKey("teamId", { type: "string" })
|
|
47
|
+
// and get back a typed handle. Feature code then uses the handle both when
|
|
48
|
+
// reading via readClaim(user, handle) and (optionally) when returning from
|
|
49
|
+
// r.authClaims hooks. Two-fold payoff:
|
|
50
|
+
//
|
|
51
|
+
// 1. Read-site is typesafe: `const teamId = readClaim(user, DriverClaims.teamId)`
|
|
52
|
+
// narrows to `string | undefined` automatically — no hand-written cast,
|
|
53
|
+
// no magic "drivers:teamId" string.
|
|
54
|
+
// 2. Runtime check: the resolver warns when a hook returns an inner-key
|
|
55
|
+
// that the feature didn't declare — catches rename/typo drift. Opt-in
|
|
56
|
+
// per feature: only checked when r.claimKey was used at least once.
|
|
57
|
+
//
|
|
58
|
+
// Keep the type union small and explicit. JS-side inference via ClaimKeyJsType
|
|
59
|
+
// maps each literal to a primitive or array — broader shapes (nested
|
|
60
|
+
// records) can land in "object" but lose narrowness; that's the trade-off
|
|
61
|
+
// for keeping the type-system simple.
|
|
62
|
+
export type ClaimKeyType = "string" | "number" | "boolean" | "string[]" | "object";
|
|
63
|
+
|
|
64
|
+
export type ClaimKeyJsType<T extends ClaimKeyType> = T extends "string"
|
|
65
|
+
? string
|
|
66
|
+
: T extends "number"
|
|
67
|
+
? number
|
|
68
|
+
: T extends "boolean"
|
|
69
|
+
? boolean
|
|
70
|
+
: T extends "string[]"
|
|
71
|
+
? readonly string[]
|
|
72
|
+
: T extends "object"
|
|
73
|
+
? Readonly<Record<string, unknown>>
|
|
74
|
+
: never;
|
|
75
|
+
|
|
76
|
+
// Stored on the FeatureDefinition. `qualifiedName` is auto-set at
|
|
77
|
+
// registration time ("<feature>:<inner-kebab>") — same naming convention
|
|
78
|
+
// as auth-claim keys.
|
|
79
|
+
export type ClaimKeyDefinition = {
|
|
80
|
+
readonly shortName: string;
|
|
81
|
+
readonly qualifiedName: string;
|
|
82
|
+
readonly type: ClaimKeyType;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Typed handle returned by r.claimKey(). `name` is the qualified key the
|
|
86
|
+
// JWT stores; `type` threads through to readClaim's generic so consumers
|
|
87
|
+
// get the right narrowed type without casting.
|
|
88
|
+
export type ClaimKeyHandle<T extends ClaimKeyType = ClaimKeyType> = {
|
|
89
|
+
readonly name: string;
|
|
90
|
+
readonly type: T;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// --- Auth Claims (r.authClaims hook) ---
|
|
94
|
+
|
|
95
|
+
// Features contribute "identity facts" into the JWT at login time. Claim keys
|
|
96
|
+
// are auto-prefixed with the feature name at merge time (`"<feature>:<key>"`)
|
|
97
|
+
// so two features can't collide — Reading code in a handler picks the claim
|
|
98
|
+
// by its prefixed key: `user.claims["drivers:teamId"]`.
|
|
99
|
+
//
|
|
100
|
+
// The context is deliberately trimmed compared to HandlerContext: login is a
|
|
101
|
+
// READ, not a write-path. Exposing appendEvent/loadAggregate/tz here would
|
|
102
|
+
// let claims hooks reach into write-time concerns — not their job, bigger
|
|
103
|
+
// mocking surface in tests. `db` is guaranteed tenant-scoped to the chosen
|
|
104
|
+
// tenant (the one the user is logging INTO, not the one making the request).
|
|
105
|
+
// `queryAs` lets a hook call another feature's query handler without direct
|
|
106
|
+
// imports — same cross-feature contract hooks otherwise follow.
|
|
107
|
+
export type AuthClaimsContext = {
|
|
108
|
+
readonly db: import("../../db/tenant-db").TenantDb;
|
|
109
|
+
readonly queryAs: (user: SessionUser, qn: string, payload: unknown) => Promise<unknown>;
|
|
110
|
+
readonly config?: ConfigAccessor;
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export type AuthClaimsFn = (
|
|
114
|
+
user: SessionUser,
|
|
115
|
+
ctx: AuthClaimsContext,
|
|
116
|
+
) => Promise<Record<string, unknown>>;
|
|
117
|
+
|
|
118
|
+
// What the registry stores per registered hook. `featureName` drives the
|
|
119
|
+
// auto-prefix at merge time, so the registry is the source of truth for the
|
|
120
|
+
// naming — features never ship pre-prefixed keys.
|
|
121
|
+
//
|
|
122
|
+
// `declaredKeys` is the set of inner-keys this hook's feature declared via
|
|
123
|
+
// r.claimKey() — the resolver uses it to warn when a hook returns a key
|
|
124
|
+
// that was never declared (typo / rename drift). `undefined` when the
|
|
125
|
+
// feature never called r.claimKey(), in which case the resolver skips the
|
|
126
|
+
// check entirely (backwards-compat for features that only use r.authClaims).
|
|
127
|
+
export type AuthClaimsHookDef = {
|
|
128
|
+
readonly featureName: string;
|
|
129
|
+
readonly fn: AuthClaimsFn;
|
|
130
|
+
readonly declaredKeys?: ReadonlySet<string>;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// --- Handler Events ---
|
|
134
|
+
|
|
135
|
+
export type WriteEvent<TPayload = unknown> = {
|
|
136
|
+
readonly type: string;
|
|
137
|
+
readonly payload: TPayload;
|
|
138
|
+
readonly user: SessionUser;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export type QueryEvent<TPayload = unknown> = {
|
|
142
|
+
readonly type: string;
|
|
143
|
+
readonly payload: TPayload;
|
|
144
|
+
readonly user: SessionUser;
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// --- Handler Results ---
|
|
148
|
+
|
|
149
|
+
import type { WriteFailure } from "../../errors/write-error-info";
|
|
150
|
+
|
|
151
|
+
export type WriteResult<TData = unknown> =
|
|
152
|
+
| { readonly isSuccess: true; readonly data: TData }
|
|
153
|
+
| WriteFailure;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Override the success-side `data` of a WriteResult while forwarding the
|
|
157
|
+
* failure half untouched. Useful for handlers that delegate to the
|
|
158
|
+
* event-store executor (which returns a SaveContext / DeleteContext
|
|
159
|
+
* envelope) but want to keep their own response shape — caller contract
|
|
160
|
+
* stays flat instead of leaking the executor's internals.
|
|
161
|
+
*
|
|
162
|
+
* ```ts
|
|
163
|
+
* const result = await executor.delete({ id }, user, db);
|
|
164
|
+
* return withResponseData(result, { userId, tenantId });
|
|
165
|
+
* ```
|
|
166
|
+
*
|
|
167
|
+
* On failure the same WriteFailure instance is returned — the error
|
|
168
|
+
* object round-trips without any wrapping, so the dispatcher / HTTP layer
|
|
169
|
+
* still read the original error code + httpStatus + i18nKey.
|
|
170
|
+
*/
|
|
171
|
+
export function withResponseData<T>(result: WriteResult<unknown>, data: T): WriteResult<T> {
|
|
172
|
+
if (!result.isSuccess) return result;
|
|
173
|
+
return { isSuccess: true, data };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// --- Context Types ---
|
|
177
|
+
|
|
178
|
+
import type { TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
179
|
+
// Forward import: Registry is in feature.ts (circular type import — fine in TS)
|
|
180
|
+
import type { Registry } from "./feature";
|
|
181
|
+
|
|
182
|
+
// Minimal interface for job event triggers (framework-owned, concrete type in jobs/)
|
|
183
|
+
export type JobRunnerRef = {
|
|
184
|
+
handleEvent(
|
|
185
|
+
eventName: string,
|
|
186
|
+
payload: Record<string, unknown>,
|
|
187
|
+
user?: SessionUser,
|
|
188
|
+
): Promise<void>;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Priority levels for notifications
|
|
192
|
+
export type NotifyPriority = "critical" | "normal" | "low";
|
|
193
|
+
|
|
194
|
+
// Options passed to a NotifyFn / DeliveryService.notify. Defined here so the
|
|
195
|
+
// framework side and the concrete delivery implementation can't drift apart.
|
|
196
|
+
export type NotifyOptions = {
|
|
197
|
+
readonly to?: string | readonly string[] | { readonly tenant: TenantId };
|
|
198
|
+
readonly route?: Readonly<Record<string, string>>;
|
|
199
|
+
readonly data?: Readonly<Record<string, unknown>>;
|
|
200
|
+
readonly priority?: NotifyPriority;
|
|
201
|
+
// Opt-in dedup. Same key within 24h = single delivery. Use when a handler
|
|
202
|
+
// can be replayed (webhook retry, user double-click) and you don't want
|
|
203
|
+
// the notification to fire twice.
|
|
204
|
+
readonly idempotencyKey?: string;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Minimal interface for delivery notifications (concrete type in bundled-features/delivery)
|
|
208
|
+
export type NotifyFn = (notificationType: string, options: NotifyOptions) => Promise<void>;
|
|
209
|
+
|
|
210
|
+
// Factory that produces a bound NotifyFn for a specific user+tenant
|
|
211
|
+
// Concrete implementation in bundled-features/delivery (cross-package boundary)
|
|
212
|
+
export type NotifyFactory = (user: SessionUser, tenantId: TenantId) => NotifyFn;
|
|
213
|
+
|
|
214
|
+
// Shared optional fields across all execution contexts
|
|
215
|
+
type SharedContextFields = {
|
|
216
|
+
readonly redis?: Redis;
|
|
217
|
+
readonly jobRunner?: JobRunnerRef;
|
|
218
|
+
readonly configResolver?: ConfigResolver;
|
|
219
|
+
readonly config?: ConfigAccessor;
|
|
220
|
+
readonly _configAccessorFactory?: ConfigAccessorFactory;
|
|
221
|
+
// Encryption round-trip partner for the config feature. Separate from
|
|
222
|
+
// configResolver so the read-only resolver contract stays clean — the
|
|
223
|
+
// set handler needs to encrypt on write, the resolver needs to decrypt
|
|
224
|
+
// on read, and both reach for the same provider. Wired via extraContext.
|
|
225
|
+
readonly configEncryption?: import("../../db").EncryptionProvider;
|
|
226
|
+
// Rate-limit resolver. Wired by the framework when the `rateLimiting`
|
|
227
|
+
// feature is loaded — pipeline reads handler.rateLimit and calls
|
|
228
|
+
// .enforce() on this resolver before access-check. Absent when the
|
|
229
|
+
// app didn't load the feature: handlers with rateLimit set are
|
|
230
|
+
// rejected at boot to surface the misconfig early.
|
|
231
|
+
readonly rateLimit?: import("../../rate-limit").RateLimitResolver;
|
|
232
|
+
readonly searchAdapter?: SearchAdapter;
|
|
233
|
+
// Binary storage, wrapped around the registered FileStorageProvider.
|
|
234
|
+
// Optional at the AppContext level — present when the app booted with
|
|
235
|
+
// `files.storageProvider`. Hooks/handlers use ctx.files.ref(key) instead
|
|
236
|
+
// of receiving binaries in payloads.
|
|
237
|
+
readonly files?: FileContext;
|
|
238
|
+
readonly entityCache?: EntityCache;
|
|
239
|
+
readonly notify?: NotifyFn;
|
|
240
|
+
readonly _notifyFactory?: NotifyFactory;
|
|
241
|
+
// Tenant-scoped secrets accessor. Present when the app wired a
|
|
242
|
+
// MasterKeyProvider at boot. Feature code reads ctx.secrets.get(...)
|
|
243
|
+
// to pull a plaintext secret; Secret<string> carries the brand that
|
|
244
|
+
// the response guard rejects on serialization.
|
|
245
|
+
readonly secrets?: import("../../secrets").SecretsContext;
|
|
246
|
+
// Raw KEK provider. Present alongside ctx.secrets — needed by the rotation
|
|
247
|
+
// job which deliberately operates outside the per-call audit trail (it
|
|
248
|
+
// processes rows system-wide, not a per-user read).
|
|
249
|
+
readonly masterKeyProvider?: import("../../secrets").MasterKeyProvider;
|
|
250
|
+
// Observability: optional at the outer boundary, always populated by the
|
|
251
|
+
// time a handler receives its ctx (Noop fallback when no provider is
|
|
252
|
+
// configured, so handler code can call ctx.tracer/ctx.metrics without
|
|
253
|
+
// defensive checks).
|
|
254
|
+
readonly tracer?: Tracer;
|
|
255
|
+
readonly meter?: Meter;
|
|
256
|
+
// Cancellation. Aborts when the HTTP client disconnects (mobile back,
|
|
257
|
+
// tab close). Undefined for non-HTTP entry-points (jobs, MSP-applies).
|
|
258
|
+
// Long-running handlers (export jobs, multi-step workflows) should
|
|
259
|
+
// throw `signal.throwIfAborted()` at chunk boundaries; short handlers
|
|
260
|
+
// can ignore it. Framework primitives (streamAllEventsByType,
|
|
261
|
+
// rebuildProjection) honour it automatically.
|
|
262
|
+
readonly signal?: AbortSignal;
|
|
263
|
+
// Effective feature-toggle resolver. Wired by the dispatcher when the
|
|
264
|
+
// feature-toggles feature is loaded — the lifecycle pipeline, MSP runner,
|
|
265
|
+
// and ctx.hasFeature all read from this single source. Returns the Set
|
|
266
|
+
// of feature names that are currently effectively enabled (after global
|
|
267
|
+
// overrides and r.requires() cascade). Absent = all features on.
|
|
268
|
+
readonly effectiveFeatures?: () => ReadonlySet<string>;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// All optional — used at pipeline/system boundaries.
|
|
272
|
+
// `db` is a DbConnection at the outer boundary (server/stack) and a TenantDb
|
|
273
|
+
// once a HandlerContext has been built — hooks receive the HandlerContext as
|
|
274
|
+
// AppContext, so the union keeps that assignment straightforward.
|
|
275
|
+
export type AppContext = SharedContextFields & {
|
|
276
|
+
readonly db?: DbConnection | TenantDb;
|
|
277
|
+
readonly registry?: Registry;
|
|
278
|
+
readonly systemUser?: SessionUser;
|
|
279
|
+
readonly log?: Logger;
|
|
280
|
+
readonly triggeredBy?: { readonly id: string; readonly tenantId: TenantId } | null;
|
|
281
|
+
/** Bei Job-Handler-Aufrufen die aus einem Event-Trigger heraus laufen
|
|
282
|
+
* (r.job mit `trigger: { on: ... }`): der Name des Handlers der das
|
|
283
|
+
* Event ausgelöst hat. Bei Multi-Trigger-Jobs (`on: [...]`) ist das
|
|
284
|
+
* die einzige Möglichkeit für den Handler zu wissen WELCHER Trigger
|
|
285
|
+
* gefeuert hat. Cron- und manual-Jobs lassen das Feld undefined. */
|
|
286
|
+
readonly triggerName?: string;
|
|
287
|
+
readonly _userId?: string | undefined;
|
|
288
|
+
readonly _handlerType?: string | undefined;
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// Handler execution: db (tenant-scoped) + registry guaranteed.
|
|
292
|
+
//
|
|
293
|
+
// Cross-feature bridge:
|
|
294
|
+
// ctx.query / ctx.write run the target handler AS THE CURRENT USER,
|
|
295
|
+
// sharing the active tx + afterCommit queue. Field-access filters apply.
|
|
296
|
+
// ctx.queryAs / ctx.writeAs switch identity (e.g. SYSTEM for privileged
|
|
297
|
+
// lookups like "find user by email for auth" — system reads aren't filtered
|
|
298
|
+
// by field-access read rules).
|
|
299
|
+
//
|
|
300
|
+
// The design: handlers are the contract between features. Feature A requires
|
|
301
|
+
// Feature B and talks to it through B's registered handlers — never through
|
|
302
|
+
// direct imports of B's tables or internal types.
|
|
303
|
+
//
|
|
304
|
+
// TMap propagates the strict event-type-map through `appendEvent`. Defaults
|
|
305
|
+
// to the global KumikoEventTypeMap (augmented per app via
|
|
306
|
+
// `declare module "@cosmicdrift/kumiko-framework/engine"`). Code that bypasses the
|
|
307
|
+
// type-map (runtime-pluggable events) uses `appendEventUnsafe`.
|
|
308
|
+
export type HandlerContext<TMap extends object = KumikoEventTypeMap> = SharedContextFields & {
|
|
309
|
+
readonly db: TenantDb;
|
|
310
|
+
readonly registry: Registry;
|
|
311
|
+
/** Aktiver SessionUser des Handler-Aufrufs — Convenience-Alias zu
|
|
312
|
+
* `event.user`. Existiert weil Handler intuitiv `ctx.user.tenantId`
|
|
313
|
+
* schreiben (Context = "kennt seinen User") und der Pfad sonst nur
|
|
314
|
+
* über `event.user` läuft, was typo-anfällig ist und stillschweigend
|
|
315
|
+
* zu `internal_error` führt wenn der falsche Pfad gewählt wird.
|
|
316
|
+
* Identisch zum event.user-Wert; Identity-Switches nutzen
|
|
317
|
+
* weiterhin queryAs/writeAs. */
|
|
318
|
+
readonly user: SessionUser;
|
|
319
|
+
readonly systemUser?: SessionUser;
|
|
320
|
+
readonly log?: Logger;
|
|
321
|
+
readonly triggeredBy?: { readonly id: string; readonly tenantId: TenantId } | null;
|
|
322
|
+
readonly _userId?: string | undefined;
|
|
323
|
+
readonly _handlerType?: string | undefined;
|
|
324
|
+
|
|
325
|
+
readonly query: (qn: string, payload: unknown) => Promise<unknown>;
|
|
326
|
+
readonly queryAs: (user: SessionUser, qn: string, payload: unknown) => Promise<unknown>;
|
|
327
|
+
readonly write: (qn: string, payload: unknown) => Promise<WriteResult>;
|
|
328
|
+
readonly writeAs: (user: SessionUser, qn: string, payload: unknown) => Promise<WriteResult>;
|
|
329
|
+
|
|
330
|
+
// Runtime-check whether a feature is currently effectively-enabled. Use
|
|
331
|
+
// inside an active handler when logic should opt into behaviour that
|
|
332
|
+
// depends on another toggleable feature being on (e.g. "if premiumInvoices
|
|
333
|
+
// is on, add extra columns to the export"). The dispatcher gate already
|
|
334
|
+
// blocks calls to handlers of disabled features — this is the fine-grained
|
|
335
|
+
// opt-in counterpart, not a substitute for the gate.
|
|
336
|
+
readonly hasFeature: (featureName: string) => boolean;
|
|
337
|
+
|
|
338
|
+
// Append a domain event to a specific aggregate stream in the current tx.
|
|
339
|
+
// Marten-aligned: every event belongs to exactly one aggregate. The runtime
|
|
340
|
+
// reads the current stream version, bumps it, and fires projections that
|
|
341
|
+
// match the event type in the same transaction. Use it when a write-handler
|
|
342
|
+
// wants to record a domain event alongside the auto-generated CRUD events
|
|
343
|
+
// (e.g. "invoice.approved" on the same invoice stream that already carries
|
|
344
|
+
// "invoice.created" + "invoice.updated").
|
|
345
|
+
readonly appendEvent: AppendEventFn<TMap>;
|
|
346
|
+
|
|
347
|
+
// Escape-hatch for runtime-pluggable features without a compile-time
|
|
348
|
+
// augmentation. See AppendEventUnsafeFn — same runtime as appendEvent,
|
|
349
|
+
// but the type-surface is `payload: unknown`. Use only when the event-
|
|
350
|
+
// type is not knowable at compile-time; otherwise the strict path
|
|
351
|
+
// (appendEvent) is the contract Designer/AI rely on.
|
|
352
|
+
readonly appendEventUnsafe: AppendEventUnsafeFn;
|
|
353
|
+
|
|
354
|
+
// Marten FetchForWriting equivalent: load the current stream, optionally
|
|
355
|
+
// enforce expectedVersion, and get a handle that appends further events
|
|
356
|
+
// onto that stream without re-specifying aggregateId/aggregateType.
|
|
357
|
+
// Fails fast with VersionConflictError when expectedVersion doesn't
|
|
358
|
+
// match — the write-handler never touches state it didn't expect.
|
|
359
|
+
readonly fetchForWriting: (args: FetchForWritingArgs) => Promise<AggregateStreamHandle>;
|
|
360
|
+
|
|
361
|
+
// Load the full stream of events for an aggregate, tenant-scoped to the
|
|
362
|
+
// current user. Events pass through the registered upcaster chain, so the
|
|
363
|
+
// payloads returned match the current schema shape regardless of when
|
|
364
|
+
// they were written. Use inside a queryHandler to expose Marten-style
|
|
365
|
+
// AggregateStreamAsync: hand the events to a reducer and return the
|
|
366
|
+
// derived state (live aggregation).
|
|
367
|
+
//
|
|
368
|
+
// `options.asOf` restricts to events whose createdAt is ≤ the given
|
|
369
|
+
// timestamp — the point-in-time / "what did this aggregate look like
|
|
370
|
+
// yesterday" query.
|
|
371
|
+
readonly loadAggregate: (
|
|
372
|
+
aggregateId: string,
|
|
373
|
+
options?: { readonly asOf?: Temporal.Instant },
|
|
374
|
+
) => Promise<readonly import("../../event-store").StoredEvent[]>;
|
|
375
|
+
|
|
376
|
+
// Marten-aligned stream lifecycle. Archived streams become read-only:
|
|
377
|
+
// ctx.appendEvent throws ArchivedStreamError, ctx.loadAggregate returns []
|
|
378
|
+
// (pass { includeArchived: true } on the low-level loaders to override).
|
|
379
|
+
// restoreStream reopens a stream; aggregate-level lifecycle states like
|
|
380
|
+
// "closed" stay in the domain events, not the archive flag.
|
|
381
|
+
readonly archiveStream: (
|
|
382
|
+
aggregateId: string,
|
|
383
|
+
args: { readonly aggregateType: string; readonly reason?: string },
|
|
384
|
+
) => Promise<void>;
|
|
385
|
+
readonly restoreStream: (aggregateId: string) => Promise<void>;
|
|
386
|
+
readonly isStreamArchived: (aggregateId: string) => Promise<boolean>;
|
|
387
|
+
|
|
388
|
+
// Cache the current state of an aggregate as a snapshot. Callers that
|
|
389
|
+
// hold the state (e.g. just reduced the stream in a queryHandler, or
|
|
390
|
+
// finished a write batch) pass it in alongside the version it reflects.
|
|
391
|
+
// The framework handles storage + upsert semantics; the snapshot policy
|
|
392
|
+
// (every N events, every M minutes, on-demand) stays with the feature.
|
|
393
|
+
// Snapshots are a perf optimisation — the event log remains the source
|
|
394
|
+
// of truth.
|
|
395
|
+
readonly snapshotAggregate: (args: {
|
|
396
|
+
readonly aggregateId: string;
|
|
397
|
+
readonly aggregateType: string;
|
|
398
|
+
readonly version: number;
|
|
399
|
+
readonly state: Record<string, unknown>;
|
|
400
|
+
}) => Promise<void>;
|
|
401
|
+
|
|
402
|
+
// Snapshot-aware rehydrate. Loads the latest snapshot (if any), runs the
|
|
403
|
+
// registered upcaster chain on every delta event, and folds them onto
|
|
404
|
+
// the snapshot state with the caller's reducer. Returns the final state,
|
|
405
|
+
// the latest event version, and whether a snapshot was used — the last
|
|
406
|
+
// lets a feature's snapshot policy make informed decisions
|
|
407
|
+
// (e.g. "snapshot every 100 events past the last snapshot").
|
|
408
|
+
//
|
|
409
|
+
// Archived streams behave like ctx.loadAggregate — empty result with
|
|
410
|
+
// version=0, not an exception.
|
|
411
|
+
readonly loadAggregateWithSnapshot: <TState extends Record<string, unknown>>(
|
|
412
|
+
aggregateId: string,
|
|
413
|
+
reducer: import("../../event-store").SnapshotReducer<TState>,
|
|
414
|
+
initial: TState,
|
|
415
|
+
) => Promise<import("../../event-store").LoadAggregateWithSnapshotResult<TState>>;
|
|
416
|
+
|
|
417
|
+
// Read rows from a registered projection table, tenant-scoped to the
|
|
418
|
+
// current user. Marten's equivalent of session.Query<T>() — the projection
|
|
419
|
+
// table is the read model; this surface makes it reachable by qualified
|
|
420
|
+
// name without the feature having to import the drizzle-table directly.
|
|
421
|
+
//
|
|
422
|
+
// Auto-applies tenant_id filter when the projection table has a tenant_id
|
|
423
|
+
// column (or opt out with { allTenants: true } for system-scoped reads
|
|
424
|
+
// like cross-tenant analytics). Unknown projection name throws.
|
|
425
|
+
readonly queryProjection: <T = Record<string, unknown>>(
|
|
426
|
+
qualifiedName: string,
|
|
427
|
+
options?: { readonly allTenants?: boolean },
|
|
428
|
+
) => Promise<readonly T[]>;
|
|
429
|
+
|
|
430
|
+
// Always populated — Noop when no observability provider is configured.
|
|
431
|
+
// Feature code can call ctx.metrics.inc(...) / ctx.tracer.startSpan(...)
|
|
432
|
+
// without null-checks.
|
|
433
|
+
readonly metrics: MetricsHandle;
|
|
434
|
+
readonly tracer: Tracer;
|
|
435
|
+
|
|
436
|
+
// Time + TZ helper. Feature-Code MUSS hier durch statt `new Date()` —
|
|
437
|
+
// ctx.tz.now() liefert Temporal.Instant, ctx.tz.parse(wallClock, tz)
|
|
438
|
+
// produziert ZonedDateTime, ctx.tz.toLocatedJson serialisiert für die
|
|
439
|
+
// API-Boundary. Lint-Regel gegen `new Date()` kommt sobald alle internen
|
|
440
|
+
// usages migriert sind. Tenant + User-TZ defaults aktuell "UTC", werden
|
|
441
|
+
// aus tenant.timezone / user.timezone gelesen sobald die Felder existieren.
|
|
442
|
+
readonly tz: TzContext;
|
|
443
|
+
|
|
444
|
+
// Resolve every registered r.authClaims() hook against `user` and return
|
|
445
|
+
// the merged claim record (keys auto-prefixed with the feature name). Used
|
|
446
|
+
// by login + switch-tenant write-handlers to populate SessionUser.claims
|
|
447
|
+
// before the JWT is signed. Thin pass-through to dispatcher.resolveAuthClaims
|
|
448
|
+
// so there's a single resolve impl — both entry-points can't drift.
|
|
449
|
+
readonly resolveAuthClaims: (user: SessionUser) => Promise<Record<string, unknown>>;
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
// Job execution: db + registry + systemUser + logging guaranteed
|
|
453
|
+
export type JobContext = SharedContextFields & {
|
|
454
|
+
readonly db: DbConnection;
|
|
455
|
+
readonly registry: Registry;
|
|
456
|
+
readonly systemUser: SessionUser;
|
|
457
|
+
readonly log: Logger;
|
|
458
|
+
readonly triggeredBy: { readonly id: string; readonly tenantId: TenantId } | null;
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// --- Handler Functions ---
|
|
462
|
+
|
|
463
|
+
export type WriteHandlerFn<TPayload = unknown, TData = unknown> = (
|
|
464
|
+
event: WriteEvent<TPayload>,
|
|
465
|
+
context: HandlerContext,
|
|
466
|
+
) => Promise<WriteResult<TData>>;
|
|
467
|
+
|
|
468
|
+
export type QueryHandlerFn<TPayload = unknown, TResult = unknown> = (
|
|
469
|
+
query: QueryEvent<TPayload>,
|
|
470
|
+
context: HandlerContext,
|
|
471
|
+
) => Promise<TResult>;
|
|
472
|
+
|
|
473
|
+
// --- Event Definitions ---
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Compile-time mirror of `engine/qualified-name.ts:toKebab` for camelCase
|
|
477
|
+
* → kebab-case. Drives the literal-type of `EventDef.name`, so that
|
|
478
|
+
* `r.defineEvent("foo", schema)` inside `defineFeature("driverOrders")`
|
|
479
|
+
* carries `name: "driver-orders:event:foo"` as a literal — strict-mode
|
|
480
|
+
* for `ctx.appendEvent({ type: eventDef.name, ... })` lights up.
|
|
481
|
+
*
|
|
482
|
+
* Algorithm mirrors the runtime regex pipeline:
|
|
483
|
+
* 1. `.` → `-` (dot acts as word-boundary)
|
|
484
|
+
* 2. Insert `-` between `[A-Z]+` and `[A-Z][a-z]` (so `SSEFoo` →
|
|
485
|
+
* `SSE-Foo`, splitting an uppercase run before a camel-hump)
|
|
486
|
+
* 3. Insert `-` between `[a-z0-9]` and `[A-Z]` (camelCase boundary,
|
|
487
|
+
* so `ticketAssigned` → `ticket-Assigned`)
|
|
488
|
+
* 4. lowercase everything
|
|
489
|
+
*
|
|
490
|
+
* Implemented as a state machine with one-char lookahead. State:
|
|
491
|
+
* - "start" — at start of string, or right after a dot-boundary
|
|
492
|
+
* - "upper" — last emitted char came from an uppercase letter
|
|
493
|
+
* - "post-letter" — last emitted char was a lowercase letter or digit
|
|
494
|
+
*
|
|
495
|
+
* Sync vs runtime is verified by `engine/__tests__/camel-to-kebab.test-d.ts`
|
|
496
|
+
* — the type-tests cross-check identical inputs against `toKebab()`.
|
|
497
|
+
*/
|
|
498
|
+
export type CamelToKebab<S extends string> = CamelToKebabImpl<S, "start", "">;
|
|
499
|
+
|
|
500
|
+
type CamelToKebabImpl<
|
|
501
|
+
S extends string,
|
|
502
|
+
Prev extends "start" | "upper" | "post-letter",
|
|
503
|
+
Acc extends string,
|
|
504
|
+
> = S extends `${infer C}${infer Rest}`
|
|
505
|
+
? CharKind<C> extends "upper"
|
|
506
|
+
? Prev extends "start"
|
|
507
|
+
? CamelToKebabImpl<Rest, "upper", `${Acc}${Lowercase<C>}`>
|
|
508
|
+
: Prev extends "post-letter"
|
|
509
|
+
? CamelToKebabImpl<Rest, "upper", `${Acc}-${Lowercase<C>}`>
|
|
510
|
+
: // Prev = "upper" — peek next char to decide between
|
|
511
|
+
// continuing-the-run and splitting-before-camel-hump.
|
|
512
|
+
Rest extends `${infer Next}${string}`
|
|
513
|
+
? CharKind<Next> extends "lower"
|
|
514
|
+
? CamelToKebabImpl<Rest, "upper", `${Acc}-${Lowercase<C>}`>
|
|
515
|
+
: CamelToKebabImpl<Rest, "upper", `${Acc}${Lowercase<C>}`>
|
|
516
|
+
: `${Acc}${Lowercase<C>}`
|
|
517
|
+
: CharKind<C> extends "lower"
|
|
518
|
+
? CamelToKebabImpl<Rest, "post-letter", `${Acc}${C}`>
|
|
519
|
+
: // Non-letter: dots become word-boundaries (state resets to "start"
|
|
520
|
+
// so the next uppercase letter doesn't pick up a redundant dash).
|
|
521
|
+
// Other non-letters (digits etc.) act like lowercase for transitions.
|
|
522
|
+
C extends "."
|
|
523
|
+
? CamelToKebabImpl<Rest, "start", `${Acc}-`>
|
|
524
|
+
: CamelToKebabImpl<Rest, "post-letter", `${Acc}${C}`>
|
|
525
|
+
: Acc;
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Three-way classification used by `CamelToKebab`:
|
|
529
|
+
* - "lower" — a lowercase letter (a-z and Unicode lowercase)
|
|
530
|
+
* - "upper" — an uppercase letter (A-Z and Unicode uppercase)
|
|
531
|
+
* - "non-letter" — digit, dot, dash, etc. (Lowercase==Uppercase for them)
|
|
532
|
+
*/
|
|
533
|
+
type CharKind<C extends string> =
|
|
534
|
+
C extends Lowercase<C> ? (C extends Uppercase<C> ? "non-letter" : "lower") : "upper";
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Builds the qualified event-name from feature + inner-name in the same
|
|
538
|
+
* shape the runtime emits via `qn(toKebab(feature), "event", toKebab(inner))`.
|
|
539
|
+
*/
|
|
540
|
+
export type QualifiedEventName<
|
|
541
|
+
TFeature extends string,
|
|
542
|
+
TInner extends string,
|
|
543
|
+
> = `${CamelToKebab<TFeature>}:event:${CamelToKebab<TInner>}`;
|
|
544
|
+
|
|
545
|
+
export type EventDef<TPayload = unknown, TName extends string = string> = {
|
|
546
|
+
readonly name: TName;
|
|
547
|
+
readonly schema: ZodType<TPayload>;
|
|
548
|
+
// Schema generation number. Starts at 1; bumped whenever a breaking change
|
|
549
|
+
// to the payload shape lands together with a matching r.eventMigration that
|
|
550
|
+
// upcasts older stored events. Reads consult this to decide if upcasters
|
|
551
|
+
// need to run before the payload hits consumer code.
|
|
552
|
+
readonly version: number;
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// Args for ctx.appendEvent — explicit aggregate target, Marten-style.
|
|
556
|
+
// `type` must match a name returned by r.defineEvent in any registered
|
|
557
|
+
// feature; payload is validated against that event's Zod schema before
|
|
558
|
+
// being written to the events-table.
|
|
559
|
+
//
|
|
560
|
+
// `headers` lands in StoredEvent.metadata.headers — Marten-conform free
|
|
561
|
+
// key/value space for app-specific metadata (A/B-bucket, geo-region,
|
|
562
|
+
// client SDK version). Framework does not interpret values; keep them
|
|
563
|
+
// JSON-primitive (string|number|boolean) for safe serialization.
|
|
564
|
+
export type AppendEventArgs = {
|
|
565
|
+
readonly aggregateId: string;
|
|
566
|
+
readonly aggregateType: string;
|
|
567
|
+
readonly type: string;
|
|
568
|
+
readonly payload: unknown;
|
|
569
|
+
readonly headers?: Readonly<Record<string, string | number | boolean>>;
|
|
570
|
+
};
|
|
571
|
+
|
|
572
|
+
// Typed-payload variant — used by the strict ctx.appendEvent. Keyed via
|
|
573
|
+
// the discriminator type-arg so payload inference flows from `type`-literal
|
|
574
|
+
// to the matching schema-payload.
|
|
575
|
+
//
|
|
576
|
+
// TMap is propagated as a generic parameter (not hard-coded to
|
|
577
|
+
// KumikoEventTypeMap) so the constraint `K extends keyof TMap` resolves at
|
|
578
|
+
// USE-site instead of definition-site. Cross-package augmentation only
|
|
579
|
+
// becomes visible at use-site — the App's tsc compiles the augmentation
|
|
580
|
+
// alongside its own code, so `keyof TMap` widens to include all augmented
|
|
581
|
+
// event names. Hard-coding `keyof KumikoEventTypeMap` here would resolve
|
|
582
|
+
// at definition-site (framework's compile) where the augmentation is
|
|
583
|
+
// invisible → K = never, no strict-checking. The default = KumikoEventTypeMap
|
|
584
|
+
// keeps existing call-sites zero-config.
|
|
585
|
+
export type TypedAppendEventArgs<TMap extends object, K extends keyof TMap> = {
|
|
586
|
+
readonly aggregateId: string;
|
|
587
|
+
readonly aggregateType: string;
|
|
588
|
+
readonly type: K;
|
|
589
|
+
readonly payload: TMap[K];
|
|
590
|
+
readonly headers?: Readonly<Record<string, string | number | boolean>>;
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
// Strict-only form. Single overload — `<K extends keyof TMap>` against the
|
|
594
|
+
// app's pre-bound TMap. No fallback overload: apps that need runtime-pluggable
|
|
595
|
+
// events (where the type-string isn't known at compile-time) reach for
|
|
596
|
+
// `appendEventUnsafe`.
|
|
597
|
+
//
|
|
598
|
+
// Why no fallback overload:
|
|
599
|
+
// A two-overload form (`(args: AppendEventArgs)` as the second sig)
|
|
600
|
+
// silently accepts any args via the loose overload as soon as TS can't
|
|
601
|
+
// prove the strict one matches. Cross-package, the strict overload's
|
|
602
|
+
// `K = keyof TMap` collapses to `never` when called WITHOUT a local
|
|
603
|
+
// wrapper (default-substitution is eager at definition-site → augmentation
|
|
604
|
+
// invisible). Either every caller binds TMap via wrapper → strict fires;
|
|
605
|
+
// or they don't, and the fallback would silently swallow every typo.
|
|
606
|
+
// We pick the first option and force the wrong path to fail visibly.
|
|
607
|
+
//
|
|
608
|
+
// How this is wired in practice:
|
|
609
|
+
// - Apps run `yarn kumiko codegen`, which writes `.kumiko/define.ts`
|
|
610
|
+
// with locally-bound `defineWriteHandler<TName, TSchema, TData,
|
|
611
|
+
// KumikoEventTypeMap>(...)` wrappers. Handlers inside those wrappers
|
|
612
|
+
// get a strict ctx.appendEvent.
|
|
613
|
+
// - Cross-package callers (e.g. bundled-features's set.write.ts) that
|
|
614
|
+
// can't afford a local wrapper reach for `ctx.appendEventUnsafe`
|
|
615
|
+
// instead — same runtime, looser type-surface.
|
|
616
|
+
export type AppendEventFn<TMap extends object = KumikoEventTypeMap> = <K extends keyof TMap>(
|
|
617
|
+
args: TypedAppendEventArgs<TMap, K>,
|
|
618
|
+
) => Promise<void>;
|
|
619
|
+
|
|
620
|
+
export type AppendEventUnsafeFn = (args: AppendEventArgs) => Promise<void>;
|
|
621
|
+
|
|
622
|
+
// Args for ctx.fetchForWriting — Marten FetchForWriting equivalent. Returns
|
|
623
|
+
// the current stream state + a handle that appends without re-specifying
|
|
624
|
+
// aggregateId/aggregateType. When expectedVersion is provided, the handle
|
|
625
|
+
// rejects the write immediately if the stream is ahead — optimistic
|
|
626
|
+
// concurrency enforced BEFORE any downstream work. Without expectedVersion,
|
|
627
|
+
// the handle trusts whatever version the stream currently has.
|
|
628
|
+
export type FetchForWritingArgs = {
|
|
629
|
+
readonly aggregateId: string;
|
|
630
|
+
readonly aggregateType: string;
|
|
631
|
+
readonly expectedVersion?: number;
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
export type AggregateStreamHandle = {
|
|
635
|
+
// Snapshot at fetch time — upcasted via the registered upcaster chain,
|
|
636
|
+
// so payloads match the current schema regardless of when they landed.
|
|
637
|
+
readonly events: readonly import("../../event-store").StoredEvent[];
|
|
638
|
+
readonly version: number;
|
|
639
|
+
// Append an event on this stream. Derives aggregateId/aggregateType/
|
|
640
|
+
// expectedVersion from the handle automatically. Multiple calls in a
|
|
641
|
+
// row bump the handle's internal version and the events-table in order.
|
|
642
|
+
readonly appendOne: (args: { readonly type: string; readonly payload: unknown }) => Promise<void>;
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
// --- Event Upcasters (schema migration) ---
|
|
646
|
+
//
|
|
647
|
+
// Marten's Upcaster pattern adapted for TypeScript. An event's payload shape
|
|
648
|
+
// may evolve over releases; stored events stay immutable on disk. Features
|
|
649
|
+
// register step-wise transforms that upgrade v(N) payloads to v(N+1) at read
|
|
650
|
+
// time. The framework chains them automatically — a v1 event gets walked
|
|
651
|
+
// through every registered migration up to the current version before the
|
|
652
|
+
// payload reaches a projection apply() or ctx.appendEvent consumer.
|
|
653
|
+
//
|
|
654
|
+
// Sync transforms: just return the upgraded payload. Most schema-evolution
|
|
655
|
+
// (renames, additions, format-fixes) needs no IO and stays sync — fast on
|
|
656
|
+
// the hot path of projection-rebuild.
|
|
657
|
+
//
|
|
658
|
+
// Async transforms (Marten's "AsyncOnlyEventUpcaster"): when the upgrade
|
|
659
|
+
// needs DB enrichment (e.g. v1 stored only a customerId, v2 also needs the
|
|
660
|
+
// customer's segment which lives in a reference table), accept the optional
|
|
661
|
+
// ctx-arg, run the lookup via ctx.db, return a Promise. The framework
|
|
662
|
+
// awaits unconditionally — sync transforms return a plain value and pay
|
|
663
|
+
// only the await-microtask overhead. Pattern-match Marten:
|
|
664
|
+
// r.eventMigration("invoiceCreated", 1, 2, async (payload, ctx) => {
|
|
665
|
+
// const customer = await ctx.db.select().from(customersTable)...;
|
|
666
|
+
// return { ...payload, customerSegment: customer.segment };
|
|
667
|
+
// });
|
|
668
|
+
export type EventUpcastCtx = {
|
|
669
|
+
readonly db: import("../../db").DbRunner;
|
|
670
|
+
readonly tenantId: import("./identifiers").TenantId;
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
export type EventUpcastFn = (payload: unknown, ctx: EventUpcastCtx) => unknown | Promise<unknown>;
|
|
674
|
+
|
|
675
|
+
export type EventMigrationDef = {
|
|
676
|
+
// Qualified event name, matching r.defineEvent(...).name.
|
|
677
|
+
readonly eventName: string;
|
|
678
|
+
readonly fromVersion: number;
|
|
679
|
+
readonly toVersion: number; // must be fromVersion + 1
|
|
680
|
+
readonly transform: EventUpcastFn;
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
// --- References ---
|
|
684
|
+
|
|
685
|
+
// Anything that carries a name — accepted by hooks, relations, jobs, etc.
|
|
686
|
+
export type NameOrRef = string | { readonly name: string };
|
|
687
|
+
|
|
688
|
+
export function resolveName(ref: NameOrRef): string {
|
|
689
|
+
return typeof ref === "string" ? ref : ref.name;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export type EntityRef = {
|
|
693
|
+
readonly name: string;
|
|
694
|
+
readonly table: string;
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
export type HandlerRef = {
|
|
698
|
+
readonly name: string;
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
// --- Handler Definitions (stored in feature/registry) ---
|
|
702
|
+
|
|
703
|
+
// Per-handler rate limit. Bucket key derived from `per`:
|
|
704
|
+
// "user" → userId
|
|
705
|
+
// "tenant" → tenantId
|
|
706
|
+
// "ip" → request IP
|
|
707
|
+
// "user+handler" → userId + handlerName
|
|
708
|
+
// "tenant+handler" → tenantId + handlerName
|
|
709
|
+
// "ip+handler" → IP + handlerName (anonymous endpoints)
|
|
710
|
+
// `cost` is the tokens this handler-call deducts. Default 1 — bump for
|
|
711
|
+
// expensive operations (bulk export, bulk import).
|
|
712
|
+
export type RateLimitPer =
|
|
713
|
+
| "user"
|
|
714
|
+
| "tenant"
|
|
715
|
+
| "ip"
|
|
716
|
+
| "user+handler"
|
|
717
|
+
| "tenant+handler"
|
|
718
|
+
| "ip+handler";
|
|
719
|
+
|
|
720
|
+
export type RateLimitOption = {
|
|
721
|
+
readonly per: RateLimitPer;
|
|
722
|
+
readonly limit: number;
|
|
723
|
+
readonly windowSeconds: number;
|
|
724
|
+
readonly cost?: number;
|
|
725
|
+
};
|
|
726
|
+
|
|
727
|
+
export type WriteHandlerDef = {
|
|
728
|
+
readonly name: string;
|
|
729
|
+
readonly schema: ZodType;
|
|
730
|
+
readonly handler: WriteHandlerFn;
|
|
731
|
+
readonly access?: AccessRule;
|
|
732
|
+
readonly skipTransitionGuard?: boolean;
|
|
733
|
+
readonly rateLimit?: RateLimitOption;
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
export type QueryHandlerDef = {
|
|
737
|
+
readonly name: string;
|
|
738
|
+
readonly schema: ZodType;
|
|
739
|
+
readonly handler: QueryHandlerFn;
|
|
740
|
+
readonly access?: AccessRule;
|
|
741
|
+
readonly rateLimit?: RateLimitOption;
|
|
742
|
+
};
|