@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,235 @@
|
|
|
1
|
+
import { sql } from "drizzle-orm";
|
|
2
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
3
|
+
import { createBooleanField, createEntity, createTextField } from "../../engine";
|
|
4
|
+
import { createEventsTable } from "../../event-store";
|
|
5
|
+
import { createEntityTable, createTestDb, type TestDb, TestUsers, testTenantId } from "../../stack";
|
|
6
|
+
import { createEventStoreExecutor } from "../event-store-executor";
|
|
7
|
+
import { buildDrizzleTable } from "../table-builder";
|
|
8
|
+
import { createTenantDb, type TenantDb } from "../tenant-db";
|
|
9
|
+
|
|
10
|
+
const entity = createEntity({
|
|
11
|
+
table: "read_es_exec_users",
|
|
12
|
+
fields: {
|
|
13
|
+
email: createTextField({ required: true, searchable: true }),
|
|
14
|
+
firstName: createTextField(),
|
|
15
|
+
isEnabled: createBooleanField({ default: true }),
|
|
16
|
+
},
|
|
17
|
+
softDelete: true,
|
|
18
|
+
});
|
|
19
|
+
const table = buildDrizzleTable("esExecUser", entity);
|
|
20
|
+
|
|
21
|
+
let testDb: TestDb;
|
|
22
|
+
let tdb: TenantDb;
|
|
23
|
+
const adminUser = TestUsers.admin;
|
|
24
|
+
|
|
25
|
+
beforeAll(async () => {
|
|
26
|
+
testDb = await createTestDb();
|
|
27
|
+
await createEntityTable(testDb.db, entity, "esExecUser");
|
|
28
|
+
await createEventsTable(testDb.db);
|
|
29
|
+
tdb = createTenantDb(testDb.db, adminUser.tenantId);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterAll(async () => {
|
|
33
|
+
await testDb.cleanup();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
beforeEach(async () => {
|
|
37
|
+
await testDb.db.execute(sql`TRUNCATE kumiko_events, read_es_exec_users RESTART IDENTITY CASCADE`);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("event-store-executor", () => {
|
|
41
|
+
const crud = createEventStoreExecutor(table, entity, { entityName: "esExecUser" });
|
|
42
|
+
|
|
43
|
+
test("create appends event v1 + inserts projection row", async () => {
|
|
44
|
+
const result = await crud.create({ email: "test@test.de", firstName: "Test" }, adminUser, tdb);
|
|
45
|
+
expect(result.isSuccess).toBe(true);
|
|
46
|
+
if (!result.isSuccess) return;
|
|
47
|
+
expect(result.data.isNew).toBe(true);
|
|
48
|
+
expect(typeof result.data.id).toBe("string");
|
|
49
|
+
expect(result.data.data["email"]).toBe("test@test.de");
|
|
50
|
+
expect(result.data.data["tenantId"]).toBe(testTenantId(1));
|
|
51
|
+
expect(result.data.data["version"]).toBe(1);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("update increments version + appends event", async () => {
|
|
55
|
+
const created = await crud.create({ email: "u@test.de" }, adminUser, tdb);
|
|
56
|
+
if (!created.isSuccess) throw new Error("setup failed");
|
|
57
|
+
|
|
58
|
+
const result = await crud.update(
|
|
59
|
+
{ id: created.data.id, version: 1, changes: { firstName: "Updated" } },
|
|
60
|
+
adminUser,
|
|
61
|
+
tdb,
|
|
62
|
+
);
|
|
63
|
+
expect(result.isSuccess).toBe(true);
|
|
64
|
+
if (!result.isSuccess) return;
|
|
65
|
+
expect(result.data.data["version"]).toBe(2);
|
|
66
|
+
expect(result.data.data["firstName"]).toBe("Updated");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("stale version → version_conflict", async () => {
|
|
70
|
+
const created = await crud.create({ email: "v@test.de" }, adminUser, tdb);
|
|
71
|
+
if (!created.isSuccess) throw new Error("setup failed");
|
|
72
|
+
|
|
73
|
+
await crud.update(
|
|
74
|
+
{ id: created.data.id, version: 1, changes: { firstName: "First" } },
|
|
75
|
+
adminUser,
|
|
76
|
+
tdb,
|
|
77
|
+
);
|
|
78
|
+
const stale = await crud.update(
|
|
79
|
+
{ id: created.data.id, version: 1, changes: { firstName: "Stale" } },
|
|
80
|
+
adminUser,
|
|
81
|
+
tdb,
|
|
82
|
+
);
|
|
83
|
+
expect(stale.isSuccess).toBe(false);
|
|
84
|
+
if (stale.isSuccess) return;
|
|
85
|
+
expect(stale.error.code).toBe("version_conflict");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("delete soft-deletes + appends event", async () => {
|
|
89
|
+
const created = await crud.create({ email: "d@test.de" }, adminUser, tdb);
|
|
90
|
+
if (!created.isSuccess) throw new Error("setup failed");
|
|
91
|
+
|
|
92
|
+
const deleted = await crud.delete({ id: created.data.id }, adminUser, tdb);
|
|
93
|
+
expect(deleted.isSuccess).toBe(true);
|
|
94
|
+
|
|
95
|
+
const detail = await crud.detail({ id: created.data.id }, adminUser, tdb);
|
|
96
|
+
expect(detail).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Sensitive-field stripping: passwords/tokens/IBANs stay in the entity row
|
|
101
|
+
// but MUST NOT land in the immutable event log (GDPR right-to-be-forgotten,
|
|
102
|
+
// secrets-rotation, audit discoverability). Fields marked `sensitive: true`
|
|
103
|
+
// are excluded from every event payload: create data, update changes,
|
|
104
|
+
// update previous, delete previous, restore previous.
|
|
105
|
+
const sensitiveEntity = createEntity({
|
|
106
|
+
table: "read_es_exec_sensitive",
|
|
107
|
+
fields: {
|
|
108
|
+
email: createTextField({ required: true }),
|
|
109
|
+
passwordHash: createTextField({ sensitive: true }),
|
|
110
|
+
apiToken: createTextField({ sensitive: true }),
|
|
111
|
+
},
|
|
112
|
+
softDelete: true,
|
|
113
|
+
});
|
|
114
|
+
const sensitiveTable = buildDrizzleTable("esExecSensitive", sensitiveEntity);
|
|
115
|
+
|
|
116
|
+
describe("event-store-executor — sensitive fields", () => {
|
|
117
|
+
const crud = createEventStoreExecutor(sensitiveTable, sensitiveEntity, {
|
|
118
|
+
entityName: "esExecSensitive",
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
beforeAll(async () => {
|
|
122
|
+
await createEntityTable(testDb.db, sensitiveEntity, "esExecSensitive");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
beforeEach(async () => {
|
|
126
|
+
await testDb.db.execute(
|
|
127
|
+
sql`TRUNCATE kumiko_events, read_es_exec_sensitive RESTART IDENTITY CASCADE`,
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
async function lastEvent<TPayload = Record<string, unknown>>(): Promise<{
|
|
132
|
+
type: string;
|
|
133
|
+
payload: TPayload;
|
|
134
|
+
}> {
|
|
135
|
+
const rows = await testDb.db.execute<{ type: string; payload: TPayload }>(
|
|
136
|
+
sql`SELECT type, payload FROM kumiko_events ORDER BY id DESC LIMIT 1`,
|
|
137
|
+
);
|
|
138
|
+
const row = rows[0];
|
|
139
|
+
if (!row) throw new Error("no events in store");
|
|
140
|
+
return row;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
test("create event payload excludes sensitive fields but entity row keeps them", async () => {
|
|
144
|
+
const result = await crud.create(
|
|
145
|
+
{ email: "s@test.de", passwordHash: "pw-hash-123", apiToken: "tok-abc" },
|
|
146
|
+
adminUser,
|
|
147
|
+
tdb,
|
|
148
|
+
);
|
|
149
|
+
if (!result.isSuccess) throw new Error("create failed");
|
|
150
|
+
// Entity row: full data preserved.
|
|
151
|
+
expect(result.data.data["passwordHash"]).toBe("pw-hash-123");
|
|
152
|
+
expect(result.data.data["apiToken"]).toBe("tok-abc");
|
|
153
|
+
|
|
154
|
+
// Event payload: sensitive stripped, public retained.
|
|
155
|
+
const event = await lastEvent();
|
|
156
|
+
expect(event.type).toBe("esExecSensitive.created");
|
|
157
|
+
expect(event.payload["email"]).toBe("s@test.de");
|
|
158
|
+
expect(event.payload["passwordHash"]).toBeUndefined();
|
|
159
|
+
expect(event.payload["apiToken"]).toBeUndefined();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("update event strips sensitive from BOTH changes and previous", async () => {
|
|
163
|
+
const created = await crud.create(
|
|
164
|
+
{ email: "u@test.de", passwordHash: "old-hash", apiToken: "old-tok" },
|
|
165
|
+
adminUser,
|
|
166
|
+
tdb,
|
|
167
|
+
);
|
|
168
|
+
if (!created.isSuccess) throw new Error("create failed");
|
|
169
|
+
|
|
170
|
+
const result = await crud.update(
|
|
171
|
+
{
|
|
172
|
+
id: created.data.id,
|
|
173
|
+
version: 1,
|
|
174
|
+
changes: { passwordHash: "new-hash", email: "u2@test.de" },
|
|
175
|
+
},
|
|
176
|
+
adminUser,
|
|
177
|
+
tdb,
|
|
178
|
+
);
|
|
179
|
+
if (!result.isSuccess) throw new Error("update failed");
|
|
180
|
+
|
|
181
|
+
const event = await lastEvent<{
|
|
182
|
+
changes: { email?: string; passwordHash?: string };
|
|
183
|
+
previous: { email?: string; passwordHash?: string; apiToken?: string };
|
|
184
|
+
}>();
|
|
185
|
+
expect(event.type).toBe("esExecSensitive.updated");
|
|
186
|
+
// Changes: email retained (public), passwordHash stripped.
|
|
187
|
+
expect(event.payload.changes.email).toBe("u2@test.de");
|
|
188
|
+
expect(event.payload.changes.passwordHash).toBeUndefined();
|
|
189
|
+
// Previous: email retained, passwordHash + apiToken stripped.
|
|
190
|
+
expect(event.payload.previous.email).toBe("u@test.de");
|
|
191
|
+
expect(event.payload.previous.passwordHash).toBeUndefined();
|
|
192
|
+
expect(event.payload.previous.apiToken).toBeUndefined();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("delete event strips sensitive from previous", async () => {
|
|
196
|
+
const created = await crud.create(
|
|
197
|
+
{ email: "d@test.de", passwordHash: "pw", apiToken: "tk" },
|
|
198
|
+
adminUser,
|
|
199
|
+
tdb,
|
|
200
|
+
);
|
|
201
|
+
if (!created.isSuccess) throw new Error("create failed");
|
|
202
|
+
|
|
203
|
+
await crud.delete({ id: created.data.id }, adminUser, tdb);
|
|
204
|
+
|
|
205
|
+
type SensitivePrevious = {
|
|
206
|
+
previous: { email?: string; passwordHash?: string; apiToken?: string };
|
|
207
|
+
};
|
|
208
|
+
const event = await lastEvent<SensitivePrevious>();
|
|
209
|
+
expect(event.type).toBe("esExecSensitive.deleted");
|
|
210
|
+
expect(event.payload.previous.email).toBe("d@test.de");
|
|
211
|
+
expect(event.payload.previous.passwordHash).toBeUndefined();
|
|
212
|
+
expect(event.payload.previous.apiToken).toBeUndefined();
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("restore event strips sensitive from previous", async () => {
|
|
216
|
+
const created = await crud.create(
|
|
217
|
+
{ email: "r@test.de", passwordHash: "pw", apiToken: "tk" },
|
|
218
|
+
adminUser,
|
|
219
|
+
tdb,
|
|
220
|
+
);
|
|
221
|
+
if (!created.isSuccess) throw new Error("create failed");
|
|
222
|
+
await crud.delete({ id: created.data.id }, adminUser, tdb);
|
|
223
|
+
|
|
224
|
+
await crud.restore({ id: created.data.id }, adminUser, tdb);
|
|
225
|
+
|
|
226
|
+
type SensitivePrevious = {
|
|
227
|
+
previous: { email?: string; passwordHash?: string; apiToken?: string };
|
|
228
|
+
};
|
|
229
|
+
const event = await lastEvent<SensitivePrevious>();
|
|
230
|
+
expect(event.type).toBe("esExecSensitive.restored");
|
|
231
|
+
expect(event.payload.previous.email).toBe("r@test.de");
|
|
232
|
+
expect(event.payload.previous.passwordHash).toBeUndefined();
|
|
233
|
+
expect(event.payload.previous.apiToken).toBeUndefined();
|
|
234
|
+
});
|
|
235
|
+
});
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
// Live==Rebuild-Equivalence für die ImplicitProjection (Sprint G).
|
|
2
|
+
//
|
|
3
|
+
// Beweist: für jede r.entity erzeugt der EventStoreExecutor (live) und
|
|
4
|
+
// rebuildProjection (replay über Implicit-Projection) **denselben**
|
|
5
|
+
// Tabellen-Stand. Ohne diesen Test können live + rebuild zwischen den
|
|
6
|
+
// Releases auseinanderdriften (z.B. wenn jemand die Live-Schreib-Logik
|
|
7
|
+
// im Executor ändert ohne applyEntityEvent anzupassen).
|
|
8
|
+
//
|
|
9
|
+
// Test-Strategie:
|
|
10
|
+
// 1. Live: 4 Aggregate mit verschiedenen Lifecycles (create / update /
|
|
11
|
+
// soft-delete / restore) durch den EventStoreExecutor jagen
|
|
12
|
+
// 2. Snapshot der Entity-Tabelle (nach Sortierung — ORDER BY id)
|
|
13
|
+
// 3. TRUNCATE der Entity-Tabelle
|
|
14
|
+
// 4. rebuildProjection für die ImplicitProjection
|
|
15
|
+
// 5. Snapshot erneut nehmen
|
|
16
|
+
// 6. deep-equal: identische Rows in identischer Reihenfolge
|
|
17
|
+
|
|
18
|
+
import { asc, sql } from "drizzle-orm";
|
|
19
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "vitest";
|
|
20
|
+
import { createBooleanField, createEntity, createTextField, defineFeature } from "../../engine";
|
|
21
|
+
import { createRegistry } from "../../engine/registry";
|
|
22
|
+
import { createEventsTable } from "../../event-store";
|
|
23
|
+
import { rebuildProjection } from "../../pipeline";
|
|
24
|
+
import { createProjectionStateTable } from "../../pipeline/projection-state";
|
|
25
|
+
import { createEntityTable, createTestDb, type TestDb, TestUsers } from "../../stack";
|
|
26
|
+
import { createEventStoreExecutor } from "../event-store-executor";
|
|
27
|
+
import { buildDrizzleTable } from "../table-builder";
|
|
28
|
+
import { createTenantDb, type TenantDb } from "../tenant-db";
|
|
29
|
+
|
|
30
|
+
const userEntity = createEntity({
|
|
31
|
+
table: "read_implicit_users",
|
|
32
|
+
fields: {
|
|
33
|
+
email: createTextField({ required: true }),
|
|
34
|
+
firstName: createTextField(),
|
|
35
|
+
isEnabled: createBooleanField({ default: true }),
|
|
36
|
+
},
|
|
37
|
+
softDelete: true,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const userFeature = defineFeature("implicittest", (r) => {
|
|
41
|
+
r.entity("user", userEntity);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const userTable = buildDrizzleTable("user", userEntity);
|
|
45
|
+
|
|
46
|
+
let testDb: TestDb;
|
|
47
|
+
let tdb: TenantDb;
|
|
48
|
+
const adminUser = TestUsers.admin;
|
|
49
|
+
|
|
50
|
+
beforeAll(async () => {
|
|
51
|
+
testDb = await createTestDb();
|
|
52
|
+
await createEntityTable(testDb.db, userEntity, "user");
|
|
53
|
+
await createEventsTable(testDb.db);
|
|
54
|
+
await createProjectionStateTable(testDb.db);
|
|
55
|
+
tdb = createTenantDb(testDb.db, adminUser.tenantId);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
afterAll(async () => {
|
|
59
|
+
await testDb.cleanup();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
beforeEach(async () => {
|
|
63
|
+
await testDb.db.execute(
|
|
64
|
+
sql`TRUNCATE kumiko_events, read_implicit_users, kumiko_projections RESTART IDENTITY CASCADE`,
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
async function snapshotTable(): Promise<readonly Record<string, unknown>[]> {
|
|
69
|
+
const rows = await testDb.db.select().from(userTable).orderBy(asc(userTable["id"]));
|
|
70
|
+
return rows as readonly Record<string, unknown>[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe("implicit-projection / Live==Rebuild equivalence", () => {
|
|
74
|
+
test("4 aggregates × create/update/delete/restore round-trip identical to rebuild", async () => {
|
|
75
|
+
const crud = createEventStoreExecutor(userTable, userEntity, { entityName: "user" });
|
|
76
|
+
|
|
77
|
+
// 1. Live writes — verschiedene Lifecycle-Pfade über die 4 Aggregate.
|
|
78
|
+
// Aggregate A: create + update
|
|
79
|
+
// Aggregate B: create + update + delete (soft)
|
|
80
|
+
// Aggregate C: create + delete + restore
|
|
81
|
+
// Aggregate D: create only
|
|
82
|
+
const a = await crud.create({ email: "a@test.de", firstName: "Alice" }, adminUser, tdb);
|
|
83
|
+
if (!a.isSuccess) throw new Error("setup A failed");
|
|
84
|
+
await crud.update(
|
|
85
|
+
{ id: a.data.id, version: 1, changes: { firstName: "Alice Updated" } },
|
|
86
|
+
adminUser,
|
|
87
|
+
tdb,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const b = await crud.create({ email: "b@test.de", firstName: "Bob" }, adminUser, tdb);
|
|
91
|
+
if (!b.isSuccess) throw new Error("setup B failed");
|
|
92
|
+
await crud.update({ id: b.data.id, version: 1, changes: { isEnabled: false } }, adminUser, tdb);
|
|
93
|
+
await crud.delete({ id: b.data.id }, adminUser, tdb);
|
|
94
|
+
|
|
95
|
+
const c = await crud.create({ email: "c@test.de", firstName: "Carol" }, adminUser, tdb);
|
|
96
|
+
if (!c.isSuccess) throw new Error("setup C failed");
|
|
97
|
+
await crud.delete({ id: c.data.id }, adminUser, tdb);
|
|
98
|
+
await crud.restore({ id: c.data.id }, adminUser, tdb);
|
|
99
|
+
|
|
100
|
+
await crud.create({ email: "d@test.de", firstName: "Dave" }, adminUser, tdb);
|
|
101
|
+
|
|
102
|
+
const liveSnapshot = await snapshotTable();
|
|
103
|
+
|
|
104
|
+
// Konkrete Erwartung an den Live-Stand: 4 erzeugte Aggregate, B ist
|
|
105
|
+
// soft-deleted (isDeleted=true), C ist restored (isDeleted=false),
|
|
106
|
+
// A und D sind unangetastet. Wenn das nicht stimmt, ist der Test
|
|
107
|
+
// setup buggy bevor wir den Rebuild überhaupt vergleichen.
|
|
108
|
+
expect(liveSnapshot).toHaveLength(4);
|
|
109
|
+
const byEmail = new Map(liveSnapshot.map((r) => [r["email"] as string, r]));
|
|
110
|
+
expect(byEmail.get("a@test.de")).toMatchObject({
|
|
111
|
+
firstName: "Alice Updated",
|
|
112
|
+
version: 2,
|
|
113
|
+
isDeleted: false,
|
|
114
|
+
});
|
|
115
|
+
expect(byEmail.get("b@test.de")).toMatchObject({
|
|
116
|
+
isEnabled: false,
|
|
117
|
+
version: 3,
|
|
118
|
+
isDeleted: true,
|
|
119
|
+
});
|
|
120
|
+
expect(byEmail.get("c@test.de")).toMatchObject({
|
|
121
|
+
version: 3,
|
|
122
|
+
isDeleted: false,
|
|
123
|
+
});
|
|
124
|
+
expect(byEmail.get("d@test.de")).toMatchObject({
|
|
125
|
+
firstName: "Dave",
|
|
126
|
+
version: 1,
|
|
127
|
+
isDeleted: false,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// 2. Rebuild from event-log — registry baut die ImplicitProjection,
|
|
131
|
+
// rebuildProjection findet sie über getAllProjections().
|
|
132
|
+
const registry = createRegistry([userFeature]);
|
|
133
|
+
const implicitName = "implicittest:projection:user-entity";
|
|
134
|
+
expect(registry.getAllProjections().has(implicitName)).toBe(true);
|
|
135
|
+
|
|
136
|
+
const result = await rebuildProjection(implicitName, {
|
|
137
|
+
db: testDb.db,
|
|
138
|
+
registry,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// 4 creates + 2 updates + 2 deletes + 1 restore = 9 Events. Wenn
|
|
142
|
+
// die ImplicitProjection silently nichts apply'd hätte, wäre der
|
|
143
|
+
// Count 0 — der Test würde dann den nachfolgenden deep-equal trotzdem
|
|
144
|
+
// verfehlen, aber explizit der Count fängt den Sub-Bug "apply lief,
|
|
145
|
+
// aber für die falsche Event-Anzahl".
|
|
146
|
+
expect(result.eventsProcessed).toBe(9);
|
|
147
|
+
|
|
148
|
+
// 3. Vergleich. Erst ID-für-ID strikt prüfen damit klar ist welche
|
|
149
|
+
// Felder verglichen werden — dann das Array-deep-equal als Catch-all.
|
|
150
|
+
const rebuildSnapshot = await snapshotTable();
|
|
151
|
+
expect(rebuildSnapshot).toHaveLength(liveSnapshot.length);
|
|
152
|
+
for (let i = 0; i < liveSnapshot.length; i++) {
|
|
153
|
+
const live = liveSnapshot[i];
|
|
154
|
+
const rebuilt = rebuildSnapshot[i];
|
|
155
|
+
// Diese Felder sind die User-sichtbare Truth (was sieht die UI?
|
|
156
|
+
// was schreibt der Audit-Log?). Wenn eines davon driftet, ist
|
|
157
|
+
// Live==Rebuild nicht mehr gegeben.
|
|
158
|
+
const fields = [
|
|
159
|
+
"id",
|
|
160
|
+
"tenantId",
|
|
161
|
+
"version",
|
|
162
|
+
"email",
|
|
163
|
+
"firstName",
|
|
164
|
+
"isEnabled",
|
|
165
|
+
"isDeleted",
|
|
166
|
+
"insertedAt",
|
|
167
|
+
"modifiedAt",
|
|
168
|
+
"deletedAt",
|
|
169
|
+
"insertedById",
|
|
170
|
+
"modifiedById",
|
|
171
|
+
"deletedById",
|
|
172
|
+
] as const;
|
|
173
|
+
for (const f of fields) {
|
|
174
|
+
expect(rebuilt?.[f], `field "${f}" at row ${i}`).toEqual(live?.[f]);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// Catch-all: irgendein Feld das wir nicht explizit listen?
|
|
178
|
+
expect(rebuildSnapshot).toEqual(liveSnapshot);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("ImplicitProjection ist im Registry registriert mit korrekten apply-keys", () => {
|
|
182
|
+
const registry = createRegistry([userFeature]);
|
|
183
|
+
const projection = registry.getAllProjections().get("implicittest:projection:user-entity");
|
|
184
|
+
expect(projection).toBeDefined();
|
|
185
|
+
if (!projection) return;
|
|
186
|
+
// 4 Auto-Verben weil softDelete=true → restored kommt dazu
|
|
187
|
+
expect(Object.keys(projection.apply).sort()).toEqual([
|
|
188
|
+
"user.created",
|
|
189
|
+
"user.deleted",
|
|
190
|
+
"user.restored",
|
|
191
|
+
"user.updated",
|
|
192
|
+
]);
|
|
193
|
+
expect(projection.source).toBe("user");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("ohne softDelete → keine restore-apply-key registriert", () => {
|
|
197
|
+
const hardDeleteEntity = createEntity({
|
|
198
|
+
table: "read_implicit_hard",
|
|
199
|
+
fields: { name: createTextField({ required: true }) },
|
|
200
|
+
});
|
|
201
|
+
const hardFeature = defineFeature("implicithard", (r) => {
|
|
202
|
+
r.entity("widget", hardDeleteEntity);
|
|
203
|
+
});
|
|
204
|
+
const registry = createRegistry([hardFeature]);
|
|
205
|
+
const projection = registry.getAllProjections().get("implicithard:projection:widget-entity");
|
|
206
|
+
expect(projection).toBeDefined();
|
|
207
|
+
if (!projection) return;
|
|
208
|
+
expect(Object.keys(projection.apply).sort()).toEqual([
|
|
209
|
+
"widget.created",
|
|
210
|
+
"widget.deleted",
|
|
211
|
+
"widget.updated",
|
|
212
|
+
]);
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Sensitive-Drift ist eine bekannte Welle-3-Lücke: das Event-Log strippt
|
|
217
|
+
// sensitive-Felder VOR dem Append (GDPR-Annahme), die Live-Read-Tabelle
|
|
218
|
+
// bekommt sie über den unstripped flatData, der Rebuild-Pfad nur den
|
|
219
|
+
// stripped event.payload. Bei Schema-Rebuilds gehen sensitive Daten
|
|
220
|
+
// verloren.
|
|
221
|
+
//
|
|
222
|
+
// Dieser Test pinst die Drift explizit: Live row hat das sensitive Feld,
|
|
223
|
+
// Rebuild row hat NULL. Wenn Welle 3 das fixt (z.B. via separater
|
|
224
|
+
// sensitive-Spalte oder verschlüsseltem Event-Payload), bricht der Test
|
|
225
|
+
// und zwingt zu Aufmerksamkeit.
|
|
226
|
+
|
|
227
|
+
import { sql as drizzleSql, eq } from "drizzle-orm";
|
|
228
|
+
|
|
229
|
+
const sensitiveTable = "read_implicit_sensitive_users";
|
|
230
|
+
|
|
231
|
+
const sensitiveEntity = createEntity({
|
|
232
|
+
table: sensitiveTable,
|
|
233
|
+
fields: {
|
|
234
|
+
email: createTextField({ required: true }),
|
|
235
|
+
apiKey: createTextField({ sensitive: true }),
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const sensitiveFeature = defineFeature("implicitsensitive", (r) => {
|
|
240
|
+
r.entity("sensitive-user", sensitiveEntity);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
const sensitiveDrizzleTable = buildDrizzleTable("sensitive-user", sensitiveEntity);
|
|
244
|
+
|
|
245
|
+
describe("implicit-projection / dokumentierte Sensitive-Drift", () => {
|
|
246
|
+
beforeAll(async () => {
|
|
247
|
+
await createEntityTable(testDb.db, sensitiveEntity, "sensitive-user");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
beforeEach(async () => {
|
|
251
|
+
await testDb.db.execute(
|
|
252
|
+
drizzleSql.raw(
|
|
253
|
+
`TRUNCATE ${sensitiveTable}, kumiko_events, kumiko_projections RESTART IDENTITY CASCADE`,
|
|
254
|
+
),
|
|
255
|
+
);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("Live schreibt sensitive-Felder, Rebuild lässt sie NULL (Welle-3-Roadmap)", async () => {
|
|
259
|
+
const crud = createEventStoreExecutor(sensitiveDrizzleTable, sensitiveEntity, {
|
|
260
|
+
entityName: "sensitive-user",
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// 1. Live: create mit apiKey (sensitive). Read-Tabelle bekommt den
|
|
264
|
+
// Wert direkt vom Live-Pfad (unstripped flatData).
|
|
265
|
+
const created = await crud.create(
|
|
266
|
+
{ email: "x@test.de", apiKey: "secret-token-abc" },
|
|
267
|
+
adminUser,
|
|
268
|
+
tdb,
|
|
269
|
+
);
|
|
270
|
+
if (!created.isSuccess) throw new Error("setup failed");
|
|
271
|
+
|
|
272
|
+
const [liveRow] = await testDb.db
|
|
273
|
+
.select()
|
|
274
|
+
.from(sensitiveDrizzleTable)
|
|
275
|
+
.where(eq(sensitiveDrizzleTable["id"], created.data.id as string));
|
|
276
|
+
expect(liveRow?.["apiKey"]).toBe("secret-token-abc");
|
|
277
|
+
expect(liveRow?.["email"]).toBe("x@test.de");
|
|
278
|
+
|
|
279
|
+
// 2. Verifiziere dass das Event-Log das Feld NICHT enthält (stripped).
|
|
280
|
+
const events = await testDb.db.execute<{ payload: Record<string, unknown> }>(
|
|
281
|
+
drizzleSql`SELECT payload FROM kumiko_events WHERE aggregate_id = ${created.data.id}::uuid`,
|
|
282
|
+
);
|
|
283
|
+
expect(events[0]?.payload).toBeDefined();
|
|
284
|
+
expect(events[0]?.payload?.["apiKey"]).toBeUndefined();
|
|
285
|
+
expect(events[0]?.payload?.["email"]).toBe("x@test.de");
|
|
286
|
+
|
|
287
|
+
// 3. Rebuild über die ImplicitProjection. Read-Tabelle wird aus
|
|
288
|
+
// event.payload neu materialisiert — apiKey ist nicht im Log,
|
|
289
|
+
// landet also als NULL/undefined in der rebuilt Row.
|
|
290
|
+
const registry = createRegistry([sensitiveFeature]);
|
|
291
|
+
await rebuildProjection("implicitsensitive:projection:sensitive-user-entity", {
|
|
292
|
+
db: testDb.db,
|
|
293
|
+
registry,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const [rebuiltRow] = await testDb.db
|
|
297
|
+
.select()
|
|
298
|
+
.from(sensitiveDrizzleTable)
|
|
299
|
+
.where(eq(sensitiveDrizzleTable["id"], created.data.id as string));
|
|
300
|
+
expect(rebuiltRow?.["email"]).toBe("x@test.de");
|
|
301
|
+
// DAS ist die Drift: sensitive Feld ist nach Rebuild weg.
|
|
302
|
+
expect(rebuiltRow?.["apiKey"]).toBeNull();
|
|
303
|
+
});
|
|
304
|
+
});
|