@cosmicdrift/kumiko-framework 1.0.0 → 2.0.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 +9 -38
- package/package.json +21 -2
- package/src/__tests__/anonymous-access.integration.test.ts +185 -0
- package/src/__tests__/consumer-cli.integration.test.ts +172 -0
- package/src/__tests__/entity-permalink-open.integration.test.ts +94 -0
- package/src/__tests__/full-stack.integration.test.ts +1 -1
- package/src/__tests__/schema-cli-temporal-polyfill.test.ts +48 -0
- package/src/__tests__/schema-cli.integration.test.ts +50 -0
- package/src/__tests__/store-table.integration.test.ts +93 -0
- package/src/api/__tests__/api.test.ts +373 -0
- package/src/api/__tests__/auth-middleware-anonymous-access-boot.test.ts +40 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +17 -1
- package/src/api/__tests__/auth-routes-invalid-body-invite.test.ts +253 -0
- package/src/api/__tests__/auth-routes-mfa-preauth-confirm.test.ts +222 -0
- package/src/api/__tests__/auth-routes-mfa-preauth-enable-start.test.ts +249 -0
- package/src/api/__tests__/auth-routes-mfa-verify.test.ts +202 -0
- package/src/api/__tests__/auth-routes-trusted-proxy.test.ts +135 -0
- package/src/api/__tests__/batch.integration.test.ts +101 -11
- package/src/api/__tests__/csrf-constants-sync.test.ts +20 -0
- package/src/api/__tests__/dispatcher-live.integration.test.ts +74 -0
- package/src/api/__tests__/jwt.test.ts +200 -1
- package/src/api/__tests__/login-rate-limiter-sweep.test.ts +50 -0
- package/src/api/__tests__/pat-scope.test.ts +36 -0
- package/src/api/__tests__/pii-leak-guard.integration.test.ts +103 -0
- package/src/api/__tests__/redis-login-rate-limiter.integration.test.ts +138 -0
- package/src/api/__tests__/request-id-middleware.test.ts +51 -0
- package/src/api/__tests__/server-boot-guards.test.ts +71 -0
- package/src/api/__tests__/server-jwt-ttl.test.ts +58 -0
- package/src/api/__tests__/sse-broker.test.ts +57 -0
- package/src/api/__tests__/sse-route.test.ts +4 -0
- package/src/api/api-constants.ts +11 -0
- package/src/api/auth-middleware.ts +248 -40
- package/src/api/auth-routes.ts +576 -95
- package/src/api/index.ts +13 -4
- package/src/api/jwt.ts +170 -11
- package/src/api/pat-scope.ts +14 -0
- package/src/api/pii-leak-guard.ts +47 -0
- package/src/api/request-context.ts +3 -0
- package/src/api/request-id-middleware.ts +2 -0
- package/src/api/routes.ts +169 -2
- package/src/api/server.ts +112 -16
- package/src/api/sse-broker.ts +39 -0
- package/src/bun-db/__tests__/PATTERN.md +0 -1
- package/src/bun-db/__tests__/coerce-row-temporal.test.ts +41 -0
- package/src/bun-db/__tests__/select-many-retry.test.ts +79 -0
- package/src/bun-db/__tests__/write-brand.test.ts +21 -0
- package/src/bun-db/connection.ts +3 -3
- package/src/bun-db/index.ts +2 -0
- package/src/bun-db/query.ts +105 -32
- package/src/consumer-cli.ts +134 -0
- package/src/crypto/__tests__/blind-index.test.ts +130 -0
- package/src/crypto/__tests__/event-pii.test.ts +161 -0
- package/src/crypto/__tests__/kek-rotation.integration.test.ts +180 -0
- package/src/crypto/__tests__/kms-adapter-contract.ts +134 -0
- package/src/crypto/__tests__/kms-adapter.contract.test.ts +4 -0
- package/src/crypto/__tests__/pg-kms-adapter.integration.test.ts +117 -0
- package/src/crypto/__tests__/pii-field-encryption.test.ts +376 -0
- package/src/crypto/__tests__/request-kms-cache.test.ts +74 -0
- package/src/crypto/__tests__/subject-resolver.test.ts +108 -0
- package/src/crypto/blind-index.ts +118 -0
- package/src/crypto/event-pii.ts +70 -0
- package/src/crypto/in-memory-kms-adapter.ts +50 -0
- package/src/crypto/index.ts +67 -0
- package/src/crypto/kms-adapter.ts +2 -0
- package/src/crypto/pg-kms-adapter.ts +295 -0
- package/src/crypto/pii-field-encryption.ts +248 -0
- package/src/crypto/request-kms-cache.ts +38 -0
- package/src/crypto/subject-resolver.ts +91 -0
- package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +191 -0
- package/src/db/__tests__/blind-index.integration.test.ts +248 -0
- package/src/db/__tests__/build-filter-where.test.ts +34 -0
- package/src/db/__tests__/collect-table-metas.test.ts +10 -10
- package/src/db/__tests__/config-seed.integration.test.ts +13 -5
- package/src/db/__tests__/dialect-instant.test.ts +1 -4
- package/src/db/__tests__/entity-field-encryption.test.ts +7 -7
- package/src/db/__tests__/entity-table-meta-source.test.ts +50 -0
- package/src/db/__tests__/event-store-executor-context.pii-roundtrip.test.ts +67 -0
- package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +402 -0
- package/src/db/__tests__/event-store-executor.integration.test.ts +299 -29
- package/src/db/__tests__/feature-table-sources.test.ts +34 -0
- package/src/db/__tests__/implicit-projection-equivalence.integration.test.ts +118 -39
- package/src/db/__tests__/instant-to-driver-temporal.test.ts +21 -0
- package/src/db/__tests__/located-timestamp.test.ts +19 -0
- package/src/db/__tests__/migrate-generator.test.ts +21 -0
- package/src/db/__tests__/migrate-runner.test.ts +61 -0
- package/src/db/__tests__/number-field-fractional.integration.test.ts +58 -0
- package/src/db/__tests__/rebuild-marker.test.ts +13 -3
- package/src/db/__tests__/replay-migration-sql.test.ts +384 -0
- package/src/db/__tests__/schema-inspection.test.ts +7 -0
- package/src/db/__tests__/schema-migration.integration.test.ts +1 -1
- package/src/db/__tests__/table-builder-meta-lockstep.test.ts +39 -0
- package/src/db/__tests__/tenant-db-where-merge.test.ts +49 -3
- package/src/db/__tests__/tenant-db.integration.test.ts +6 -2
- package/src/db/api.ts +2 -2
- package/src/db/apply-entity-event.ts +18 -14
- package/src/db/blind-index-cleanup.ts +55 -0
- package/src/db/bun-provider.ts +2 -2
- package/src/db/collect-table-metas.ts +6 -7
- package/src/db/config-seed.ts +9 -9
- package/src/db/connection.ts +7 -12
- package/src/db/cursor.ts +1 -18
- package/src/db/dialect.ts +20 -26
- package/src/db/entity-field-encryption.ts +81 -24
- package/src/db/entity-table-meta-types.ts +2 -0
- package/src/db/entity-table-meta.ts +63 -90
- package/src/db/event-store-executor-context.ts +404 -0
- package/src/db/event-store-executor-read.ts +275 -0
- package/src/db/event-store-executor-write.ts +644 -0
- package/src/db/event-store-executor.ts +28 -1155
- package/src/db/feature-table-sources.ts +6 -5
- package/src/db/index.ts +20 -3
- package/src/db/located-timestamp.ts +4 -0
- package/src/db/migrate-generator.ts +39 -6
- package/src/db/migrate-runner.ts +107 -11
- package/src/db/pg-error.ts +9 -1
- package/src/db/postgres-provider.ts +2 -2
- package/src/db/queries/__tests__/event-store-idempotency-index.integration.test.ts +80 -0
- package/src/db/queries/backfill-pii.ts +277 -0
- package/src/db/queries/ddl.ts +45 -0
- package/src/db/queries/event-consumer.ts +35 -2
- package/src/db/queries/event-store.ts +126 -19
- package/src/db/queries/projection-rebuild.ts +22 -8
- package/src/db/queries/shadow-swap.ts +183 -0
- package/src/db/queries/test-stack.ts +4 -30
- package/src/db/query-api.ts +1 -0
- package/src/db/query.ts +1 -0
- package/src/db/rebuild-marker.ts +18 -2
- package/src/db/reference-data.ts +2 -3
- package/src/db/replay-migration-sql.ts +257 -0
- package/src/db/schema-inspection.ts +1 -1
- package/src/db/table-builder.ts +102 -71
- package/src/db/tenant-db.ts +15 -53
- package/src/engine/__tests__/boot-validator-action-wiring.test.ts +242 -0
- package/src/engine/__tests__/boot-validator-boot-check.test.ts +99 -0
- package/src/engine/__tests__/boot-validator-dashboard.test.ts +237 -0
- package/src/engine/__tests__/boot-validator-entity-list.test.ts +104 -0
- package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +7 -69
- package/src/engine/__tests__/boot-validator-i18n-keys.test.ts +26 -5
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +3 -19
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +246 -4
- package/src/engine/__tests__/boot-validator.test.ts +219 -15
- package/src/engine/__tests__/build-app-schema.test.ts +82 -0
- package/src/engine/__tests__/build-config-feature-schema.test.ts +125 -0
- package/src/engine/__tests__/build-target.test.ts +3 -11
- package/src/engine/__tests__/codemod-pipeline.test.ts +152 -21
- package/src/engine/__tests__/config-helpers.test.ts +16 -0
- package/src/engine/__tests__/define-feature-entity-mapping.test.ts +6 -0
- package/src/engine/__tests__/define-roles.test.ts +21 -0
- package/src/engine/__tests__/engine.test.ts +163 -1
- package/src/engine/__tests__/entity-handlers.test.ts +80 -0
- package/src/engine/__tests__/event-migration-declarative.test.ts +65 -0
- package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
- package/src/engine/__tests__/extend-entity-projection.test.ts +123 -0
- package/src/engine/__tests__/factories-time.test.ts +2 -66
- package/src/engine/__tests__/feature-crud-shorthand.test.ts +28 -0
- package/src/engine/__tests__/feature-manifest.test.ts +31 -1
- package/src/engine/__tests__/field-access.test.ts +23 -1
- package/src/engine/__tests__/hook-phases.test.ts +5 -5
- package/src/engine/__tests__/membership-roles.test.ts +4 -10
- package/src/engine/__tests__/nav.test.ts +86 -1
- package/src/engine/__tests__/pipeline-engine.test.ts +9 -9
- package/src/engine/__tests__/pipeline-handler.integration.test.ts +18 -18
- package/src/engine/__tests__/pipeline-observability.integration.test.ts +2 -2
- package/src/engine/__tests__/pipeline-performance.integration.test.ts +3 -3
- package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +9 -9
- package/src/engine/__tests__/post-query-hook.test.ts +7 -7
- package/src/engine/__tests__/registrar-object-form.test.ts +141 -0
- package/src/engine/__tests__/registry.test.ts +144 -0
- package/src/engine/__tests__/schema-builder.test.ts +18 -0
- package/src/engine/__tests__/screen.test.ts +147 -1
- package/src/engine/__tests__/soft-delete-cleanup.test.ts +3 -0
- package/src/engine/__tests__/store-table.test.ts +229 -0
- package/src/engine/__tests__/tier-resolver-extension.test.ts +19 -1
- package/src/engine/__tests__/{visual-tree-patterns.test.ts → tree-actions-patterns.test.ts} +7 -95
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +15 -15
- package/src/engine/boot-validator/__tests__/config-deps.test.ts +92 -0
- package/src/engine/boot-validator/action-wiring.ts +149 -0
- package/src/engine/boot-validator/boot-check.ts +21 -0
- package/src/engine/boot-validator/config-deps.ts +40 -4
- package/src/engine/boot-validator/entity-handler.ts +20 -21
- package/src/engine/boot-validator/entity-list-screens.ts +88 -0
- package/src/engine/boot-validator/gdpr-storage.ts +0 -20
- package/src/engine/boot-validator/i18n-keys.ts +58 -4
- package/src/engine/boot-validator/index.ts +33 -12
- package/src/engine/boot-validator/nav.ts +125 -0
- package/src/engine/boot-validator/pii-retention.ts +95 -10
- package/src/engine/boot-validator/{screens-nav.ts → screens.ts} +214 -191
- package/src/engine/boot-validator/workspaces.ts +68 -0
- package/src/engine/build-app-schema.ts +16 -0
- package/src/engine/build-config-feature-schema.ts +44 -7
- package/src/engine/codemod/pipeline-codemod.ts +5 -5
- package/src/engine/config-helpers.ts +15 -0
- package/src/engine/constants.ts +32 -6
- package/src/engine/create-app.ts +11 -0
- package/src/engine/define-feature.ts +95 -947
- package/src/engine/define-handler.ts +28 -94
- package/src/engine/define-workflow.ts +1 -1
- package/src/engine/effective-features.ts +12 -2
- package/src/engine/entity-handlers.ts +76 -11
- package/src/engine/extensions/tenant-data.ts +19 -0
- package/src/engine/extensions/user-data.ts +29 -2
- package/src/engine/factories.ts +8 -49
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +26 -29
- package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/feature.ts +9 -0
- package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/screens.ts +4 -0
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -2
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +18 -8
- package/src/engine/feature-ast/__tests__/parse.test.ts +1640 -160
- package/src/engine/feature-ast/__tests__/patch.test.ts +206 -12
- package/src/engine/feature-ast/__tests__/patcher.test.ts +20 -23
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +380 -5
- package/src/engine/feature-ast/extractors/events.ts +322 -0
- package/src/engine/feature-ast/extractors/handlers.ts +234 -0
- package/src/engine/feature-ast/extractors/hooks.ts +243 -0
- package/src/engine/feature-ast/extractors/index.ts +34 -31
- package/src/engine/feature-ast/extractors/jobs-routes.ts +221 -0
- package/src/engine/feature-ast/extractors/projections-screens.ts +269 -0
- package/src/engine/feature-ast/extractors/round1.ts +3 -3
- package/src/engine/feature-ast/extractors/round5.ts +3 -3
- package/src/engine/feature-ast/extractors/round6.ts +2 -36
- package/src/engine/feature-ast/extractors/shared.ts +64 -6
- package/src/engine/feature-ast/index.ts +2 -4
- package/src/engine/feature-ast/parse.ts +130 -23
- package/src/engine/feature-ast/patch.ts +39 -50
- package/src/engine/feature-ast/patcher.ts +37 -38
- package/src/engine/feature-ast/patterns.ts +50 -62
- package/src/engine/feature-ast/render.ts +67 -44
- package/src/engine/feature-builder-state.ts +168 -0
- package/src/engine/feature-config-events-jobs.ts +403 -0
- package/src/engine/feature-entity-handlers.ts +208 -0
- package/src/engine/feature-manifest.ts +2 -1
- package/src/engine/feature-ui-extensions.ts +500 -0
- package/src/engine/field-access.ts +13 -2
- package/src/engine/field-helpers.ts +31 -0
- package/src/engine/handler-helpers.ts +26 -0
- package/src/engine/hook-helpers.ts +16 -0
- package/src/engine/index.ts +23 -7
- package/src/engine/membership-roles.ts +13 -0
- package/src/engine/object-form.ts +27 -0
- package/src/engine/ownership.ts +26 -79
- package/src/engine/pattern-library/__tests__/library.test.ts +7 -20
- package/src/engine/pattern-library/library.ts +44 -1152
- package/src/engine/pattern-library/mixed-schemas.ts +450 -0
- package/src/engine/pattern-library/opaque-schemas.ts +124 -0
- package/src/engine/pattern-library/shared-fields.ts +75 -0
- package/src/engine/pattern-library/static-schemas.ts +456 -0
- package/src/engine/pipeline.ts +6 -11
- package/src/engine/registry-facade.ts +362 -0
- package/src/engine/registry-ingest.ts +478 -0
- package/src/engine/registry-state.ts +388 -0
- package/src/engine/registry-validate.ts +642 -0
- package/src/engine/registry.ts +78 -1658
- package/src/engine/run-pipeline.ts +1 -1
- package/src/engine/schema-builder.ts +1 -0
- package/src/engine/screen-helpers.ts +54 -0
- package/src/engine/soft-delete-cleanup.ts +6 -2
- package/src/engine/steps/__tests__/duration-utils.test.ts +20 -0
- package/src/engine/steps/_duration-utils.ts +2 -0
- package/src/engine/steps/unsafe-projection-upsert.ts +1 -4
- package/src/engine/tier-resolver-extension.ts +3 -2
- package/src/engine/types/config.ts +2 -482
- package/src/engine/types/define-handler.ts +2 -0
- package/src/engine/types/entity-handlers.ts +2 -0
- package/src/engine/types/event-type-map.ts +1 -37
- package/src/engine/types/feature.ts +2 -972
- package/src/engine/types/fields.ts +2 -675
- package/src/engine/types/handlers.ts +2 -774
- package/src/engine/types/hooks.ts +2 -184
- package/src/engine/types/http-route.ts +1 -54
- package/src/engine/types/identifiers.ts +1 -47
- package/src/engine/types/index.ts +86 -40
- package/src/engine/types/nav.ts +2 -63
- package/src/engine/types/ownership.ts +2 -0
- package/src/engine/types/projection.ts +2 -138
- package/src/engine/types/relations.ts +1 -51
- package/src/engine/types/screen.ts +2 -574
- package/src/engine/types/step.ts +2 -334
- package/src/engine/types/target-ref.ts +1 -21
- package/src/engine/types/tree-node.ts +1 -132
- package/src/engine/types/workspace.ts +2 -49
- package/src/engine/validate-projection-allowlist.ts +6 -6
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.test.ts +120 -1
- package/src/entrypoint/index.ts +49 -6
- package/src/errors/classes.ts +22 -1
- package/src/errors/field-issue.ts +0 -3
- package/src/errors/index.ts +1 -1
- package/src/errors/write-error-info.ts +10 -22
- package/src/es-ops/README.md +1 -1
- package/src/es-ops/__tests__/runner.integration.test.ts +74 -0
- package/src/es-ops/context.ts +4 -2
- package/src/es-ops/types.ts +8 -1
- package/src/event-store/__tests__/admin-api.integration.test.ts +27 -1
- package/src/event-store/__tests__/backfill-pii.integration.test.ts +279 -0
- package/src/event-store/__tests__/event-store.integration.test.ts +201 -0
- package/src/event-store/__tests__/row-to-stored-event.test.ts +2 -2
- package/src/event-store/__tests__/snapshot.integration.test.ts +86 -0
- package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +58 -0
- package/src/event-store/__tests__/upcaster.integration.test.ts +77 -45
- package/src/event-store/admin-api.ts +11 -4
- package/src/event-store/errors.ts +2 -35
- package/src/event-store/event-store.ts +64 -78
- package/src/event-store/events-schema.ts +11 -13
- package/src/event-store/index.ts +18 -3
- package/src/event-store/rebuild-dead-letter.ts +111 -0
- package/src/event-store/snapshot.ts +63 -40
- package/src/event-store/types.ts +2 -0
- package/src/files/__tests__/build-storage-key.test.ts +28 -0
- package/src/files/__tests__/file-ref-entity.test.ts +8 -0
- package/src/files/__tests__/files.integration.test.ts +24 -0
- package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
- package/src/files/__tests__/local-provider.test.ts +31 -0
- package/src/files/__tests__/provider-resolver.test.ts +70 -0
- package/src/files/__tests__/write-stream.test.ts +13 -0
- package/src/files/file-handle.ts +2 -19
- package/src/files/file-ref-entity.ts +2 -2
- package/src/files/file-routes.ts +13 -0
- package/src/files/index.ts +1 -1
- package/src/files/local-provider.ts +27 -8
- package/src/files/provider-resolver.ts +31 -20
- package/src/files/types.ts +13 -55
- package/src/i18n/__tests__/required-surface-keys.test.ts +17 -0
- package/src/i18n/required-surface-keys.ts +120 -1
- package/src/jobs/__tests__/job-queue-depth.integration.test.ts +82 -0
- package/src/jobs/__tests__/jobs.integration.test.ts +329 -3
- package/src/jobs/job-runner.ts +82 -12
- package/src/logging/types.ts +1 -7
- package/src/migrations/pending-rebuilds.ts +1 -1
- package/src/observability/__tests__/observability.integration.test.ts +4 -1
- package/src/observability/__tests__/recording-tracer.test.ts +6 -4
- package/src/observability/index.ts +2 -0
- package/src/observability/noop-provider.ts +0 -9
- package/src/observability/recording-tracer.ts +0 -12
- package/src/observability/standard-metrics.ts +64 -2
- package/src/observability/types/index.ts +1 -29
- package/src/observability/types/metric.ts +1 -56
- package/src/observability/types/provider.ts +1 -32
- package/src/observability/types/span.ts +1 -64
- package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +2 -2
- package/src/pipeline/__tests__/dispatcher.test.ts +366 -1
- package/src/pipeline/__tests__/event-consumer-state.integration.test.ts +31 -0
- package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +126 -0
- package/src/pipeline/__tests__/event-dispatcher-rearm.integration.test.ts +263 -0
- package/src/pipeline/__tests__/event-dispatcher.integration.test.ts +10 -0
- package/src/pipeline/__tests__/job-trigger-consumer.integration.test.ts +106 -0
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +308 -77
- package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +16 -8
- package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
- package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +80 -2
- package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +274 -0
- package/src/pipeline/__tests__/try-append-event.integration.test.ts +166 -0
- package/src/pipeline/dispatch-batch.ts +187 -0
- package/src/pipeline/dispatch-query.ts +165 -0
- package/src/pipeline/dispatch-shared.ts +826 -0
- package/src/pipeline/dispatch-stream.ts +90 -0
- package/src/pipeline/dispatch-write.ts +451 -0
- package/src/pipeline/dispatcher-utils.ts +1 -1
- package/src/pipeline/dispatcher.ts +44 -1372
- package/src/pipeline/entity-cache.ts +2 -33
- package/src/pipeline/event-consumer-state.ts +30 -2
- package/src/pipeline/event-dispatcher-admin.ts +293 -0
- package/src/pipeline/event-dispatcher-delivery.ts +305 -0
- package/src/pipeline/event-dispatcher.ts +85 -542
- package/src/pipeline/index.ts +2 -0
- package/src/pipeline/msp-rebuild.ts +42 -3
- package/src/pipeline/multi-stream-apply-context.ts +4 -42
- package/src/pipeline/projection-rebuild.ts +105 -3
- package/src/pipeline/system-hooks.ts +144 -1
- package/src/rate-limit/__tests__/resolver.integration.test.ts +18 -0
- package/src/rate-limit/resolver.ts +16 -32
- package/src/schema-cli.ts +76 -16
- package/src/search/__tests__/meilisearch-adapter.integration.test.ts +207 -185
- package/src/search/__tests__/meilisearch-ids.test.ts +30 -0
- package/src/search/__tests__/reindex-entity.integration.test.ts +144 -0
- package/src/search/index.ts +6 -0
- package/src/search/meilisearch-adapter.ts +13 -12
- package/src/search/reindex-entity.ts +177 -0
- package/src/search/types.ts +1 -39
- package/src/secrets/__tests__/contains-secret.test.ts +34 -0
- package/src/secrets/__tests__/envelope-cipher.test.ts +59 -0
- package/src/secrets/__tests__/envelope.test.ts +1 -1
- package/src/secrets/dek-cache.ts +26 -5
- package/src/secrets/envelope-cipher.ts +53 -0
- package/src/secrets/envelope.ts +5 -3
- package/src/secrets/index.ts +13 -1
- package/src/secrets/stored-envelope.ts +46 -0
- package/src/secrets/types.ts +2 -162
- package/src/stack/__tests__/event-collector.test.ts +42 -0
- package/src/stack/__tests__/setup-test-stack-jobs.integration.test.ts +125 -0
- package/src/stack/db.ts +2 -1
- package/src/stack/push-entity-projection-tables.ts +4 -3
- package/src/stack/redis.ts +8 -0
- package/src/stack/request-helper.ts +38 -2
- package/src/stack/table-helpers.ts +6 -4
- package/src/stack/test-stack.ts +249 -131
- package/src/testing/__tests__/late-bound.test.ts +32 -0
- package/src/testing/__tests__/wait-for.test.ts +59 -0
- package/src/testing/boot-validator-fixture.ts +121 -0
- package/src/testing/e2e-generator.ts +8 -0
- package/src/testing/file-provider-contract.ts +104 -0
- package/src/testing/handler-context.ts +3 -1
- package/src/testing/index.ts +5 -0
- package/src/testing/late-bound.ts +5 -3
- package/src/testing/mutable-master-key-provider.ts +29 -3
- package/src/testing/wait-for.ts +3 -0
- package/src/testing/without-ambient-temporal.ts +14 -0
- package/src/time/__tests__/tz-dateline.test.ts +8 -10
- package/src/time/geo-tz.ts +1 -32
- package/src/time/index.ts +1 -0
- package/src/time/legacy-date.ts +18 -0
- package/src/time/polyfill.ts +21 -38
- package/src/time/tz-context.ts +44 -85
- package/src/ui-types/app-schema.ts +12 -0
- package/src/ui-types/index.ts +20 -7
- package/src/utils/__tests__/safe-json-temporal.test.ts +18 -0
- package/src/utils/safe-json.ts +13 -1
- package/src/__tests__/raw-table.integration.test.ts +0 -116
- package/src/bun-db/__tests__/bun-test-stack.ts +0 -6
- package/src/db/__tests__/encryption.test.ts +0 -39
- package/src/db/encryption.ts +0 -39
- package/src/db/row-helpers.ts +0 -4
- package/src/engine/__tests__/raw-table.test.ts +0 -150
- package/src/engine/__tests__/unmanaged-table.test.ts +0 -127
- package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +0 -184
- package/src/engine/feature-ast/extractors/round4.ts +0 -1366
|
@@ -56,21 +56,32 @@ const orderFeature = defineFeature("upcastshop", (r) => {
|
|
|
56
56
|
const orderPriced = r.defineEvent(
|
|
57
57
|
"priced",
|
|
58
58
|
z.object({ totalCents: z.number().int(), currency: z.string() }),
|
|
59
|
-
{
|
|
59
|
+
{
|
|
60
|
+
version: 3,
|
|
61
|
+
migrations: [
|
|
62
|
+
// v1 → v2: renamed totalEuros → total (kept as string for this step)
|
|
63
|
+
{
|
|
64
|
+
fromVersion: 1,
|
|
65
|
+
toVersion: 2,
|
|
66
|
+
transform: (payload) => {
|
|
67
|
+
const p = payload as { totalEuros: string };
|
|
68
|
+
return { total: p.totalEuros, currency: "EUR" };
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
// v2 → v3: parse "total" string into integer cents
|
|
72
|
+
{
|
|
73
|
+
fromVersion: 2,
|
|
74
|
+
toVersion: 3,
|
|
75
|
+
transform: (payload) => {
|
|
76
|
+
const p = payload as { total: string; currency: string };
|
|
77
|
+
const euros = Number.parseFloat(p.total);
|
|
78
|
+
return { totalCents: Math.round(euros * 100), currency: p.currency };
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
},
|
|
60
83
|
);
|
|
61
84
|
|
|
62
|
-
// v1 → v2: renamed totalEuros → total (kept as string for this step)
|
|
63
|
-
r.eventMigration("priced", 1, 2, (payload) => {
|
|
64
|
-
const p = payload as { totalEuros: string };
|
|
65
|
-
return { total: p.totalEuros, currency: "EUR" };
|
|
66
|
-
});
|
|
67
|
-
// v2 → v3: parse "total" string into integer cents
|
|
68
|
-
r.eventMigration("priced", 2, 3, (payload) => {
|
|
69
|
-
const p = payload as { total: string; currency: string };
|
|
70
|
-
const euros = Number.parseFloat(p.total);
|
|
71
|
-
return { totalCents: Math.round(euros * 100), currency: p.currency };
|
|
72
|
-
});
|
|
73
|
-
|
|
74
85
|
r.projection({
|
|
75
86
|
name: "order-summary",
|
|
76
87
|
source: "upcast-order",
|
|
@@ -309,18 +320,27 @@ describe("upcaster: async (Marten AsyncOnlyEventUpcaster — DB-Lookups)", () =>
|
|
|
309
320
|
const placed = r.defineEvent(
|
|
310
321
|
"placed",
|
|
311
322
|
z.object({ customerId: z.string(), segment: z.string() }),
|
|
312
|
-
{
|
|
323
|
+
{
|
|
324
|
+
version: 2,
|
|
325
|
+
migrations: [
|
|
326
|
+
{
|
|
327
|
+
fromVersion: 1,
|
|
328
|
+
toVersion: 2,
|
|
329
|
+
transform: async (payload, ctx) => {
|
|
330
|
+
const p = payload as { customerId: string };
|
|
331
|
+
const [row] = await selectMany(ctx.db, customerSegments, {
|
|
332
|
+
customerId: p.customerId,
|
|
333
|
+
});
|
|
334
|
+
return {
|
|
335
|
+
customerId: p.customerId,
|
|
336
|
+
segment: (row as { segment?: string } | undefined)?.segment ?? "UNKNOWN",
|
|
337
|
+
};
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
},
|
|
313
342
|
);
|
|
314
343
|
|
|
315
|
-
r.eventMigration("placed", 1, 2, async (payload, ctx) => {
|
|
316
|
-
const p = payload as { customerId: string };
|
|
317
|
-
const [row] = await selectMany(ctx.db, customerSegments, { customerId: p.customerId });
|
|
318
|
-
return {
|
|
319
|
-
customerId: p.customerId,
|
|
320
|
-
segment: (row as { segment?: string } | undefined)?.segment ?? "UNKNOWN",
|
|
321
|
-
};
|
|
322
|
-
});
|
|
323
|
-
|
|
324
344
|
r.projection({
|
|
325
345
|
name: "async-summary",
|
|
326
346
|
source: "upcast-async-order",
|
|
@@ -385,26 +405,28 @@ describe("upcaster: boot-time validation", () => {
|
|
|
385
405
|
test("defineEvent with version=N and only partial migrations fails at registry build", () => {
|
|
386
406
|
const incomplete = defineFeature("holes", (r) => {
|
|
387
407
|
r.entity("hole-order", orderEntity);
|
|
388
|
-
r.defineEvent("bad", z.object({ v3: z.string() }), { version: 3 });
|
|
389
408
|
// Only 1→2 registered — the 2→3 gap must be rejected.
|
|
390
|
-
r.
|
|
409
|
+
r.defineEvent("bad", z.object({ v3: z.string() }), {
|
|
410
|
+
version: 3,
|
|
411
|
+
migrations: [{ fromVersion: 1, toVersion: 2, transform: (p) => p }],
|
|
412
|
+
});
|
|
391
413
|
});
|
|
392
414
|
expect(() => createRegistry([incomplete])).toThrow(/v2.*v3|covers the step v2/);
|
|
393
415
|
});
|
|
394
416
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
expect(() => createRegistry([orphan])).toThrow(/no r\.defineEvent/i);
|
|
401
|
-
});
|
|
417
|
+
// "migration declared but no defineEvent" is now structurally impossible:
|
|
418
|
+
// migrations live in defineEvent's `migrations` option, always scoped to
|
|
419
|
+
// the event being defined in that same call — there is no longer a
|
|
420
|
+
// registrar shape that can express a dangling migration for an
|
|
421
|
+
// undefined event (formerly the "ghost" registry-validate error path).
|
|
402
422
|
|
|
403
423
|
test("migration toVersion > defineEvent version → rejected", () => {
|
|
404
424
|
const future = defineFeature("future", (r) => {
|
|
405
425
|
r.entity("future-order", orderEntity);
|
|
406
|
-
r.defineEvent("early", z.object({ x: z.number() }), {
|
|
407
|
-
|
|
426
|
+
r.defineEvent("early", z.object({ x: z.number() }), {
|
|
427
|
+
version: 1,
|
|
428
|
+
migrations: [{ fromVersion: 1, toVersion: 2, transform: (p) => p }],
|
|
429
|
+
});
|
|
408
430
|
});
|
|
409
431
|
expect(() => createRegistry([future])).toThrow(/declares only version 1/);
|
|
410
432
|
});
|
|
@@ -412,34 +434,44 @@ describe("upcaster: boot-time validation", () => {
|
|
|
412
434
|
test("non-contiguous (1→2 and 3→4 without 2→3) → rejected", () => {
|
|
413
435
|
const gaps = defineFeature("gaps", (r) => {
|
|
414
436
|
r.entity("gap-order", orderEntity);
|
|
415
|
-
r.defineEvent("jumpy", z.object({ v: z.number() }), {
|
|
416
|
-
|
|
417
|
-
|
|
437
|
+
r.defineEvent("jumpy", z.object({ v: z.number() }), {
|
|
438
|
+
version: 4,
|
|
439
|
+
migrations: [
|
|
440
|
+
{ fromVersion: 1, toVersion: 2, transform: (p) => p },
|
|
441
|
+
{ fromVersion: 3, toVersion: 4, transform: (p) => p },
|
|
442
|
+
],
|
|
443
|
+
});
|
|
418
444
|
});
|
|
419
445
|
expect(() => createRegistry([gaps])).toThrow(/v2.*v3|covers the step v2/);
|
|
420
446
|
});
|
|
421
447
|
});
|
|
422
448
|
|
|
423
449
|
describe("upcaster: registrar input validation", () => {
|
|
424
|
-
test("
|
|
450
|
+
test("defineEvent migrations reject multi-step jumps", () => {
|
|
425
451
|
expect(() =>
|
|
426
452
|
defineFeature("bigstep", (r) => {
|
|
427
453
|
r.entity("bigstep-order", orderEntity);
|
|
428
|
-
r.defineEvent("biz", z.object({ x: z.number() }), {
|
|
429
|
-
|
|
454
|
+
r.defineEvent("biz", z.object({ x: z.number() }), {
|
|
455
|
+
version: 3,
|
|
456
|
+
migrations: [{ fromVersion: 1, toVersion: 3, transform: (p) => p }],
|
|
457
|
+
});
|
|
430
458
|
}),
|
|
431
459
|
).toThrow(/single-step/);
|
|
432
460
|
});
|
|
433
461
|
|
|
434
|
-
test("
|
|
462
|
+
test("defineEvent migrations reject duplicate step", () => {
|
|
435
463
|
expect(() =>
|
|
436
464
|
defineFeature("dupestep", (r) => {
|
|
437
465
|
r.entity("dup-order", orderEntity);
|
|
438
|
-
r.defineEvent("dup", z.object({ x: z.number() }), {
|
|
439
|
-
|
|
440
|
-
|
|
466
|
+
r.defineEvent("dup", z.object({ x: z.number() }), {
|
|
467
|
+
version: 2,
|
|
468
|
+
migrations: [
|
|
469
|
+
{ fromVersion: 1, toVersion: 2, transform: (p) => p },
|
|
470
|
+
{ fromVersion: 1, toVersion: 2, transform: (p) => p },
|
|
471
|
+
],
|
|
472
|
+
});
|
|
441
473
|
}),
|
|
442
|
-
).toThrow(/already
|
|
474
|
+
).toThrow(/already declared/);
|
|
443
475
|
});
|
|
444
476
|
|
|
445
477
|
test("r.defineEvent rejects non-positive version", () => {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// selbst, das Guard-Script selbst.
|
|
9
9
|
|
|
10
10
|
import type { DbRunner } from "../db";
|
|
11
|
-
import { isUniqueViolation } from "../db/pg-error";
|
|
11
|
+
import { constraintOf, isUniqueViolation } from "../db/pg-error";
|
|
12
12
|
import {
|
|
13
13
|
eventPredecessorExists,
|
|
14
14
|
findExistingEventVersion,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
insertRawSubsequentEvent,
|
|
18
18
|
} from "../db/queries/event-store-admin";
|
|
19
19
|
import type { TenantId } from "../engine/types";
|
|
20
|
-
import { VersionConflictError } from "./errors";
|
|
20
|
+
import { IdempotentAppendConflictError, VersionConflictError } from "./errors";
|
|
21
21
|
import type { EventMetadata } from "./event-store";
|
|
22
22
|
|
|
23
23
|
export type RawEventToAppend = {
|
|
@@ -39,6 +39,13 @@ export type RawEventToAppend = {
|
|
|
39
39
|
readonly createdBy: string;
|
|
40
40
|
};
|
|
41
41
|
|
|
42
|
+
function mapEventUniqueViolation(e: unknown, event: RawEventToAppend): Error {
|
|
43
|
+
if (constraintOf(e) === "events_idempotency_uq" && event.metadata.idempotencyKey) {
|
|
44
|
+
return new IdempotentAppendConflictError(event.tenantId, event.metadata.idempotencyKey);
|
|
45
|
+
}
|
|
46
|
+
return new VersionConflictError(event.aggregateId, event.expectedVersion);
|
|
47
|
+
}
|
|
48
|
+
|
|
42
49
|
// Mirrors append()'s two-path structure: typed builder equivalent for v=0,
|
|
43
50
|
// INSERT … SELECT … WHERE EXISTS gate for v>0. Caller-supplied createdAt +
|
|
44
51
|
// createdBy skip the usual userResolver/now() paths.
|
|
@@ -54,7 +61,7 @@ export async function appendRaw(runner: DbRunner, event: RawEventToAppend): Prom
|
|
|
54
61
|
}
|
|
55
62
|
} catch (e) {
|
|
56
63
|
if (isUniqueViolation(e)) {
|
|
57
|
-
throw
|
|
64
|
+
throw mapEventUniqueViolation(e, event);
|
|
58
65
|
}
|
|
59
66
|
throw e;
|
|
60
67
|
}
|
|
@@ -143,7 +150,7 @@ export async function appendRawBatch(
|
|
|
143
150
|
if (isUniqueViolation(e)) {
|
|
144
151
|
// Pre-flight ran but lost a race against a concurrent writer. Rare for
|
|
145
152
|
// migration (single-runner) but possible; we can't name the exact row.
|
|
146
|
-
throw
|
|
153
|
+
throw mapEventUniqueViolation(e, firstEvent);
|
|
147
154
|
}
|
|
148
155
|
throw e;
|
|
149
156
|
}
|
|
@@ -1,35 +1,2 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
// WriteResult error contract (version_conflict).
|
|
4
|
-
|
|
5
|
-
export class VersionConflictError extends Error {
|
|
6
|
-
public readonly aggregateId: string;
|
|
7
|
-
public readonly expectedVersion: number;
|
|
8
|
-
constructor(aggregateId: string, expectedVersion: number) {
|
|
9
|
-
super(
|
|
10
|
-
`Version conflict on aggregate ${aggregateId}: expected predecessor version ${expectedVersion}`,
|
|
11
|
-
);
|
|
12
|
-
this.name = "VersionConflictError";
|
|
13
|
-
this.aggregateId = aggregateId;
|
|
14
|
-
this.expectedVersion = expectedVersion;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Thrown when ctx.appendEvent targets an archived stream. Archived aggregates
|
|
19
|
-
// are read-only — restoreStream() makes them writable again. The archive
|
|
20
|
-
// state is not carried on the events themselves; it lives on the sparse
|
|
21
|
-
// kumiko_archived_streams table. Handlers that need to branch on archive
|
|
22
|
-
// state should call ctx.isStreamArchived(id) first.
|
|
23
|
-
export class ArchivedStreamError extends Error {
|
|
24
|
-
public readonly tenantId: string;
|
|
25
|
-
public readonly aggregateId: string;
|
|
26
|
-
constructor(tenantId: string, aggregateId: string) {
|
|
27
|
-
super(
|
|
28
|
-
`Aggregate ${aggregateId} on tenant ${tenantId} is archived — appendEvent is blocked. ` +
|
|
29
|
-
`Call restoreStream() to re-open the stream before writing.`,
|
|
30
|
-
);
|
|
31
|
-
this.name = "ArchivedStreamError";
|
|
32
|
-
this.tenantId = tenantId;
|
|
33
|
-
this.aggregateId = aggregateId;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
1
|
+
// Legacy path — re-exported for callers still importing this module directly.
|
|
2
|
+
export * from "@cosmicdrift/kumiko-types/event-store-errors";
|
|
@@ -1,42 +1,29 @@
|
|
|
1
|
+
import type { EventMetadata, StoredEvent } from "@cosmicdrift/kumiko-types/event-store-types";
|
|
2
|
+
// Value-only import, aliased to avoid shadowing the ambient global
|
|
3
|
+
// `Temporal` TYPE this file's other Temporal.Instant annotations resolve
|
|
4
|
+
// against (StoredEvent.createdAt et al. — importing the bare name here
|
|
5
|
+
// conflicts with that ambient type, see #1438). Bun doesn't expose Temporal
|
|
6
|
+
// as a runtime value on globalThis, so the un-aliased call below crashed
|
|
7
|
+
// with "Temporal is not defined" outside boot paths that install it (#1480).
|
|
8
|
+
import { Temporal as TemporalPolyfill } from "temporal-polyfill";
|
|
9
|
+
import { encryptEventPayloadPii } from "../crypto/event-pii";
|
|
1
10
|
import type { DbRunner } from "../db";
|
|
2
|
-
import { isUniqueViolation } from "../db/pg-error";
|
|
11
|
+
import { constraintOf, isUniqueViolation } from "../db/pg-error";
|
|
3
12
|
import {
|
|
4
13
|
insertSubsequentEventRow,
|
|
5
14
|
notifyPgChannel,
|
|
6
15
|
selectAggregateMaxVersion,
|
|
7
|
-
selectAggregateStreamTenant,
|
|
8
16
|
selectEventsHighWaterMark,
|
|
9
17
|
selectStreamMaxVersion,
|
|
10
18
|
} from "../db/queries/event-store";
|
|
11
19
|
import { insertOne, selectMany } from "../db/query";
|
|
12
20
|
import type { TenantId } from "../engine/types";
|
|
13
21
|
import { isStreamArchived } from "./archive";
|
|
14
|
-
import { VersionConflictError } from "./errors";
|
|
22
|
+
import { IdempotentAppendConflictError, VersionConflictError } from "./errors";
|
|
15
23
|
import { eventsTable } from "./events-schema";
|
|
16
24
|
import { toStoredEvent } from "./row-to-stored-event";
|
|
17
25
|
|
|
18
|
-
export type EventMetadata
|
|
19
|
-
readonly userId: string;
|
|
20
|
-
readonly requestId?: string;
|
|
21
|
-
// End-to-end business-operation id. Root HTTP requests get it from the
|
|
22
|
-
// x-correlation-id header (default: requestId). MSP-applies inherit it
|
|
23
|
-
// from the triggering event. Lets you trace "which user click caused
|
|
24
|
-
// this email 3 streams later?".
|
|
25
|
-
readonly correlationId?: string;
|
|
26
|
-
// Stored event id that triggered this write. Null for root commands;
|
|
27
|
-
// set to event.id when an MSP-apply runs ctx.appendEvent. Together with
|
|
28
|
-
// correlationId forms a causation DAG across aggregate streams.
|
|
29
|
-
readonly causationId?: string;
|
|
30
|
-
// Marten-conform free key/value space for app-specific metadata that
|
|
31
|
-
// doesn't deserve its own EventMetadata field. Examples: A/B-test bucket,
|
|
32
|
-
// feature-flag snapshot, geo-region, client SDK version. Persisted into
|
|
33
|
-
// events.metadata jsonb (no schema change — it's already a free-form
|
|
34
|
-
// jsonb column), survives upcasters untouched, available on every
|
|
35
|
-
// StoredEvent.metadata.headers. Framework does not interpret values; the
|
|
36
|
-
// app reads them when filtering/auditing. Keep values JSON-primitive
|
|
37
|
-
// (string|number|boolean) so JSON serialization stays bulletproof.
|
|
38
|
-
readonly headers?: Readonly<Record<string, string | number | boolean>>;
|
|
39
|
-
};
|
|
26
|
+
export type { EventMetadata, StoredEvent } from "@cosmicdrift/kumiko-types/event-store-types";
|
|
40
27
|
|
|
41
28
|
export type EventToAppend = {
|
|
42
29
|
readonly aggregateId: string;
|
|
@@ -50,25 +37,6 @@ export type EventToAppend = {
|
|
|
50
37
|
readonly metadata: EventMetadata;
|
|
51
38
|
};
|
|
52
39
|
|
|
53
|
-
// Generic über payload-shape. Default = Record<string, unknown> macht
|
|
54
|
-
// alle existierenden Konsumenten backwards-compatible. Konkrete Apply-
|
|
55
|
-
// Handler / Tests können `StoredEvent<MyEventPayload>` annotieren um
|
|
56
|
-
// payload typed zu lesen. Type-Propagation kommt durch r.defineEvent +
|
|
57
|
-
// SingleStreamApplyFn<T> in apply-Maps.
|
|
58
|
-
export type StoredEvent<TPayload = Record<string, unknown>> = {
|
|
59
|
-
readonly id: string;
|
|
60
|
-
readonly aggregateId: string;
|
|
61
|
-
readonly aggregateType: string;
|
|
62
|
-
readonly tenantId: TenantId;
|
|
63
|
-
readonly version: number;
|
|
64
|
-
readonly type: string;
|
|
65
|
-
readonly eventVersion: number;
|
|
66
|
-
readonly payload: TPayload;
|
|
67
|
-
readonly metadata: EventMetadata;
|
|
68
|
-
readonly createdAt: Temporal.Instant;
|
|
69
|
-
readonly createdBy: string;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
40
|
type SelectedEvent = {
|
|
73
41
|
readonly id: bigint;
|
|
74
42
|
readonly aggregateId: string;
|
|
@@ -108,27 +76,36 @@ type SelectedEvent = {
|
|
|
108
76
|
export const EVENTS_PUBSUB_CHANNEL = "kumiko_events_new";
|
|
109
77
|
|
|
110
78
|
export async function append(db: DbRunner, event: EventToAppend): Promise<StoredEvent> {
|
|
111
|
-
|
|
112
|
-
|
|
79
|
+
// Event-PII (#799): stored payload AND returned echo carry ciphertext, so
|
|
80
|
+
// inline projections and rebuilds materialize identical rows.
|
|
81
|
+
const payload = await encryptEventPayloadPii(event.type, event.payload);
|
|
82
|
+
const toStore = payload === event.payload ? event : { ...event, payload };
|
|
83
|
+
const newVersion = toStore.expectedVersion + 1;
|
|
84
|
+
const eventVersion = toStore.eventVersion ?? 1;
|
|
113
85
|
|
|
114
86
|
try {
|
|
115
87
|
const row =
|
|
116
|
-
|
|
117
|
-
? await insertFirstEvent(db,
|
|
118
|
-
: await insertSubsequentEvent(db,
|
|
88
|
+
toStore.expectedVersion === 0
|
|
89
|
+
? await insertFirstEvent(db, toStore, newVersion, eventVersion)
|
|
90
|
+
: await insertSubsequentEvent(db, toStore, newVersion, eventVersion);
|
|
119
91
|
|
|
120
92
|
// NOTIFY fires on commit (PG buffers NOTIFY per TX), so subscribers never
|
|
121
93
|
// see a wake-up for an event that later rolled back. Harmless no-op when
|
|
122
94
|
// no LISTENer is attached.
|
|
123
95
|
await notifyPgChannel(db, EVENTS_PUBSUB_CHANNEL);
|
|
124
96
|
|
|
125
|
-
return buildStoredEvent(
|
|
97
|
+
return buildStoredEvent(toStore, newVersion, eventVersion, row);
|
|
126
98
|
} catch (e) {
|
|
127
99
|
if (isUniqueViolation(e)) {
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
//
|
|
100
|
+
// Two unique constraints on this table: events_aggregate_version_uq
|
|
101
|
+
// (tenant_id, aggregate_id, version) — a concurrent writer won the
|
|
102
|
+
// race to the next version — and events_idempotency_uq (tenant_id,
|
|
103
|
+
// metadata->>'idempotencyKey') — the caller reused an idempotency key.
|
|
104
|
+
// constraintOf() tells them apart; unknown/renamed constraint falls
|
|
105
|
+
// back to VersionConflictError, the pre-existing behaviour.
|
|
106
|
+
if (constraintOf(e) === "events_idempotency_uq" && event.metadata.idempotencyKey) {
|
|
107
|
+
throw new IdempotentAppendConflictError(event.tenantId, event.metadata.idempotencyKey);
|
|
108
|
+
}
|
|
132
109
|
throw new VersionConflictError(event.aggregateId, event.expectedVersion);
|
|
133
110
|
}
|
|
134
111
|
throw e;
|
|
@@ -181,12 +158,20 @@ async function insertSubsequentEvent(
|
|
|
181
158
|
expectedVersion: event.expectedVersion,
|
|
182
159
|
});
|
|
183
160
|
if (!row) throw new VersionConflictError(event.aggregateId, event.expectedVersion);
|
|
161
|
+
const createdAt =
|
|
162
|
+
row.created_at instanceof Date
|
|
163
|
+
? TemporalPolyfill.Instant.fromEpochMilliseconds(row.created_at.getTime())
|
|
164
|
+
: TemporalPolyfill.Instant.from(row.created_at);
|
|
184
165
|
return {
|
|
185
166
|
id: typeof row.id === "bigint" ? row.id : BigInt(row.id),
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
167
|
+
// @cast-boundary temporal-polyfill-vs-ambient: same TC39 Temporal.Instant
|
|
168
|
+
// at runtime, two separate .d.ts sources (temporal-polyfill's bundled
|
|
169
|
+
// types vs the ambient temporal-spec global) disagree on a couple of
|
|
170
|
+
// method-overload signatures (until/equals argument types), so TS treats
|
|
171
|
+
// them as nominally distinct. Safe: this value is only ever consumed as
|
|
172
|
+
// a Temporal.Instant (comparisons, .toString(), arithmetic), never
|
|
173
|
+
// narrowed on the specific overload shape that differs.
|
|
174
|
+
createdAt: createdAt as unknown as Temporal.Instant,
|
|
190
175
|
};
|
|
191
176
|
}
|
|
192
177
|
|
|
@@ -276,29 +261,19 @@ export async function getStreamVersion(
|
|
|
276
261
|
return selectStreamMaxVersion(db, aggregateId, tenantId);
|
|
277
262
|
}
|
|
278
263
|
|
|
279
|
-
/** MAX(version) for one aggregate — no tenant filter.
|
|
264
|
+
/** MAX(version) for one aggregate — no tenant filter. SECURITY: existence-oracle,
|
|
265
|
+
* a caller can probe whether an aggregateId has any events regardless of tenant
|
|
266
|
+
* membership. Only call from seed/system-internal paths (idempotency checks
|
|
267
|
+
* against a known aggregateId) — never from a handler reachable with
|
|
268
|
+
* caller-controlled input. */
|
|
280
269
|
// @wrapper-known semantic-alias
|
|
281
|
-
export async function
|
|
270
|
+
export async function getUnscopedAggregateStreamMaxVersion(
|
|
282
271
|
db: DbRunner,
|
|
283
272
|
aggregateId: string,
|
|
284
273
|
): Promise<number> {
|
|
285
274
|
return selectAggregateMaxVersion(db, aggregateId);
|
|
286
275
|
}
|
|
287
276
|
|
|
288
|
-
/** Stream tenant of an aggregate (the tenant_id its events live under), with no
|
|
289
|
-
* membership/tenant filter. Recovers the write target for a systemScope
|
|
290
|
-
* aggregate whose stream tenant isn't one of the subject's memberships.
|
|
291
|
-
* Returns null for unknown streams. */
|
|
292
|
-
export async function getAggregateStreamTenant(
|
|
293
|
-
db: DbRunner,
|
|
294
|
-
aggregateId: string,
|
|
295
|
-
aggregateType: string,
|
|
296
|
-
): Promise<TenantId | null> {
|
|
297
|
-
const tenantId = await selectAggregateStreamTenant(db, aggregateId, aggregateType);
|
|
298
|
-
// DB-boundary: kumiko_events.tenant_id is a TenantId-shaped uuid column.
|
|
299
|
-
return tenantId as TenantId | null;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
277
|
// Global high-water-mark = MAX(events.id). Marten/Wolverine standard for
|
|
303
278
|
// projection/consumer lag math: lag = HWM - cursor. Single-row aggregate over
|
|
304
279
|
// the bigserial PK index — sub-millisecond cost. Returns 0n on an empty log
|
|
@@ -328,15 +303,19 @@ export async function loadEventsAfterVersion(
|
|
|
328
303
|
|
|
329
304
|
// Load every event for an aggregate_type across all tenants. Ordered by
|
|
330
305
|
// (created_at, id) — chronological replay order for projection rebuilds.
|
|
331
|
-
//
|
|
332
|
-
// CAUTION — buffers ALL matching events in memory. Safe for smaller
|
|
333
|
-
// aggregate-types (≤ 100k events), a memory cliff for large stores.
|
|
334
|
-
// For >100k events use `streamAllEventsByType` (yields batchwise).
|
|
335
306
|
// Mostly called from tests today — production rebuild goes through
|
|
336
307
|
// projection-rebuild's own streaming path.
|
|
308
|
+
//
|
|
309
|
+
// Fails loud past LOAD_ALL_EVENTS_ROW_LIMIT rather than silently buffering
|
|
310
|
+
// an unbounded result set — that's the memory cliff this guard exists to
|
|
311
|
+
// prevent.
|
|
312
|
+
export const LOAD_ALL_EVENTS_ROW_LIMIT = 100_000;
|
|
313
|
+
|
|
314
|
+
/** Buffers ALL matching events in memory — a memory cliff for large stores. Use `streamAllEventsByType` (yields batchwise) for production reads; this is test-only in practice. */
|
|
337
315
|
export async function loadAllEventsByType(
|
|
338
316
|
db: DbRunner,
|
|
339
317
|
aggregateType: string,
|
|
318
|
+
rowLimit: number = LOAD_ALL_EVENTS_ROW_LIMIT,
|
|
340
319
|
): Promise<readonly StoredEvent[]> {
|
|
341
320
|
const rows = await selectMany<SelectedEvent>(
|
|
342
321
|
db,
|
|
@@ -347,8 +326,15 @@ export async function loadAllEventsByType(
|
|
|
347
326
|
{ col: "createdAt", direction: "asc" },
|
|
348
327
|
{ col: "id", direction: "asc" },
|
|
349
328
|
],
|
|
329
|
+
limit: rowLimit + 1,
|
|
350
330
|
},
|
|
351
331
|
);
|
|
332
|
+
if (rows.length > rowLimit) {
|
|
333
|
+
throw new Error(
|
|
334
|
+
`loadAllEventsByType("${aggregateType}") exceeds ${rowLimit} rows — ` +
|
|
335
|
+
"use streamAllEventsByType instead of buffering the full result set in memory.",
|
|
336
|
+
);
|
|
337
|
+
}
|
|
352
338
|
return rows.map(toStoredEvent);
|
|
353
339
|
}
|
|
354
340
|
|
|
@@ -12,28 +12,22 @@ import {
|
|
|
12
12
|
uniqueIndex,
|
|
13
13
|
uuid,
|
|
14
14
|
} from "../db/dialect";
|
|
15
|
+
import { ensureIdempotencyKeyIndex } from "../db/queries/event-store";
|
|
15
16
|
import { unsafePushTables } from "../stack";
|
|
16
17
|
import { createArchivedStreamsTable } from "./archive";
|
|
17
18
|
import { createSnapshotsTable } from "./snapshot";
|
|
19
|
+
import type { EventMetadata } from "./types";
|
|
18
20
|
|
|
19
21
|
// Event-store schema as a Drizzle table. The typed select/insert path handles
|
|
20
22
|
// most operations; append() for subsequent versions uses raw SQL because
|
|
21
23
|
// INSERT ... SELECT ... WHERE EXISTS isn't ergonomic in the typed builder.
|
|
22
24
|
//
|
|
23
25
|
// HTTP-level retry idempotency is handled by pipeline/idempotency.ts
|
|
24
|
-
// (Redis-backed check + cached-response replay).
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
readonly requestId?: string;
|
|
30
|
-
readonly correlationId?: string;
|
|
31
|
-
readonly causationId?: string;
|
|
32
|
-
// App-specific free key/value (Marten "headers"). Mirror of the canonical
|
|
33
|
-
// type in event-store.ts — kept duplicate because events-schema must stay
|
|
34
|
-
// import-cycle-free vs the event-store module.
|
|
35
|
-
readonly headers?: Readonly<Record<string, string | number | boolean>>;
|
|
36
|
-
};
|
|
26
|
+
// (Redis-backed check + cached-response replay); metadata.requestId is
|
|
27
|
+
// purely a trace marker (no uniqueness constraint — one request may write
|
|
28
|
+
// N events). Callers that need a hard per-event guarantee as a second line
|
|
29
|
+
// of defense set metadata.idempotencyKey, enforced by the tenant-scoped
|
|
30
|
+
// partial unique index ensureIdempotencyKeyIndex() creates below.
|
|
37
31
|
|
|
38
32
|
export const eventsTable = pgTable(
|
|
39
33
|
"kumiko_events",
|
|
@@ -86,6 +80,10 @@ export async function createEventsTable(db: DbConnection): Promise<void> {
|
|
|
86
80
|
if (!(await tableExists(db, "public.kumiko_events"))) {
|
|
87
81
|
await unsafePushTables(db, { kumikoEvents: eventsTable });
|
|
88
82
|
}
|
|
83
|
+
// Runs unconditionally (both fresh + already-existing table) so installs
|
|
84
|
+
// that predate the idempotency-key index get healed the same way
|
|
85
|
+
// ensureSnapshotVersionColumn heals kumiko_snapshots.
|
|
86
|
+
await ensureIdempotencyKeyIndex(db);
|
|
89
87
|
await createArchivedStreamsTable(db);
|
|
90
88
|
await createSnapshotsTable(db);
|
|
91
89
|
}
|
package/src/event-store/index.ts
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export {
|
|
2
|
+
backfillEventPiiEncryption,
|
|
3
|
+
type PiiBackfillFailure,
|
|
4
|
+
type PiiBackfillOptions,
|
|
5
|
+
type PiiBackfillResult,
|
|
6
|
+
} from "../db/queries/backfill-pii";
|
|
1
7
|
export {
|
|
2
8
|
type ArchiveStreamArgs,
|
|
3
9
|
archivedStreamsTable,
|
|
@@ -6,16 +12,16 @@ export {
|
|
|
6
12
|
isStreamArchived,
|
|
7
13
|
restoreStream,
|
|
8
14
|
} from "./archive";
|
|
9
|
-
export { ArchivedStreamError, VersionConflictError } from "./errors";
|
|
15
|
+
export { ArchivedStreamError, IdempotentAppendConflictError, VersionConflictError } from "./errors";
|
|
10
16
|
export {
|
|
11
17
|
append,
|
|
12
18
|
EVENTS_PUBSUB_CHANNEL,
|
|
13
19
|
type EventMetadata,
|
|
14
20
|
type EventToAppend,
|
|
15
|
-
getAggregateStreamMaxVersion,
|
|
16
|
-
getAggregateStreamTenant,
|
|
17
21
|
getEventsHighWaterMark,
|
|
18
22
|
getStreamVersion,
|
|
23
|
+
getUnscopedAggregateStreamMaxVersion,
|
|
24
|
+
LOAD_ALL_EVENTS_ROW_LIMIT,
|
|
19
25
|
loadAggregate,
|
|
20
26
|
loadAggregateAsOf,
|
|
21
27
|
loadAllEventsByType,
|
|
@@ -24,9 +30,18 @@ export {
|
|
|
24
30
|
streamAllEventsByType,
|
|
25
31
|
} from "./event-store";
|
|
26
32
|
export { createEventsTable, eventsTable } from "./events-schema";
|
|
33
|
+
export {
|
|
34
|
+
createRebuildDeadLetterTable,
|
|
35
|
+
listRebuildDeadLetters,
|
|
36
|
+
type RebuildDeadLetterRow,
|
|
37
|
+
rebuildDeadLetterTable,
|
|
38
|
+
recordRebuildDeadLetters,
|
|
39
|
+
type SkippedApply,
|
|
40
|
+
} from "./rebuild-dead-letter";
|
|
27
41
|
export { toStoredEvent } from "./row-to-stored-event";
|
|
28
42
|
export {
|
|
29
43
|
createSnapshotsTable,
|
|
44
|
+
type LoadAggregateWithSnapshotOptions,
|
|
30
45
|
type LoadAggregateWithSnapshotResult,
|
|
31
46
|
loadAggregateWithSnapshot,
|
|
32
47
|
loadLatestSnapshot,
|