@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,434 @@
|
|
|
1
|
+
import { SYSTEM_TENANT_ID, type TenantId } from "@cosmicdrift/kumiko-framework/engine";
|
|
2
|
+
import { and, type Column, eq, getTableName, or, type SQL } from "drizzle-orm";
|
|
3
|
+
import { emitDbQuery, type Meter, registerStandardMetrics, type Tracer } from "../observability";
|
|
4
|
+
import type { DbRunner } from "./connection";
|
|
5
|
+
import type { TableColumns } from "./dialect";
|
|
6
|
+
|
|
7
|
+
// biome-ignore lint/suspicious/noExplicitAny: Drizzle dynamic tables
|
|
8
|
+
type Table = TableColumns<any>;
|
|
9
|
+
|
|
10
|
+
// biome-ignore lint/suspicious/noExplicitAny: Drizzle column selection
|
|
11
|
+
type ColumnSelection = Record<string, any>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* TenantDb scope modes:
|
|
15
|
+
*
|
|
16
|
+
* - "tenant" (default): SELECT/UPDATE/DELETE filtered by tenantId + reference data (tenantId=0).
|
|
17
|
+
* INSERT forces tenantId — handler cannot override.
|
|
18
|
+
*
|
|
19
|
+
* - "system" (r.systemScope()): No tenant filter on reads/updates/deletes.
|
|
20
|
+
* INSERT uses tenantId as default but handler can override (e.g. write a
|
|
21
|
+
* cross-tenant row to a shared sentinel like SYSTEM_TENANT_ID).
|
|
22
|
+
*
|
|
23
|
+
* Tables without a tenantId column are always unfiltered regardless of mode.
|
|
24
|
+
*/
|
|
25
|
+
export type TenantDbMode = "tenant" | "system";
|
|
26
|
+
|
|
27
|
+
export type TenantDb = {
|
|
28
|
+
readonly tenantId: TenantId;
|
|
29
|
+
readonly mode: TenantDbMode;
|
|
30
|
+
/**
|
|
31
|
+
* Underlying DbRunner. Framework-internal use (event-store, migrations) —
|
|
32
|
+
* bypasses tenant-filter. Feature code should stick to the typed wrappers
|
|
33
|
+
* above so the automatic scoping stays intact.
|
|
34
|
+
*/
|
|
35
|
+
readonly raw: DbRunner;
|
|
36
|
+
select(): TenantSelect;
|
|
37
|
+
select(columns: ColumnSelection): TenantSelect;
|
|
38
|
+
insert(table: Table): TenantInsert;
|
|
39
|
+
update(table: Table): TenantUpdate;
|
|
40
|
+
delete(table: Table): TenantDelete;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type TenantSelect = {
|
|
44
|
+
from(table: Table): TenantSelectQuery;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type WhereCondition = SQL | undefined;
|
|
48
|
+
|
|
49
|
+
type RowLockStrength = "update" | "no key update" | "share" | "key share";
|
|
50
|
+
|
|
51
|
+
type TenantSelectQuery = PromiseLike<Record<string, unknown>[]> & {
|
|
52
|
+
where(condition: WhereCondition): TenantSelectQuery;
|
|
53
|
+
limit(n: number): TenantSelectQuery;
|
|
54
|
+
offset(n: number): TenantSelectQuery;
|
|
55
|
+
orderBy(...columns: (SQL | Column)[]): TenantSelectQuery;
|
|
56
|
+
/** Row-level locking (FOR UPDATE / FOR SHARE). Must be called inside a tx. */
|
|
57
|
+
for(strength: RowLockStrength): TenantSelectQuery;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type TenantInsert = {
|
|
61
|
+
values(data: Record<string, unknown>): TenantInsertValues;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
type ConflictTarget = Column | readonly Column[];
|
|
65
|
+
type ConflictUpdate = {
|
|
66
|
+
target: ConflictTarget;
|
|
67
|
+
set: Record<string, unknown>;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
type TenantInsertValues = PromiseLike<void> & {
|
|
71
|
+
returning(): PromiseLike<Record<string, unknown>[]>;
|
|
72
|
+
onConflictDoUpdate(spec: ConflictUpdate): PromiseLike<void>;
|
|
73
|
+
onConflictDoNothing(spec?: { target: ConflictTarget }): PromiseLike<void>;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
type TenantUpdate = {
|
|
77
|
+
set(data: Record<string, unknown>): TenantUpdateSet;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type TenantUpdateSet = PromiseLike<void> & {
|
|
81
|
+
where(condition: WhereCondition): TenantUpdateWhere;
|
|
82
|
+
returning(): PromiseLike<Record<string, unknown>[]>;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
type TenantUpdateWhere = PromiseLike<void> & {
|
|
86
|
+
returning(): PromiseLike<Record<string, unknown>[]>;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
type TenantDelete = {
|
|
90
|
+
where(condition: WhereCondition): PromiseLike<void>;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Cast helper for the `Record<string, unknown>[]` rows that
|
|
95
|
+
* `TenantDb.select()` returns.
|
|
96
|
+
*
|
|
97
|
+
* Usage:
|
|
98
|
+
* const rows = castTenantRows<MyRow>(
|
|
99
|
+
* await ctx.db.select({...}).from(myTable),
|
|
100
|
+
* );
|
|
101
|
+
*
|
|
102
|
+
* Why this exists: drizzle's `.select({col1: t.col1, ...})` natively
|
|
103
|
+
* returns `Array<{col1: T1, ...}>`, but our TenantDb wrapper erases
|
|
104
|
+
* that shape to `Record<string, unknown>[]` so it can centralize tenant-
|
|
105
|
+
* scoping. Until the wrapper preserves the typed-row shape (see memory:
|
|
106
|
+
* project_tenant_db_typed_rows), call sites need to assert the column
|
|
107
|
+
* shape they just selected. This helper:
|
|
108
|
+
* - centralises the cast (single grep target for the future refactor)
|
|
109
|
+
* - tags it with `@cast-boundary tenant-db-row` for the as-cast audit
|
|
110
|
+
* - documents the trade-off once instead of N times
|
|
111
|
+
*
|
|
112
|
+
* Removal plan: when TenantSelectQuery becomes generic over the
|
|
113
|
+
* column-shape, every `castTenantRows<T>(...)` call is just `await ...`
|
|
114
|
+
* and this helper goes away.
|
|
115
|
+
*/
|
|
116
|
+
// @cast-boundary tenant-db-row
|
|
117
|
+
export function castTenantRows<T>(rows: readonly Record<string, unknown>[]): readonly T[] {
|
|
118
|
+
return rows as unknown as readonly T[];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function createTenantDb(
|
|
122
|
+
db: DbRunner,
|
|
123
|
+
tenantId: TenantId,
|
|
124
|
+
mode: TenantDbMode = "tenant",
|
|
125
|
+
tracer?: Tracer,
|
|
126
|
+
meter?: Meter,
|
|
127
|
+
// Pre-flight cancellation: when set, every query check
|
|
128
|
+
// `signal.throwIfAborted()` BEFORE issuing the SQL. The currently
|
|
129
|
+
// running query is not actively cancelled (postgres-js connection
|
|
130
|
+
// cancel is a separate, riskier feature). This still saves the bulk
|
|
131
|
+
// of the wasted work in handlers that fire many sequential queries
|
|
132
|
+
// — once the client disconnects, the next query throws and the rest
|
|
133
|
+
// of the chain falls away.
|
|
134
|
+
signal?: AbortSignal,
|
|
135
|
+
): TenantDb {
|
|
136
|
+
// If a meter was passed, make sure standard metrics are registered on it
|
|
137
|
+
// before we try to emit. Idempotent — buildServer typically registers them
|
|
138
|
+
// up front; this guards against test call-sites that wire up a TenantDb
|
|
139
|
+
// directly with a fresh meter.
|
|
140
|
+
if (meter) registerStandardMetrics(meter);
|
|
141
|
+
|
|
142
|
+
function hasTenantColumn(table: Table): boolean {
|
|
143
|
+
return table["tenantId"] !== undefined;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Drizzle's terminal builders (insert, update().where, delete().where) are
|
|
147
|
+
// thenable — `.then` is there so `await` works — but the declared return
|
|
148
|
+
// types don't include PromiseLike. Cast via this helper so the double-
|
|
149
|
+
// cast is named and lives in exactly one place per scope.
|
|
150
|
+
function asDrizzleThenable<T>(builder: unknown): PromiseLike<T> {
|
|
151
|
+
return builder as PromiseLike<T>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Wrap a DB query promise in a `db.query` span + emit the DB duration
|
|
155
|
+
// histogram. Row count is recorded when the result is an array (SELECTs
|
|
156
|
+
// + *.returning()). Metric is emitted both on success and on throw so
|
|
157
|
+
// slow failing queries show up too.
|
|
158
|
+
function withDbSpan<T>(
|
|
159
|
+
operation: "select" | "insert" | "update" | "delete",
|
|
160
|
+
table: Table,
|
|
161
|
+
exec: () => PromiseLike<T>,
|
|
162
|
+
): PromiseLike<T> {
|
|
163
|
+
// Pre-flight cancellation. Sits above the early-return so the check
|
|
164
|
+
// fires regardless of observability config — cancellation is a
|
|
165
|
+
// correctness feature, not an observability one.
|
|
166
|
+
signal?.throwIfAborted();
|
|
167
|
+
if (!tracer && !meter) return exec();
|
|
168
|
+
const tableName = getTableName(table);
|
|
169
|
+
const start = performance.now();
|
|
170
|
+
const emitMetric = () => {
|
|
171
|
+
if (meter) {
|
|
172
|
+
emitDbQuery(meter, { operation, table: tableName }, (performance.now() - start) / 1000);
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
if (!tracer) {
|
|
177
|
+
// Tracer absent but meter present: just time + emit, no span.
|
|
178
|
+
return (async () => {
|
|
179
|
+
try {
|
|
180
|
+
return await exec();
|
|
181
|
+
} finally {
|
|
182
|
+
emitMetric();
|
|
183
|
+
}
|
|
184
|
+
})();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return tracer.withSpan(
|
|
188
|
+
"db.query",
|
|
189
|
+
{
|
|
190
|
+
kind: "client",
|
|
191
|
+
attributes: {
|
|
192
|
+
"db.system": "postgresql",
|
|
193
|
+
"db.operation": operation,
|
|
194
|
+
"db.table": tableName,
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
async (span) => {
|
|
198
|
+
try {
|
|
199
|
+
const result = await exec();
|
|
200
|
+
if (Array.isArray(result)) {
|
|
201
|
+
span.setAttribute("db.row_count", result.length);
|
|
202
|
+
}
|
|
203
|
+
return result;
|
|
204
|
+
} finally {
|
|
205
|
+
emitMetric();
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// --- Read filter (SELECT WHERE clause) ---
|
|
212
|
+
//
|
|
213
|
+
// Reads in tenant mode see their own rows AND global reference data (rows
|
|
214
|
+
// with tenantId = SYSTEM_TENANT_ID). Writes explicitly do NOT — see writeFilter.
|
|
215
|
+
|
|
216
|
+
function readFilter(table: Table, ...extra: SQL[]): SQL | undefined {
|
|
217
|
+
if (!hasTenantColumn(table)) {
|
|
218
|
+
return extra.length > 0 ? and(...extra) : undefined;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (mode === "system") {
|
|
222
|
+
// System mode: no tenant restriction, only pass through extra conditions
|
|
223
|
+
return extra.length > 0 ? and(...extra) : undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Tenant mode: own data + reference data (zero-UUID tenantId for global rows).
|
|
227
|
+
// Drizzle's `or()` is typed `SQL | undefined` (variadic-empty case); both
|
|
228
|
+
// `eq()` args always produce SQL, so the cast documents that assumption.
|
|
229
|
+
const ownOrGlobal = or(
|
|
230
|
+
eq(table["tenantId"], tenantId),
|
|
231
|
+
eq(table["tenantId"], SYSTEM_TENANT_ID),
|
|
232
|
+
) as SQL;
|
|
233
|
+
return extra.length > 0 ? and(ownOrGlobal, ...extra) : ownOrGlobal;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// --- Write filter (UPDATE/DELETE WHERE clause) ---
|
|
237
|
+
//
|
|
238
|
+
// Writes in tenant mode must NEVER match reference rows — otherwise a tenant
|
|
239
|
+
// could mutate global data by coincidence of id/condition. Only system-scope
|
|
240
|
+
// (r.systemScope()) may modify reference data.
|
|
241
|
+
|
|
242
|
+
function writeFilter(table: Table, ...extra: SQL[]): SQL | undefined {
|
|
243
|
+
if (!hasTenantColumn(table)) {
|
|
244
|
+
return extra.length > 0 ? and(...extra) : undefined;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (mode === "system") {
|
|
248
|
+
return extra.length > 0 ? and(...extra) : undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const ownOnly = eq(table["tenantId"], tenantId);
|
|
252
|
+
return extra.length > 0 ? and(ownOnly, ...extra) : ownOnly;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// --- Write values (INSERT tenantId handling) ---
|
|
256
|
+
|
|
257
|
+
function insertValues(table: Table, data: Record<string, unknown>): Record<string, unknown> {
|
|
258
|
+
if (!hasTenantColumn(table)) return data;
|
|
259
|
+
|
|
260
|
+
if (mode === "system") {
|
|
261
|
+
// System mode: tenantId is a default the handler can override —
|
|
262
|
+
// e.g. to write a cross-tenant row under SYSTEM_TENANT_ID, or to
|
|
263
|
+
// target a foreign tenant's projection from a SystemAdmin action.
|
|
264
|
+
return { tenantId, ...data };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Tenant mode: tenantId is forced, handler cannot override
|
|
268
|
+
return { ...data, tenantId };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// --- Select wrapper (lazy filter + chainable) ---
|
|
272
|
+
|
|
273
|
+
function wrapSelect(
|
|
274
|
+
// biome-ignore lint/suspicious/noExplicitAny: Drizzle internal query type
|
|
275
|
+
query: any,
|
|
276
|
+
table: Table,
|
|
277
|
+
filtered: boolean,
|
|
278
|
+
): TenantSelectQuery {
|
|
279
|
+
function ensureFiltered() {
|
|
280
|
+
if (filtered) return query;
|
|
281
|
+
const filter = readFilter(table);
|
|
282
|
+
return filter ? query.where(filter) : query;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
where(condition: SQL) {
|
|
287
|
+
const filter = readFilter(table, condition);
|
|
288
|
+
return wrapSelect(filter ? query.where(filter) : query.where(condition), table, true);
|
|
289
|
+
},
|
|
290
|
+
limit(n: number) {
|
|
291
|
+
return wrapSelect(ensureFiltered().limit(n), table, true);
|
|
292
|
+
},
|
|
293
|
+
offset(n: number) {
|
|
294
|
+
return wrapSelect(ensureFiltered().offset(n), table, true);
|
|
295
|
+
},
|
|
296
|
+
orderBy(...columns: SQL[]) {
|
|
297
|
+
return wrapSelect(ensureFiltered().orderBy(...columns), table, true);
|
|
298
|
+
},
|
|
299
|
+
for(strength: RowLockStrength) {
|
|
300
|
+
return wrapSelect(ensureFiltered().for(strength), table, true);
|
|
301
|
+
},
|
|
302
|
+
// biome-ignore lint/suspicious/noThenProperty: thenable for await
|
|
303
|
+
then(
|
|
304
|
+
resolve: ((value: Record<string, unknown>[]) => void) | null,
|
|
305
|
+
reject: ((reason: unknown) => void) | null,
|
|
306
|
+
) {
|
|
307
|
+
return withDbSpan<Record<string, unknown>[]>("select", table, () => ensureFiltered()).then(
|
|
308
|
+
(rows) => resolve?.(rows),
|
|
309
|
+
reject ?? undefined,
|
|
310
|
+
);
|
|
311
|
+
},
|
|
312
|
+
} as TenantSelectQuery;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// --- Where helper for update/delete ---
|
|
316
|
+
|
|
317
|
+
function whereClause(table: Table, condition: SQL): SQL {
|
|
318
|
+
const filter = writeFilter(table, condition);
|
|
319
|
+
return filter ?? condition;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return {
|
|
323
|
+
tenantId,
|
|
324
|
+
mode,
|
|
325
|
+
raw: db,
|
|
326
|
+
|
|
327
|
+
select(columns?: ColumnSelection) {
|
|
328
|
+
return {
|
|
329
|
+
from(table: Table) {
|
|
330
|
+
const baseQuery = columns ? db.select(columns).from(table) : db.select().from(table);
|
|
331
|
+
return wrapSelect(baseQuery, table, false);
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
insert(table: Table) {
|
|
337
|
+
return {
|
|
338
|
+
values(data: Record<string, unknown>) {
|
|
339
|
+
const q = db.insert(table).values(insertValues(table, data));
|
|
340
|
+
return {
|
|
341
|
+
returning() {
|
|
342
|
+
return withDbSpan<Record<string, unknown>[]>(
|
|
343
|
+
"insert",
|
|
344
|
+
table,
|
|
345
|
+
() => q.returning() as PromiseLike<Record<string, unknown>[]>,
|
|
346
|
+
);
|
|
347
|
+
},
|
|
348
|
+
onConflictDoUpdate(spec: ConflictUpdate) {
|
|
349
|
+
return withDbSpan<void>("insert", table, () =>
|
|
350
|
+
(
|
|
351
|
+
q as unknown as {
|
|
352
|
+
onConflictDoUpdate: (s: ConflictUpdate) => PromiseLike<void>;
|
|
353
|
+
}
|
|
354
|
+
).onConflictDoUpdate(spec),
|
|
355
|
+
);
|
|
356
|
+
},
|
|
357
|
+
onConflictDoNothing(spec?: { target: ConflictTarget }) {
|
|
358
|
+
return withDbSpan<void>("insert", table, () =>
|
|
359
|
+
(
|
|
360
|
+
q as unknown as {
|
|
361
|
+
onConflictDoNothing: (s?: { target: ConflictTarget }) => PromiseLike<void>;
|
|
362
|
+
}
|
|
363
|
+
).onConflictDoNothing(spec),
|
|
364
|
+
);
|
|
365
|
+
},
|
|
366
|
+
// biome-ignore lint/suspicious/noThenProperty: thenable for await
|
|
367
|
+
then(resolve, reject) {
|
|
368
|
+
return withDbSpan<void>("insert", table, () => asDrizzleThenable<void>(q)).then(
|
|
369
|
+
resolve,
|
|
370
|
+
reject,
|
|
371
|
+
);
|
|
372
|
+
},
|
|
373
|
+
} as TenantInsertValues;
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
update(table: Table) {
|
|
379
|
+
return {
|
|
380
|
+
set(data: Record<string, unknown>) {
|
|
381
|
+
const q = db.update(table).set(data);
|
|
382
|
+
return {
|
|
383
|
+
where(condition: SQL) {
|
|
384
|
+
const wq = q.where(whereClause(table, condition));
|
|
385
|
+
return {
|
|
386
|
+
returning() {
|
|
387
|
+
return withDbSpan<Record<string, unknown>[]>(
|
|
388
|
+
"update",
|
|
389
|
+
table,
|
|
390
|
+
() => wq.returning() as PromiseLike<Record<string, unknown>[]>,
|
|
391
|
+
);
|
|
392
|
+
},
|
|
393
|
+
// biome-ignore lint/suspicious/noThenProperty: thenable for await
|
|
394
|
+
then(resolve, reject) {
|
|
395
|
+
return withDbSpan<void>("update", table, () => asDrizzleThenable<void>(wq)).then(
|
|
396
|
+
resolve,
|
|
397
|
+
reject,
|
|
398
|
+
);
|
|
399
|
+
},
|
|
400
|
+
} as TenantUpdateWhere;
|
|
401
|
+
},
|
|
402
|
+
returning(): PromiseLike<Record<string, unknown>[]> {
|
|
403
|
+
return Promise.reject(
|
|
404
|
+
new Error(
|
|
405
|
+
"TenantDb.update().set().returning() without .where() would mass-update all tenant rows. " +
|
|
406
|
+
"Add .where(...) first, or call .set(...).where(...).returning().",
|
|
407
|
+
),
|
|
408
|
+
);
|
|
409
|
+
},
|
|
410
|
+
// biome-ignore lint/suspicious/noThenProperty: thenable for await
|
|
411
|
+
then(resolve, reject) {
|
|
412
|
+
return Promise.reject(
|
|
413
|
+
new Error(
|
|
414
|
+
"TenantDb.update().set() awaited without .where() would mass-update all tenant rows. " +
|
|
415
|
+
"Add .where(...) before awaiting.",
|
|
416
|
+
),
|
|
417
|
+
).then(resolve, reject);
|
|
418
|
+
},
|
|
419
|
+
} as TenantUpdateSet;
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
},
|
|
423
|
+
|
|
424
|
+
delete(table: Table) {
|
|
425
|
+
return {
|
|
426
|
+
where(condition: SQL) {
|
|
427
|
+
return withDbSpan<void>("delete", table, () =>
|
|
428
|
+
asDrizzleThenable<void>(db.delete(table).where(whereClause(table, condition))),
|
|
429
|
+
);
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
},
|
|
433
|
+
};
|
|
434
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { createRegistry, defineFeature } from "../index";
|
|
3
|
+
|
|
4
|
+
describe("r.authClaims() — registrar collection", () => {
|
|
5
|
+
test("feature without authClaims has an empty hooks list", () => {
|
|
6
|
+
const feature = defineFeature("empty", () => {});
|
|
7
|
+
expect(feature.authClaimsHooks).toEqual([]);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("single authClaims call stores the fn on the feature definition", () => {
|
|
11
|
+
const hook = async () => ({ teamId: "t-1" });
|
|
12
|
+
const feature = defineFeature("drivers", (r) => {
|
|
13
|
+
r.authClaims(hook);
|
|
14
|
+
});
|
|
15
|
+
expect(feature.authClaimsHooks).toHaveLength(1);
|
|
16
|
+
expect(feature.authClaimsHooks[0]).toBe(hook);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("multiple authClaims calls inside one feature are all retained (last-wins is a merge concern, not a storage concern)", () => {
|
|
20
|
+
const feature = defineFeature("billing", (r) => {
|
|
21
|
+
r.authClaims(async () => ({ plan: "free" }));
|
|
22
|
+
r.authClaims(async () => ({ plan: "pro" }));
|
|
23
|
+
});
|
|
24
|
+
expect(feature.authClaimsHooks).toHaveLength(2);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
describe("Registry.getAuthClaimsHooks — aggregation across features", () => {
|
|
29
|
+
test("empty registry has no hooks", () => {
|
|
30
|
+
const reg = createRegistry([]);
|
|
31
|
+
expect(reg.getAuthClaimsHooks()).toEqual([]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("aggregates hooks from multiple features with feature name tagged", () => {
|
|
35
|
+
const driversFeature = defineFeature("drivers", (r) => {
|
|
36
|
+
r.authClaims(async () => ({ teamId: "t-1" }));
|
|
37
|
+
});
|
|
38
|
+
const billingFeature = defineFeature("billing", (r) => {
|
|
39
|
+
r.authClaims(async () => ({ plan: "pro" }));
|
|
40
|
+
});
|
|
41
|
+
const reg = createRegistry([driversFeature, billingFeature]);
|
|
42
|
+
|
|
43
|
+
const hooks = reg.getAuthClaimsHooks();
|
|
44
|
+
expect(hooks).toHaveLength(2);
|
|
45
|
+
|
|
46
|
+
const names = hooks.map((h) => h.featureName).sort();
|
|
47
|
+
expect(names).toEqual(["billing", "drivers"]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("preserves registration order within a feature", () => {
|
|
51
|
+
const feature = defineFeature("billing", (r) => {
|
|
52
|
+
r.authClaims(async () => ({ x: 1 }));
|
|
53
|
+
r.authClaims(async () => ({ x: 2 }));
|
|
54
|
+
});
|
|
55
|
+
const reg = createRegistry([feature]);
|
|
56
|
+
|
|
57
|
+
const hooks = reg.getAuthClaimsHooks();
|
|
58
|
+
expect(hooks).toHaveLength(2);
|
|
59
|
+
// Both carry the same featureName; the resolver decides the merge policy.
|
|
60
|
+
expect(hooks.every((h) => h.featureName === "billing")).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("features without r.authClaims contribute nothing", () => {
|
|
64
|
+
const plain = defineFeature("plain", () => {});
|
|
65
|
+
const withClaims = defineFeature("drivers", (r) => {
|
|
66
|
+
r.authClaims(async () => ({ teamId: "t-1" }));
|
|
67
|
+
});
|
|
68
|
+
const reg = createRegistry([plain, withClaims]);
|
|
69
|
+
|
|
70
|
+
const hooks = reg.getAuthClaimsHooks();
|
|
71
|
+
expect(hooks).toHaveLength(1);
|
|
72
|
+
expect(hooks[0]?.featureName).toBe("drivers");
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Boot-Validator-Tests für locatedBy-Markers.
|
|
2
|
+
//
|
|
3
|
+
// Ein Timestamp-Feld mit `locatedBy: "X"` muss ein bestehendes tz-Feld "X"
|
|
4
|
+
// in derselben Entity referenzieren. Sonst silent data loss — wir wollen
|
|
5
|
+
// fail-fast beim Boot.
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "vitest";
|
|
8
|
+
import { validateBoot } from "../boot-validator";
|
|
9
|
+
import { defineFeature } from "../define-feature";
|
|
10
|
+
import { createEntity, createTimestampField, createTzField, locatedTimestamp } from "../factories";
|
|
11
|
+
|
|
12
|
+
describe("validateBoot — locatedBy markers", () => {
|
|
13
|
+
test("locatedTimestamp(name) Helper-Pair passiert validiert (positive case)", () => {
|
|
14
|
+
const feature = defineFeature("test", (r) => {
|
|
15
|
+
r.entity(
|
|
16
|
+
"order",
|
|
17
|
+
createEntity({
|
|
18
|
+
fields: {
|
|
19
|
+
...locatedTimestamp("pickup"),
|
|
20
|
+
...locatedTimestamp("delivery"),
|
|
21
|
+
},
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
expect(() => validateBoot([feature])).not.toThrow();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("manuelle Konstruktion mit korrektem Pair passiert (positive case)", () => {
|
|
30
|
+
const feature = defineFeature("test", (r) => {
|
|
31
|
+
r.entity(
|
|
32
|
+
"order",
|
|
33
|
+
createEntity({
|
|
34
|
+
fields: {
|
|
35
|
+
customAt: createTimestampField({ locatedBy: "customTz" }),
|
|
36
|
+
customTz: createTzField(),
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(() => validateBoot([feature])).not.toThrow();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("locatedBy zeigt auf nicht-existierendes Feld → Fehler beim Boot", () => {
|
|
46
|
+
const feature = defineFeature("test", (r) => {
|
|
47
|
+
r.entity(
|
|
48
|
+
"order",
|
|
49
|
+
createEntity({
|
|
50
|
+
fields: {
|
|
51
|
+
pickupAt: createTimestampField({ locatedBy: "pickupTz" }),
|
|
52
|
+
// pickupTz fehlt komplett!
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
expect(() => validateBoot([feature])).toThrow(/no field with that name exists/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("locatedBy zeigt auf falschen Feld-Typ → Fehler beim Boot", () => {
|
|
62
|
+
const feature = defineFeature("test", (r) => {
|
|
63
|
+
r.entity(
|
|
64
|
+
"order",
|
|
65
|
+
createEntity({
|
|
66
|
+
fields: {
|
|
67
|
+
pickupAt: createTimestampField({ locatedBy: "pickupTz" }),
|
|
68
|
+
// text statt tz — typo-Falle die der Validator fängt
|
|
69
|
+
pickupTz: { type: "text", maxLength: 100 },
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(() => validateBoot([feature])).toThrow(/expected "tz"/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("Fehlermeldung verweist auf locatedTimestamp-Helper als Fix", () => {
|
|
79
|
+
const feature = defineFeature("test", (r) => {
|
|
80
|
+
r.entity(
|
|
81
|
+
"order",
|
|
82
|
+
createEntity({
|
|
83
|
+
fields: {
|
|
84
|
+
pickupAt: createTimestampField({ locatedBy: "pickupTz" }),
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(() => validateBoot([feature])).toThrow(/locatedTimestamp\("pickup"\)/);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("Timestamp ohne locatedBy ist OK (reiner UTC-Instant)", () => {
|
|
94
|
+
const feature = defineFeature("test", (r) => {
|
|
95
|
+
r.entity(
|
|
96
|
+
"order",
|
|
97
|
+
createEntity({
|
|
98
|
+
fields: {
|
|
99
|
+
createdAt: createTimestampField(),
|
|
100
|
+
actualPickupAt: createTimestampField({ required: true }),
|
|
101
|
+
},
|
|
102
|
+
}),
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(() => validateBoot([feature])).not.toThrow();
|
|
107
|
+
});
|
|
108
|
+
});
|