@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,154 @@
|
|
|
1
|
+
// Tests für buildAppSchema. Drei Invarianten pinnen:
|
|
2
|
+
// 1. Multi-Feature: jedes Feature kommt mit eigenem featureName +
|
|
3
|
+
// seinen Entities/Screens/Navs in der features-Liste an.
|
|
4
|
+
// 2. Workspaces werden mit aufgelösten navMembers (cross-feature
|
|
5
|
+
// gemerged) auf AppSchema-Ebene gehoben.
|
|
6
|
+
// 3. JSON-Safety: function defaults und Zod-validators werden im
|
|
7
|
+
// projection-Schritt rausgefiltert — sonst landet Server-Runtime
|
|
8
|
+
// im Browser-Bundle.
|
|
9
|
+
|
|
10
|
+
import { describe, expect, test } from "vitest";
|
|
11
|
+
import { buildAppSchema } from "../build-app-schema";
|
|
12
|
+
import { defineFeature } from "../define-feature";
|
|
13
|
+
import { createRegistry } from "../registry";
|
|
14
|
+
import type { EntityDefinition } from "../types/fields";
|
|
15
|
+
|
|
16
|
+
describe("buildAppSchema", () => {
|
|
17
|
+
test("Multi-Feature: jedes Feature wird mit eigenem featureName projiziert", () => {
|
|
18
|
+
const orderEntity = {
|
|
19
|
+
table: "orders",
|
|
20
|
+
fields: { label: { type: "text" } },
|
|
21
|
+
} as unknown as EntityDefinition;
|
|
22
|
+
const fleetEntity = {
|
|
23
|
+
table: "vehicles",
|
|
24
|
+
fields: { plate: { type: "text" } },
|
|
25
|
+
} as unknown as EntityDefinition;
|
|
26
|
+
|
|
27
|
+
const orderFeature = defineFeature("orders", (r) => {
|
|
28
|
+
r.entity("order", orderEntity);
|
|
29
|
+
r.screen({ id: "list", type: "entityList", entity: "order", columns: ["label"] });
|
|
30
|
+
r.nav({ id: "list", label: "Order List" });
|
|
31
|
+
});
|
|
32
|
+
const fleetFeature = defineFeature("fleet", (r) => {
|
|
33
|
+
r.entity("vehicle", fleetEntity);
|
|
34
|
+
r.screen({ id: "list", type: "entityList", entity: "vehicle", columns: ["plate"] });
|
|
35
|
+
r.nav({ id: "list", label: "Fleet List" });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const registry = createRegistry([orderFeature, fleetFeature]);
|
|
39
|
+
const app = buildAppSchema(registry);
|
|
40
|
+
|
|
41
|
+
expect(app.features.map((f) => f.featureName).sort()).toEqual(["fleet", "orders"]);
|
|
42
|
+
const orders = app.features.find((f) => f.featureName === "orders");
|
|
43
|
+
const fleet = app.features.find((f) => f.featureName === "fleet");
|
|
44
|
+
expect(orders?.screens).toHaveLength(1);
|
|
45
|
+
expect(orders?.navs).toHaveLength(1);
|
|
46
|
+
expect(orders?.entities["order"]).toBeDefined();
|
|
47
|
+
expect(fleet?.entities["vehicle"]).toBeDefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("Workspaces — definition + aufgelöste navMembers landen auf AppSchema-Ebene", () => {
|
|
51
|
+
const ordersFeature = defineFeature("orders", (r) => {
|
|
52
|
+
r.nav({ id: "list", label: "List" });
|
|
53
|
+
});
|
|
54
|
+
const fleetFeature = defineFeature("fleet", (r) => {
|
|
55
|
+
r.nav({ id: "vehicles", label: "Vehicles" });
|
|
56
|
+
});
|
|
57
|
+
const adminFeature = defineFeature("app", (r) => {
|
|
58
|
+
r.workspace({
|
|
59
|
+
id: "admin",
|
|
60
|
+
label: "Admin",
|
|
61
|
+
access: { openToAll: true },
|
|
62
|
+
nav: ["orders:nav:list", "fleet:nav:vehicles"],
|
|
63
|
+
default: true,
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const registry = createRegistry([ordersFeature, fleetFeature, adminFeature]);
|
|
68
|
+
const app = buildAppSchema(registry);
|
|
69
|
+
|
|
70
|
+
expect(app.workspaces).toHaveLength(1);
|
|
71
|
+
const admin = app.workspaces?.[0];
|
|
72
|
+
// Short id — Renderer matcht gegen URL-Segment ("/admin/...") und
|
|
73
|
+
// erwartet die kurze Form. Registry intern qualifiziert, buildAppSchema
|
|
74
|
+
// projeziert zurück auf short.
|
|
75
|
+
expect(admin?.definition.id).toBe("admin");
|
|
76
|
+
// Cross-feature merge: beide Members sind drin, der Workspace-Owner
|
|
77
|
+
// (`app`) sieht die anderen Features ohne dass er sie importiert.
|
|
78
|
+
expect(admin?.navMembers).toEqual(["orders:nav:list", "fleet:nav:vehicles"]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("Apps ohne Workspaces lassen das Feld weg (omit-undefined-Pattern)", () => {
|
|
82
|
+
const f = defineFeature("only", (r) => {
|
|
83
|
+
r.nav({ id: "x", label: "X" });
|
|
84
|
+
});
|
|
85
|
+
const app = buildAppSchema(createRegistry([f]));
|
|
86
|
+
expect(app.workspaces).toBeUndefined();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("JSON-Safety: Function-Defaults werden in der Projection rausgefiltert", () => {
|
|
90
|
+
// Field mit function-default — typisch z.B. () => generateId(). Auf
|
|
91
|
+
// dem Server legitimer Code, im Browser-Bundle aber unbrauchbar weil
|
|
92
|
+
// die Function auf Server-Internals zugreifen würde. Projection muss
|
|
93
|
+
// den default-Slot weglassen, nicht die Function durchlassen.
|
|
94
|
+
const entity = {
|
|
95
|
+
fields: {
|
|
96
|
+
id: { type: "text", default: () => "would-be-runtime-id" },
|
|
97
|
+
title: { type: "text" },
|
|
98
|
+
},
|
|
99
|
+
} as unknown as EntityDefinition;
|
|
100
|
+
|
|
101
|
+
const f = defineFeature("ent", (r) => {
|
|
102
|
+
r.entity("thing", entity);
|
|
103
|
+
});
|
|
104
|
+
const app = buildAppSchema(createRegistry([f]));
|
|
105
|
+
const projectedEntity = app.features[0]?.entities["thing"] as unknown as {
|
|
106
|
+
fields: Record<string, Record<string, unknown>>;
|
|
107
|
+
};
|
|
108
|
+
const idField = projectedEntity.fields["id"];
|
|
109
|
+
expect(idField).toBeDefined();
|
|
110
|
+
expect(idField?.["default"]).toBeUndefined(); // Function abgewiesen
|
|
111
|
+
expect(idField?.["type"]).toBe("text"); // type kommt durch
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("JSON-Safety: literal Defaults bleiben erhalten", () => {
|
|
115
|
+
const entity = {
|
|
116
|
+
fields: {
|
|
117
|
+
active: { type: "boolean", default: false },
|
|
118
|
+
count: { type: "number", default: 0 },
|
|
119
|
+
label: { type: "text", default: "" },
|
|
120
|
+
},
|
|
121
|
+
} as unknown as EntityDefinition;
|
|
122
|
+
|
|
123
|
+
const f = defineFeature("ent", (r) => {
|
|
124
|
+
r.entity("thing", entity);
|
|
125
|
+
});
|
|
126
|
+
const app = buildAppSchema(createRegistry([f]));
|
|
127
|
+
const fields = (
|
|
128
|
+
app.features[0]?.entities["thing"] as unknown as {
|
|
129
|
+
fields: Record<string, Record<string, unknown>>;
|
|
130
|
+
}
|
|
131
|
+
).fields;
|
|
132
|
+
expect(fields["active"]?.["default"]).toBe(false);
|
|
133
|
+
expect(fields["count"]?.["default"]).toBe(0);
|
|
134
|
+
expect(fields["label"]?.["default"]).toBe("");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("AppSchema ist via JSON.stringify roundtrip-sicher", () => {
|
|
138
|
+
// Echter Smoke-Test des Vertrags — wenn jemand in den project-
|
|
139
|
+
// Helper eine Function reinschmuggelt, würde das hier brennen.
|
|
140
|
+
const entity = {
|
|
141
|
+
fields: { id: { type: "text", default: () => "x" } },
|
|
142
|
+
} as unknown as EntityDefinition;
|
|
143
|
+
const f = defineFeature("ent", (r) => {
|
|
144
|
+
r.entity("thing", entity);
|
|
145
|
+
r.nav({ id: "n", label: "N" });
|
|
146
|
+
r.workspace({ id: "ws", label: "Ws", access: { openToAll: true } });
|
|
147
|
+
});
|
|
148
|
+
const app = buildAppSchema(createRegistry([f]));
|
|
149
|
+
const json = JSON.stringify(app);
|
|
150
|
+
const parsed = JSON.parse(json);
|
|
151
|
+
// Feature-namen identisch nach Roundtrip
|
|
152
|
+
expect(parsed.features[0].featureName).toBe("ent");
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { createRegistry, defineFeature, readClaim } from "../index";
|
|
3
|
+
import type { ClaimKeyHandle, SessionUser } from "../types";
|
|
4
|
+
|
|
5
|
+
// --- r.claimKey() registration ---
|
|
6
|
+
|
|
7
|
+
describe("r.claimKey() — registration", () => {
|
|
8
|
+
test("feature without claimKey has an empty claimKeys record", () => {
|
|
9
|
+
const feature = defineFeature("plain", () => {});
|
|
10
|
+
expect(feature.claimKeys).toEqual({});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("single claimKey is stored with qualified name '<feature>:<shortName>' (no kebab conversion)", () => {
|
|
14
|
+
const feature = defineFeature("drivers", (r) => {
|
|
15
|
+
r.claimKey("teamId", { type: "string" });
|
|
16
|
+
});
|
|
17
|
+
expect(feature.claimKeys["teamId"]).toBeDefined();
|
|
18
|
+
// Claim keys are NOT QNs — they keep the raw camelCase shortName so the
|
|
19
|
+
// authClaims resolver's `<feature>:<innerKey>` merge finds them.
|
|
20
|
+
expect(feature.claimKeys["teamId"]?.qualifiedName).toBe("drivers:teamId");
|
|
21
|
+
expect(feature.claimKeys["teamId"]?.type).toBe("string");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("returns a typed ClaimKeyHandle whose `name` is the qualified key", () => {
|
|
25
|
+
let handle: ClaimKeyHandle | undefined;
|
|
26
|
+
defineFeature("drivers", (r) => {
|
|
27
|
+
handle = r.claimKey("teamId", { type: "string" });
|
|
28
|
+
});
|
|
29
|
+
expect(handle?.name).toBe("drivers:teamId");
|
|
30
|
+
expect(handle?.type).toBe("string");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("duplicate short-name within one feature throws", () => {
|
|
34
|
+
expect(() =>
|
|
35
|
+
defineFeature("drivers", (r) => {
|
|
36
|
+
r.claimKey("teamId", { type: "string" });
|
|
37
|
+
r.claimKey("teamId", { type: "number" });
|
|
38
|
+
}),
|
|
39
|
+
).toThrow(/Claim key "teamId" already declared/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("camelCase feature + shortName are preserved as-is", () => {
|
|
43
|
+
const feature = defineFeature("driverOrders", (r) => {
|
|
44
|
+
r.claimKey("regionId", { type: "string" });
|
|
45
|
+
});
|
|
46
|
+
expect(feature.claimKeys["regionId"]?.qualifiedName).toBe("driverOrders:regionId");
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// --- Registry aggregation ---
|
|
51
|
+
|
|
52
|
+
describe("Registry.getAllClaimKeys / getClaimKey", () => {
|
|
53
|
+
test("aggregates keys from all features", () => {
|
|
54
|
+
const drivers = defineFeature("drivers", (r) => {
|
|
55
|
+
r.claimKey("teamId", { type: "string" });
|
|
56
|
+
r.claimKey("regionId", { type: "number" });
|
|
57
|
+
});
|
|
58
|
+
const billing = defineFeature("billing", (r) => {
|
|
59
|
+
r.claimKey("plan", { type: "string" });
|
|
60
|
+
});
|
|
61
|
+
const reg = createRegistry([drivers, billing]);
|
|
62
|
+
|
|
63
|
+
const all = reg.getAllClaimKeys();
|
|
64
|
+
expect(all.size).toBe(3);
|
|
65
|
+
expect(all.has("drivers:teamId")).toBe(true);
|
|
66
|
+
expect(all.has("drivers:regionId")).toBe(true);
|
|
67
|
+
expect(all.has("billing:plan")).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("getClaimKey returns the definition by qualified name", () => {
|
|
71
|
+
const drivers = defineFeature("drivers", (r) => {
|
|
72
|
+
r.claimKey("teamId", { type: "string" });
|
|
73
|
+
});
|
|
74
|
+
const reg = createRegistry([drivers]);
|
|
75
|
+
|
|
76
|
+
const def = reg.getClaimKey("drivers:teamId");
|
|
77
|
+
expect(def?.type).toBe("string");
|
|
78
|
+
expect(def?.shortName).toBe("teamId");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("unknown qualified name → undefined", () => {
|
|
82
|
+
const reg = createRegistry([]);
|
|
83
|
+
expect(reg.getClaimKey("whoever:xyz")).toBeUndefined();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// --- readClaim() helper ---
|
|
88
|
+
|
|
89
|
+
describe("readClaim() — type-narrowed claim access", () => {
|
|
90
|
+
const baseUser: SessionUser = {
|
|
91
|
+
id: "11111111-0000-4000-8000-000000000001",
|
|
92
|
+
tenantId: "22222222-0000-4000-8000-000000000001",
|
|
93
|
+
roles: ["User"],
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
function userWithClaims(claims: Record<string, unknown>): SessionUser {
|
|
97
|
+
return { ...baseUser, claims };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
test("returns the value cast to the handle's JS type — string", () => {
|
|
101
|
+
const drivers = defineFeature("drivers", (r) => {
|
|
102
|
+
r.claimKey("teamId", { type: "string" });
|
|
103
|
+
});
|
|
104
|
+
const handle = drivers.claimKeys["teamId"];
|
|
105
|
+
if (!handle) throw new Error("handle missing");
|
|
106
|
+
const typedHandle: ClaimKeyHandle<"string"> = { name: handle.qualifiedName, type: "string" };
|
|
107
|
+
|
|
108
|
+
const user = userWithClaims({ "drivers:teamId": "eng" });
|
|
109
|
+
const teamId = readClaim(user, typedHandle);
|
|
110
|
+
// Type-narrowing: TS treats this as `string | undefined` — we just
|
|
111
|
+
// assert the runtime value came through untouched.
|
|
112
|
+
expect(teamId).toBe("eng");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("number type", () => {
|
|
116
|
+
const handle: ClaimKeyHandle<"number"> = {
|
|
117
|
+
name: "drivers:region-id",
|
|
118
|
+
type: "number",
|
|
119
|
+
};
|
|
120
|
+
const user = userWithClaims({ "drivers:region-id": 7 });
|
|
121
|
+
expect(readClaim(user, handle)).toBe(7);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("boolean type", () => {
|
|
125
|
+
const handle: ClaimKeyHandle<"boolean"> = {
|
|
126
|
+
name: "flags:is-beta",
|
|
127
|
+
type: "boolean",
|
|
128
|
+
};
|
|
129
|
+
const user = userWithClaims({ "flags:is-beta": true });
|
|
130
|
+
expect(readClaim(user, handle)).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("string[] type", () => {
|
|
134
|
+
const handle: ClaimKeyHandle<"string[]"> = {
|
|
135
|
+
name: "flags:enabled",
|
|
136
|
+
type: "string[]",
|
|
137
|
+
};
|
|
138
|
+
const user = userWithClaims({ "flags:enabled": ["dark-mode", "new-checkout"] });
|
|
139
|
+
expect(readClaim(user, handle)).toEqual(["dark-mode", "new-checkout"]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("object type", () => {
|
|
143
|
+
const handle: ClaimKeyHandle<"object"> = {
|
|
144
|
+
name: "drivers:metadata",
|
|
145
|
+
type: "object",
|
|
146
|
+
};
|
|
147
|
+
const user = userWithClaims({ "drivers:metadata": { level: 5, tags: ["a"] } });
|
|
148
|
+
expect(readClaim(user, handle)).toEqual({ level: 5, tags: ["a"] });
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("undefined when user has no claims at all", () => {
|
|
152
|
+
const handle: ClaimKeyHandle<"string"> = {
|
|
153
|
+
name: "drivers:team-id",
|
|
154
|
+
type: "string",
|
|
155
|
+
};
|
|
156
|
+
// baseUser has no `claims` field at all.
|
|
157
|
+
expect(readClaim(baseUser, handle)).toBeUndefined();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("undefined when claims is set but the specific key is missing", () => {
|
|
161
|
+
const handle: ClaimKeyHandle<"string"> = {
|
|
162
|
+
name: "drivers:team-id",
|
|
163
|
+
type: "string",
|
|
164
|
+
};
|
|
165
|
+
const user = userWithClaims({ "other:key": "x" });
|
|
166
|
+
expect(readClaim(user, handle)).toBeUndefined();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("null value is treated the same as missing — returns undefined", () => {
|
|
170
|
+
// A feature that returned null instead of an actual string shouldn't
|
|
171
|
+
// surface as "null" through the typed helper — the contract is
|
|
172
|
+
// "present or undefined".
|
|
173
|
+
const handle: ClaimKeyHandle<"string"> = {
|
|
174
|
+
name: "drivers:team-id",
|
|
175
|
+
type: "string",
|
|
176
|
+
};
|
|
177
|
+
const user = userWithClaims({ "drivers:team-id": null });
|
|
178
|
+
expect(readClaim(user, handle)).toBeUndefined();
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// --- Round-trip: r.claimKey → r.authClaims return → resolver merge → readClaim ---
|
|
183
|
+
//
|
|
184
|
+
// Regression guard against the class of bugs where the three stages of a
|
|
185
|
+
// claim's lifecycle disagree on the key string:
|
|
186
|
+
//
|
|
187
|
+
// 1. r.claimKey creates the handle (`handle.name`)
|
|
188
|
+
// 2. r.authClaims hook returns `{ inner: value }` — framework prefixes
|
|
189
|
+
// to `"<feature>:<inner>"` at merge time
|
|
190
|
+
// 3. readClaim(user, handle) looks up `user.claims[handle.name]`
|
|
191
|
+
//
|
|
192
|
+
// If any stage applies a different transform (kebab-case on one side but
|
|
193
|
+
// not the other, different prefix convention, ...) the round-trip silently
|
|
194
|
+
// breaks — readClaim returns undefined even though the hook returned a
|
|
195
|
+
// value. This test wires all three against a single feature definition so
|
|
196
|
+
// any such drift surfaces as a failing unit test, not a broken sample.
|
|
197
|
+
|
|
198
|
+
describe("round-trip: claimKey ↔ authClaims return ↔ readClaim", () => {
|
|
199
|
+
test("value set by the hook is retrievable via the handle from the same feature", async () => {
|
|
200
|
+
// Arrange a feature that declares a claim AND produces a value for it.
|
|
201
|
+
// The hook captures a closure-scoped value so the test can assert
|
|
202
|
+
// the hook's return survives merge + readClaim untouched.
|
|
203
|
+
const driverData = new Map<string, string>();
|
|
204
|
+
driverData.set("user-1", "team-alpha");
|
|
205
|
+
|
|
206
|
+
const feature = defineFeature("drivers", (r) => {
|
|
207
|
+
const teamId = r.claimKey("teamId", { type: "string" });
|
|
208
|
+
r.authClaims(async (user) => {
|
|
209
|
+
const team = driverData.get(user.id);
|
|
210
|
+
return team ? { teamId: team } : {};
|
|
211
|
+
});
|
|
212
|
+
return { Claims: { teamId } as const };
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Run the resolver directly — same code path the Dispatcher walks.
|
|
216
|
+
const { resolveAuthClaims } = await import("../../pipeline/auth-claims-resolver");
|
|
217
|
+
const reg = createRegistry([feature]);
|
|
218
|
+
const user: SessionUser = {
|
|
219
|
+
id: "user-1",
|
|
220
|
+
tenantId: "22222222-0000-4000-8000-000000000001",
|
|
221
|
+
roles: ["User"],
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const claims = await resolveAuthClaims({
|
|
225
|
+
user,
|
|
226
|
+
hooks: reg.getAuthClaimsHooks(),
|
|
227
|
+
contextFactory: () => ({
|
|
228
|
+
db: {} as never,
|
|
229
|
+
queryAs: async () => {
|
|
230
|
+
throw new Error("unused");
|
|
231
|
+
},
|
|
232
|
+
}),
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// readClaim on the SAME handle produced at registration finds the value.
|
|
236
|
+
const userWithClaims: SessionUser = { ...user, claims };
|
|
237
|
+
const teamId = readClaim(userWithClaims, feature.exports.Claims.teamId);
|
|
238
|
+
expect(teamId).toBe("team-alpha");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("round-trip works for non-trivial feature names (camelCase preserved, not kebab'd)", async () => {
|
|
242
|
+
// This was the exact bug the kumiko dev-loop caught in the sample —
|
|
243
|
+
// early impl kebab'd the handle name but the resolver merged with the
|
|
244
|
+
// raw camelCase key, so readClaim missed. Lock the camelCase path in.
|
|
245
|
+
const feature = defineFeature("driverOrders", (r) => {
|
|
246
|
+
const regionId = r.claimKey("regionId", { type: "number" });
|
|
247
|
+
r.authClaims(async () => ({ regionId: 42 }));
|
|
248
|
+
return { Claims: { regionId } as const };
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const { resolveAuthClaims } = await import("../../pipeline/auth-claims-resolver");
|
|
252
|
+
const reg = createRegistry([feature]);
|
|
253
|
+
const user: SessionUser = {
|
|
254
|
+
id: "user-1",
|
|
255
|
+
tenantId: "22222222-0000-4000-8000-000000000001",
|
|
256
|
+
roles: ["User"],
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const claims = await resolveAuthClaims({
|
|
260
|
+
user,
|
|
261
|
+
hooks: reg.getAuthClaimsHooks(),
|
|
262
|
+
contextFactory: () => ({
|
|
263
|
+
db: {} as never,
|
|
264
|
+
queryAs: async () => {
|
|
265
|
+
throw new Error("unused");
|
|
266
|
+
},
|
|
267
|
+
}),
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
expect(claims["driverOrders:regionId"]).toBe(42);
|
|
271
|
+
const userWithClaims: SessionUser = { ...user, claims };
|
|
272
|
+
expect(readClaim(userWithClaims, feature.exports.Claims.regionId)).toBe(42);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { describe, expect, expectTypeOf, test } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
access,
|
|
4
|
+
createSystemConfig,
|
|
5
|
+
createTenantConfig,
|
|
6
|
+
createUserConfig,
|
|
7
|
+
} from "../config-helpers";
|
|
8
|
+
import type { ConfigKeyDefinition } from "../types";
|
|
9
|
+
|
|
10
|
+
describe("access presets", () => {
|
|
11
|
+
test("access.all", () => {
|
|
12
|
+
expect(access.all).toEqual(["all"]);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("access.admin", () => {
|
|
16
|
+
expect(access.admin).toEqual(["Admin", "SystemAdmin"]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("access.systemAdmin", () => {
|
|
20
|
+
expect(access.systemAdmin).toEqual(["SystemAdmin"]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("access.system", () => {
|
|
24
|
+
expect(access.system).toEqual(["system"]);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("access.privileged covers framework auth + SystemAdmin", () => {
|
|
28
|
+
expect(access.privileged).toEqual(["system", "SystemAdmin"]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("access.authenticated covers any signed-in user role (no system)", () => {
|
|
32
|
+
expect(access.authenticated).toEqual(["User", "Admin", "SystemAdmin"]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("access.roles() creates custom role list", () => {
|
|
36
|
+
expect(access.roles("Billing", "Accounting")).toEqual(["Billing", "Accounting"]);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("createTenantConfig", () => {
|
|
41
|
+
test("defaults: admin writes, all reads", () => {
|
|
42
|
+
const key = createTenantConfig("text");
|
|
43
|
+
expect(key.scope).toBe("tenant");
|
|
44
|
+
expect(key.access.write).toEqual(["Admin", "SystemAdmin"]);
|
|
45
|
+
expect(key.access.read).toEqual(["all"]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("custom roles override defaults", () => {
|
|
49
|
+
const key = createTenantConfig("text", {
|
|
50
|
+
write: access.roles("Billing"),
|
|
51
|
+
read: access.roles("Admin", "Billing"),
|
|
52
|
+
});
|
|
53
|
+
expect(key.access.write).toEqual(["Billing"]);
|
|
54
|
+
expect(key.access.read).toEqual(["Admin", "Billing"]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("encrypted flag", () => {
|
|
58
|
+
const key = createTenantConfig("text", { encrypted: true });
|
|
59
|
+
expect(key.encrypted).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("with default value", () => {
|
|
63
|
+
const key = createTenantConfig("number", { default: 42 });
|
|
64
|
+
expect(key.default).toBe(42);
|
|
65
|
+
expect(key.type).toBe("number");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("select with options", () => {
|
|
69
|
+
const key = createTenantConfig("select", { options: ["de", "en", "fr"] });
|
|
70
|
+
expect(key.options).toEqual(["de", "en", "fr"]);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("createSystemConfig", () => {
|
|
75
|
+
test("defaults: system writes, admin reads", () => {
|
|
76
|
+
const key = createSystemConfig("number");
|
|
77
|
+
expect(key.scope).toBe("system");
|
|
78
|
+
expect(key.access.write).toEqual(["system"]);
|
|
79
|
+
expect(key.access.read).toEqual(["Admin", "SystemAdmin"]);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("with default value", () => {
|
|
83
|
+
const key = createSystemConfig("number", { default: 50 });
|
|
84
|
+
expect(key.default).toBe(50);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("createUserConfig", () => {
|
|
89
|
+
test("defaults: all writes, all reads", () => {
|
|
90
|
+
const key = createUserConfig("boolean");
|
|
91
|
+
expect(key.scope).toBe("user");
|
|
92
|
+
expect(key.access.write).toEqual(["all"]);
|
|
93
|
+
expect(key.access.read).toEqual(["all"]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("with default value", () => {
|
|
97
|
+
const key = createUserConfig("boolean", { default: true });
|
|
98
|
+
expect(key.default).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// expectTypeOf + @ts-expect-error are the real assertions — they fail the
|
|
103
|
+
// build if the helper generic widens. The expect() lines exist so the
|
|
104
|
+
// fake-test guard sees runtime asserts and lint stays quiet on unused vars.
|
|
105
|
+
describe("config helpers — type narrowing", () => {
|
|
106
|
+
test("type tag is preserved per helper", () => {
|
|
107
|
+
const numberKey = createTenantConfig("number", { default: 19 });
|
|
108
|
+
const boolKey = createUserConfig("boolean", { default: true });
|
|
109
|
+
const textKey = createSystemConfig("text", { default: "x" });
|
|
110
|
+
expectTypeOf(numberKey).toEqualTypeOf<ConfigKeyDefinition<"number">>();
|
|
111
|
+
expectTypeOf(boolKey).toEqualTypeOf<ConfigKeyDefinition<"boolean">>();
|
|
112
|
+
expectTypeOf(textKey).toEqualTypeOf<ConfigKeyDefinition<"text">>();
|
|
113
|
+
expect(numberKey.type).toBe("number");
|
|
114
|
+
expect(boolKey.type).toBe("boolean");
|
|
115
|
+
expect(textKey.type).toBe("text");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("default value must match the type-tag primitive", () => {
|
|
119
|
+
// @ts-expect-error — number tag, string default
|
|
120
|
+
const wrongNumber = createTenantConfig("number", { default: "nope" });
|
|
121
|
+
// @ts-expect-error — boolean tag, number default
|
|
122
|
+
const wrongBool = createUserConfig("boolean", { default: 1 });
|
|
123
|
+
expect(wrongNumber.type).toBe("number");
|
|
124
|
+
expect(wrongBool.type).toBe("boolean");
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe("config helpers — bounds (number only)", () => {
|
|
129
|
+
test("bounds attach to the definition when provided", () => {
|
|
130
|
+
const key = createTenantConfig("number", {
|
|
131
|
+
default: 10,
|
|
132
|
+
bounds: { min: 1, max: 100 },
|
|
133
|
+
});
|
|
134
|
+
expect(key.bounds).toEqual({ min: 1, max: 100 });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("bounds can be partial (min only)", () => {
|
|
138
|
+
const key = createTenantConfig("number", {
|
|
139
|
+
default: 10,
|
|
140
|
+
bounds: { min: 0 },
|
|
141
|
+
});
|
|
142
|
+
expect(key.bounds).toEqual({ min: 0 });
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("bounds can be partial (max only)", () => {
|
|
146
|
+
const key = createSystemConfig("number", {
|
|
147
|
+
default: 50,
|
|
148
|
+
bounds: { max: 1000 },
|
|
149
|
+
});
|
|
150
|
+
expect(key.bounds).toEqual({ max: 1000 });
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("no bounds → bounds field absent", () => {
|
|
154
|
+
const key = createTenantConfig("number", { default: 10 });
|
|
155
|
+
expect(key.bounds).toBeUndefined();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("@ts-expect-error: bounds on non-number types is rejected", () => {
|
|
159
|
+
// @ts-expect-error — bounds only valid for "number"
|
|
160
|
+
const textKey = createTenantConfig("text", { bounds: { min: 1 } });
|
|
161
|
+
// @ts-expect-error — bounds only valid for "number"
|
|
162
|
+
const boolKey = createUserConfig("boolean", { bounds: { max: 1 } });
|
|
163
|
+
const selectKey = createSystemConfig("select", {
|
|
164
|
+
options: ["a", "b"],
|
|
165
|
+
// @ts-expect-error — bounds only valid for "number"
|
|
166
|
+
bounds: { min: 1 },
|
|
167
|
+
});
|
|
168
|
+
expect(textKey.type).toBe("text");
|
|
169
|
+
expect(boolKey.type).toBe("boolean");
|
|
170
|
+
expect(selectKey.type).toBe("select");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("config helpers — allowPerRequest opt-in", () => {
|
|
175
|
+
test("allowPerRequest=true attaches to the definition", () => {
|
|
176
|
+
const key = createTenantConfig("number", {
|
|
177
|
+
default: 10,
|
|
178
|
+
allowPerRequest: true,
|
|
179
|
+
});
|
|
180
|
+
expect(key.allowPerRequest).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("allowPerRequest=false is omitted (same shape as absent)", () => {
|
|
184
|
+
const key = createTenantConfig("number", {
|
|
185
|
+
default: 10,
|
|
186
|
+
allowPerRequest: false,
|
|
187
|
+
});
|
|
188
|
+
expect(key.allowPerRequest).toBeUndefined();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("no allowPerRequest → field absent (deny-by-default)", () => {
|
|
192
|
+
const key = createTenantConfig("number", { default: 10 });
|
|
193
|
+
expect(key.allowPerRequest).toBeUndefined();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("@ts-expect-error: allowPerRequest on text is rejected at compile time", () => {
|
|
197
|
+
// @ts-expect-error — text keys can't opt in to per-request overrides
|
|
198
|
+
const textKey = createTenantConfig("text", { allowPerRequest: true });
|
|
199
|
+
expect(textKey.type).toBe("text");
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("config helpers — computed (plan-based / derived values)", () => {
|
|
204
|
+
test("computed function attaches to the definition and returns the typed value", async () => {
|
|
205
|
+
const key = createTenantConfig("number", {
|
|
206
|
+
default: 10,
|
|
207
|
+
computed: async () => 200,
|
|
208
|
+
});
|
|
209
|
+
expect(typeof key.computed).toBe("function");
|
|
210
|
+
if (!key.computed) throw new Error("unreachable");
|
|
211
|
+
const value = await key.computed({
|
|
212
|
+
tenantId: "00000000-0000-4000-8000-000000000001" as never,
|
|
213
|
+
userId: "u-1",
|
|
214
|
+
db: {} as never,
|
|
215
|
+
});
|
|
216
|
+
expect(value).toBe(200);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("no computed → field absent on the definition", () => {
|
|
220
|
+
const key = createTenantConfig("number", { default: 10 });
|
|
221
|
+
expect(key.computed).toBeUndefined();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("@ts-expect-error: computed return must match the type-tag", () => {
|
|
225
|
+
const wrongNumber = createTenantConfig("number", {
|
|
226
|
+
// @ts-expect-error — number tag, string return
|
|
227
|
+
computed: async () => "not-a-number",
|
|
228
|
+
});
|
|
229
|
+
const wrongBool = createUserConfig("boolean", {
|
|
230
|
+
// @ts-expect-error — boolean tag, number return
|
|
231
|
+
computed: async () => 1,
|
|
232
|
+
});
|
|
233
|
+
expect(wrongNumber.type).toBe("number");
|
|
234
|
+
expect(wrongBool.type).toBe("boolean");
|
|
235
|
+
});
|
|
236
|
+
});
|