@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
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
createEntity,
|
|
5
5
|
createRegistry,
|
|
6
6
|
createTextField,
|
|
7
|
+
type DeleteContext,
|
|
7
8
|
defineFeature,
|
|
8
9
|
type PostSaveHookFn,
|
|
9
10
|
type PreSaveHookFn,
|
|
@@ -201,74 +202,78 @@ describe("runPostSave", () => {
|
|
|
201
202
|
test("postSave errors don't throw — logged and continued", async () => {
|
|
202
203
|
const calls: string[] = [];
|
|
203
204
|
const consoleSpy = spyOn(console, "error").mockImplementation(() => {});
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
},
|
|
210
|
-
],
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
const systemHooks: SystemHooks = {
|
|
214
|
-
postSave: [
|
|
215
|
-
{
|
|
216
|
-
name: "search",
|
|
217
|
-
priority: 1000,
|
|
218
|
-
fn: async () => {
|
|
219
|
-
calls.push("search-ran");
|
|
205
|
+
try {
|
|
206
|
+
const registry = makeRegistry({
|
|
207
|
+
postSave: [
|
|
208
|
+
async () => {
|
|
209
|
+
throw new Error("feature-fail");
|
|
220
210
|
},
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
const pipeline = createLifecycleHooks(registry, systemHooks);
|
|
226
|
-
// Should not throw
|
|
227
|
-
await pipeline.runPostSave("test:write:user", savectx, {});
|
|
211
|
+
],
|
|
212
|
+
});
|
|
228
213
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
214
|
+
const systemHooks: SystemHooks = {
|
|
215
|
+
postSave: [
|
|
216
|
+
{
|
|
217
|
+
name: "search",
|
|
218
|
+
priority: 1000,
|
|
219
|
+
fn: async () => {
|
|
220
|
+
calls.push("search-ran");
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const pipeline = createLifecycleHooks(registry, systemHooks);
|
|
227
|
+
// Should not throw
|
|
228
|
+
await pipeline.runPostSave("test:write:user", savectx, {});
|
|
229
|
+
|
|
230
|
+
// System hook still ran despite feature hook failure
|
|
231
|
+
expect(calls).toContain("search-ran");
|
|
232
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
233
|
+
} finally {
|
|
234
|
+
consoleSpy.mockRestore();
|
|
235
|
+
}
|
|
233
236
|
});
|
|
234
237
|
|
|
235
238
|
test("system hook failure doesn't block other system hooks", async () => {
|
|
236
239
|
const calls: string[] = [];
|
|
237
240
|
const consoleSpy = spyOn(console, "error").mockImplementation(() => {});
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
241
|
+
try {
|
|
242
|
+
const registry = makeRegistry();
|
|
243
|
+
|
|
244
|
+
const systemHooks: SystemHooks = {
|
|
245
|
+
postSave: [
|
|
246
|
+
{
|
|
247
|
+
name: "search",
|
|
248
|
+
priority: 1000,
|
|
249
|
+
fn: async () => {
|
|
250
|
+
throw new Error("meili-down");
|
|
251
|
+
},
|
|
248
252
|
},
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
253
|
+
{
|
|
254
|
+
name: "sse",
|
|
255
|
+
priority: 1001,
|
|
256
|
+
fn: async () => {
|
|
257
|
+
calls.push("sse-ran");
|
|
258
|
+
},
|
|
255
259
|
},
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
260
|
+
{
|
|
261
|
+
name: "audit",
|
|
262
|
+
priority: 1002,
|
|
263
|
+
fn: async () => {
|
|
264
|
+
calls.push("audit-ran");
|
|
265
|
+
},
|
|
262
266
|
},
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
};
|
|
267
|
+
],
|
|
268
|
+
};
|
|
266
269
|
|
|
267
|
-
|
|
268
|
-
|
|
270
|
+
const pipeline = createLifecycleHooks(registry, systemHooks);
|
|
271
|
+
await pipeline.runPostSave("test:write:user", savectx, {});
|
|
269
272
|
|
|
270
|
-
|
|
271
|
-
|
|
273
|
+
expect(calls).toEqual(["sse-ran", "audit-ran"]);
|
|
274
|
+
} finally {
|
|
275
|
+
consoleSpy.mockRestore();
|
|
276
|
+
}
|
|
272
277
|
});
|
|
273
278
|
});
|
|
274
279
|
|
|
@@ -335,29 +340,32 @@ describe("runPostSave phase routing", () => {
|
|
|
335
340
|
|
|
336
341
|
test("afterCommit phase: hook errors are logged, never thrown", async () => {
|
|
337
342
|
const consoleSpy = spyOn(console, "error").mockImplementation(() => {});
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
+
try {
|
|
344
|
+
const afterRan: string[] = [];
|
|
345
|
+
const feature = defineFeature("phases", (r) => {
|
|
346
|
+
r.entity("user", createEntity({ table: "Users", fields: {} }));
|
|
347
|
+
r.writeHandler("user", z.object({}), async () => ({ isSuccess: true, data: null }), {
|
|
348
|
+
access: { openToAll: true },
|
|
349
|
+
});
|
|
350
|
+
r.hook("postSave", "user", async () => {
|
|
351
|
+
throw new Error("afterCommit-boom");
|
|
352
|
+
});
|
|
353
|
+
r.hook("postSave", "user", async () => {
|
|
354
|
+
afterRan.push("second");
|
|
355
|
+
});
|
|
343
356
|
});
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
});
|
|
347
|
-
r.hook("postSave", "user", async () => {
|
|
348
|
-
afterRan.push("second");
|
|
349
|
-
});
|
|
350
|
-
});
|
|
351
|
-
const registry = createRegistry([feature]);
|
|
352
|
-
const pipeline = createLifecycleHooks(registry);
|
|
357
|
+
const registry = createRegistry([feature]);
|
|
358
|
+
const pipeline = createLifecycleHooks(registry);
|
|
353
359
|
|
|
354
|
-
|
|
355
|
-
|
|
360
|
+
// Must not throw — errors are swallowed + logged
|
|
361
|
+
await pipeline.runPostSave("phases:write:user", savectx, {}, "afterCommit");
|
|
356
362
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
363
|
+
// Subsequent hooks still fire (failure in one hook doesn't block the rest)
|
|
364
|
+
expect(afterRan).toEqual(["second"]);
|
|
365
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
366
|
+
} finally {
|
|
367
|
+
consoleSpy.mockRestore();
|
|
368
|
+
}
|
|
361
369
|
});
|
|
362
370
|
|
|
363
371
|
test("system hooks respect their phase setting", async () => {
|
|
@@ -424,7 +432,7 @@ describe("Sprint 8a: per-tenant entity-hook filter", () => {
|
|
|
424
432
|
});
|
|
425
433
|
|
|
426
434
|
const featureB = defineFeature("feat-b", (r) => {
|
|
427
|
-
r.
|
|
435
|
+
r.hook("postSave", { allOf: "widget" }, async (_result, ctx) => {
|
|
428
436
|
calls.push({ tenant: ctx._tenantId ?? "no-tenant" });
|
|
429
437
|
});
|
|
430
438
|
});
|
|
@@ -528,3 +536,226 @@ describe("buildEventId — dedup key construction", () => {
|
|
|
528
536
|
expect(buildEventId("handler", { id: 0 }, "phase")).toBeNull();
|
|
529
537
|
});
|
|
530
538
|
});
|
|
539
|
+
|
|
540
|
+
// --- PreDelete / PostDelete pipeline ---
|
|
541
|
+
|
|
542
|
+
const deletectx: DeleteContext = {
|
|
543
|
+
kind: "delete",
|
|
544
|
+
id: 1,
|
|
545
|
+
data: { email: "test@test.de" },
|
|
546
|
+
entityName: "user",
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
describe("runPreDelete", () => {
|
|
550
|
+
test("runs feature + entity + system hooks, all inTransaction (throw on error)", async () => {
|
|
551
|
+
const calls: string[] = [];
|
|
552
|
+
const feature = defineFeature("test", (r) => {
|
|
553
|
+
r.entity("user", createEntity({ table: "Users", fields: { email: createTextField() } }));
|
|
554
|
+
r.writeHandler("user", z.object({}), async () => ({ isSuccess: true as const, data: null }), {
|
|
555
|
+
access: { openToAll: true },
|
|
556
|
+
});
|
|
557
|
+
r.hook("preDelete", "user", async () => {
|
|
558
|
+
calls.push("handler");
|
|
559
|
+
});
|
|
560
|
+
r.hook("preDelete", { allOf: "user" }, async () => {
|
|
561
|
+
calls.push("entity");
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
const registry = createRegistry([feature]);
|
|
565
|
+
const systemHooks: SystemHooks = {
|
|
566
|
+
preDelete: [
|
|
567
|
+
{
|
|
568
|
+
name: "sys",
|
|
569
|
+
priority: 1000,
|
|
570
|
+
fn: async () => {
|
|
571
|
+
calls.push("system");
|
|
572
|
+
},
|
|
573
|
+
},
|
|
574
|
+
],
|
|
575
|
+
};
|
|
576
|
+
const pipeline = createLifecycleHooks(registry, systemHooks);
|
|
577
|
+
await pipeline.runPreDelete("test:write:user", deletectx, {});
|
|
578
|
+
expect(calls).toEqual(["handler", "entity", "system"]);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test("a hook throwing aborts the delete (rejects, not swallowed)", async () => {
|
|
582
|
+
const feature = defineFeature("test", (r) => {
|
|
583
|
+
r.entity("user", createEntity({ table: "Users", fields: { email: createTextField() } }));
|
|
584
|
+
r.writeHandler("user", z.object({}), async () => ({ isSuccess: true as const, data: null }), {
|
|
585
|
+
access: { openToAll: true },
|
|
586
|
+
});
|
|
587
|
+
r.hook("preDelete", "user", async () => {
|
|
588
|
+
throw new Error("blocked-delete");
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
const registry = createRegistry([feature]);
|
|
592
|
+
const pipeline = createLifecycleHooks(registry);
|
|
593
|
+
await expect(pipeline.runPreDelete("test:write:user", deletectx, {})).rejects.toThrow(
|
|
594
|
+
"blocked-delete",
|
|
595
|
+
);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test("system hook with a non-inTransaction phase is skipped", async () => {
|
|
599
|
+
const registry = makeRegistry();
|
|
600
|
+
const calls: string[] = [];
|
|
601
|
+
const systemHooks: SystemHooks = {
|
|
602
|
+
preDelete: [
|
|
603
|
+
{
|
|
604
|
+
name: "sys",
|
|
605
|
+
priority: 1000,
|
|
606
|
+
phase: "afterCommit",
|
|
607
|
+
fn: async () => {
|
|
608
|
+
calls.push("system");
|
|
609
|
+
},
|
|
610
|
+
},
|
|
611
|
+
],
|
|
612
|
+
};
|
|
613
|
+
const pipeline = createLifecycleHooks(registry, systemHooks);
|
|
614
|
+
await pipeline.runPreDelete("test:write:user", deletectx, {});
|
|
615
|
+
expect(calls).toEqual([]);
|
|
616
|
+
});
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
describe("runPostDelete", () => {
|
|
620
|
+
test("runs feature then system hooks (best-effort by default phase)", async () => {
|
|
621
|
+
const calls: string[] = [];
|
|
622
|
+
const feature = defineFeature("test", (r) => {
|
|
623
|
+
r.entity("user", createEntity({ table: "Users", fields: { email: createTextField() } }));
|
|
624
|
+
r.writeHandler("user", z.object({}), async () => ({ isSuccess: true as const, data: null }), {
|
|
625
|
+
access: { openToAll: true },
|
|
626
|
+
});
|
|
627
|
+
r.hook("postDelete", "user", async () => {
|
|
628
|
+
calls.push("feature");
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
const registry = createRegistry([feature]);
|
|
632
|
+
const systemHooks: SystemHooks = {
|
|
633
|
+
postDelete: [
|
|
634
|
+
{
|
|
635
|
+
name: "sys",
|
|
636
|
+
priority: 1000,
|
|
637
|
+
fn: async () => {
|
|
638
|
+
calls.push("system");
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
],
|
|
642
|
+
};
|
|
643
|
+
const pipeline = createLifecycleHooks(registry, systemHooks);
|
|
644
|
+
await pipeline.runPostDelete("test:write:user", deletectx, {});
|
|
645
|
+
expect(calls).toEqual(["feature", "system"]);
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
test("inTransaction phase: hook errors throw", async () => {
|
|
649
|
+
const feature = defineFeature("test", (r) => {
|
|
650
|
+
r.entity("user", createEntity({ table: "Users", fields: { email: createTextField() } }));
|
|
651
|
+
r.writeHandler("user", z.object({}), async () => ({ isSuccess: true as const, data: null }), {
|
|
652
|
+
access: { openToAll: true },
|
|
653
|
+
});
|
|
654
|
+
r.hook(
|
|
655
|
+
"postDelete",
|
|
656
|
+
"user",
|
|
657
|
+
async () => {
|
|
658
|
+
throw new Error("postDelete-inTx-boom");
|
|
659
|
+
},
|
|
660
|
+
{ phase: "inTransaction" },
|
|
661
|
+
);
|
|
662
|
+
});
|
|
663
|
+
const registry = createRegistry([feature]);
|
|
664
|
+
const pipeline = createLifecycleHooks(registry);
|
|
665
|
+
await expect(
|
|
666
|
+
pipeline.runPostDelete("test:write:user", deletectx, {}, "inTransaction"),
|
|
667
|
+
).rejects.toThrow("postDelete-inTx-boom");
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
// --- Batch hooks ---
|
|
672
|
+
|
|
673
|
+
describe("runPostSaveBatch / runPostDeleteBatch", () => {
|
|
674
|
+
test("no batch hooks registered → resolves without throwing", async () => {
|
|
675
|
+
const pipeline = createLifecycleHooks(makeRegistry());
|
|
676
|
+
await expect(pipeline.runPostSaveBatch([savectx], {})).resolves.toBeUndefined();
|
|
677
|
+
await expect(pipeline.runPostDeleteBatch([deletectx], {})).resolves.toBeUndefined();
|
|
678
|
+
// Should not throw — nothing registered.
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
test("runPostSaveBatch runs all system hooks concurrently with the batch payload", async () => {
|
|
682
|
+
const seen: (readonly SaveContext[])[] = [];
|
|
683
|
+
const systemHooks: SystemHooks = {
|
|
684
|
+
postSaveBatch: [
|
|
685
|
+
{
|
|
686
|
+
name: "a",
|
|
687
|
+
priority: 1000,
|
|
688
|
+
fn: async (results) => {
|
|
689
|
+
seen.push(results);
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
],
|
|
693
|
+
};
|
|
694
|
+
const pipeline = createLifecycleHooks(makeRegistry(), systemHooks);
|
|
695
|
+
await pipeline.runPostSaveBatch([savectx], {});
|
|
696
|
+
expect(seen).toEqual([[savectx]]);
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
test("runPostDeleteBatch runs all system hooks with the batch payload", async () => {
|
|
700
|
+
const seen: (readonly DeleteContext[])[] = [];
|
|
701
|
+
const systemHooks: SystemHooks = {
|
|
702
|
+
postDeleteBatch: [
|
|
703
|
+
{
|
|
704
|
+
name: "a",
|
|
705
|
+
priority: 1000,
|
|
706
|
+
fn: async (payloads) => {
|
|
707
|
+
seen.push(payloads);
|
|
708
|
+
},
|
|
709
|
+
},
|
|
710
|
+
],
|
|
711
|
+
};
|
|
712
|
+
const pipeline = createLifecycleHooks(makeRegistry(), systemHooks);
|
|
713
|
+
await pipeline.runPostDeleteBatch([deletectx], {});
|
|
714
|
+
expect(seen).toEqual([[deletectx]]);
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test.each([
|
|
718
|
+
[
|
|
719
|
+
"postSaveBatch",
|
|
720
|
+
(hooks: { name: string; priority: number; fn: () => Promise<void> }[]) =>
|
|
721
|
+
({ postSaveBatch: hooks }) satisfies SystemHooks,
|
|
722
|
+
(pipeline: ReturnType<typeof createLifecycleHooks>) =>
|
|
723
|
+
pipeline.runPostSaveBatch([savectx], {}),
|
|
724
|
+
],
|
|
725
|
+
[
|
|
726
|
+
"postDeleteBatch",
|
|
727
|
+
(hooks: { name: string; priority: number; fn: () => Promise<void> }[]) =>
|
|
728
|
+
({ postDeleteBatch: hooks }) satisfies SystemHooks,
|
|
729
|
+
(pipeline: ReturnType<typeof createLifecycleHooks>) =>
|
|
730
|
+
pipeline.runPostDeleteBatch([deletectx], {}),
|
|
731
|
+
],
|
|
732
|
+
])("one %s hook throwing doesn't stop the others (Promise.allSettled) — logged, never thrown", async (_name, buildHooks, run) => {
|
|
733
|
+
const consoleSpy = spyOn(console, "error").mockImplementation(() => {});
|
|
734
|
+
try {
|
|
735
|
+
const calls: string[] = [];
|
|
736
|
+
const systemHooks = buildHooks([
|
|
737
|
+
{
|
|
738
|
+
name: "failing",
|
|
739
|
+
priority: 1000,
|
|
740
|
+
fn: async () => {
|
|
741
|
+
throw new Error("batch-hook-boom");
|
|
742
|
+
},
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
name: "ok",
|
|
746
|
+
priority: 1001,
|
|
747
|
+
fn: async () => {
|
|
748
|
+
calls.push("ok-ran");
|
|
749
|
+
},
|
|
750
|
+
},
|
|
751
|
+
]);
|
|
752
|
+
const pipeline = createLifecycleHooks(makeRegistry(), systemHooks);
|
|
753
|
+
// Must not throw.
|
|
754
|
+
await run(pipeline);
|
|
755
|
+
expect(calls).toEqual(["ok-ran"]);
|
|
756
|
+
expect(consoleSpy).toHaveBeenCalled();
|
|
757
|
+
} finally {
|
|
758
|
+
consoleSpy.mockRestore();
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
});
|
|
@@ -45,15 +45,23 @@ const asOfFeature = defineFeature("asoftest", (r) => {
|
|
|
45
45
|
const approved = r.defineEvent(
|
|
46
46
|
"approved",
|
|
47
47
|
z.object({ amount: z.number().int(), approvedBy: z.string() }),
|
|
48
|
-
{
|
|
48
|
+
{
|
|
49
|
+
version: 2,
|
|
50
|
+
migrations: [
|
|
51
|
+
{
|
|
52
|
+
fromVersion: 1,
|
|
53
|
+
toVersion: 2,
|
|
54
|
+
transform: (payload) => {
|
|
55
|
+
const p = payload as { amount: string; approvedBy: string };
|
|
56
|
+
return {
|
|
57
|
+
amount: Math.round(Number.parseFloat(p.amount) * 100),
|
|
58
|
+
approvedBy: p.approvedBy,
|
|
59
|
+
};
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
},
|
|
49
64
|
);
|
|
50
|
-
r.eventMigration("approved", 1, 2, (payload) => {
|
|
51
|
-
const p = payload as { amount: string; approvedBy: string };
|
|
52
|
-
return {
|
|
53
|
-
amount: Math.round(Number.parseFloat(p.amount) * 100),
|
|
54
|
-
approvedBy: p.approvedBy,
|
|
55
|
-
};
|
|
56
|
-
});
|
|
57
65
|
|
|
58
66
|
const executor = createEventStoreExecutor(invoiceTable, invoiceEntity, {
|
|
59
67
|
entityName: "asof-invoice",
|
|
@@ -65,7 +65,7 @@ const postQueryFeature = defineFeature("postquerytest", (r) => {
|
|
|
65
65
|
r.hook("postQuery", "widget:list", handlerKeyedHook);
|
|
66
66
|
|
|
67
67
|
// Entity-keyed: fires for ALL query-handlers of widget-entity
|
|
68
|
-
r.
|
|
68
|
+
r.hook("postQuery", { allOf: widget }, entityKeyedHook);
|
|
69
69
|
});
|
|
70
70
|
|
|
71
71
|
// --- Single-object-result invariant fixtures ---
|
|
@@ -93,13 +93,13 @@ const singleObjectFeature = defineFeature("singleobjtest", (r) => {
|
|
|
93
93
|
r.queryHandler("gadget:get", z.object({}), async () => ({ id: "g1", name: "Gadget" }), {
|
|
94
94
|
access: { openToAll: true },
|
|
95
95
|
});
|
|
96
|
-
r.
|
|
96
|
+
r.hook("postQuery", { allOf: gadget }, dropRowHook);
|
|
97
97
|
|
|
98
98
|
const gizmo = r.entity("gizmo", gizmoEntity);
|
|
99
99
|
r.queryHandler("gizmo:get", z.object({}), async () => ({ id: "z1", name: "Gizmo" }), {
|
|
100
100
|
access: { openToAll: true },
|
|
101
101
|
});
|
|
102
|
-
r.
|
|
102
|
+
r.hook("postQuery", { allOf: gizmo }, duplicateRowHook);
|
|
103
103
|
});
|
|
104
104
|
|
|
105
105
|
// --- Test stack ---
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
defineFeature,
|
|
27
27
|
} from "../../engine";
|
|
28
28
|
import type { ProjectionDefinition } from "../../engine/types";
|
|
29
|
-
import { createEventsTable } from "../../event-store";
|
|
29
|
+
import { archiveStream, createEventsTable } from "../../event-store";
|
|
30
30
|
import {
|
|
31
31
|
createProjectionStateTable,
|
|
32
32
|
getAllProjectionProgress,
|
|
@@ -116,7 +116,7 @@ afterAll(async () => {
|
|
|
116
116
|
|
|
117
117
|
beforeEach(async () => {
|
|
118
118
|
await asRawClient(testDb.db).unsafe(
|
|
119
|
-
`TRUNCATE kumiko_events, read_rebuild_items, read_rebuild_items_per_group, kumiko_projections RESTART IDENTITY CASCADE`,
|
|
119
|
+
`TRUNCATE kumiko_events, read_rebuild_items, read_rebuild_items_per_group, kumiko_projections, kumiko_archived_streams RESTART IDENTITY CASCADE`,
|
|
120
120
|
);
|
|
121
121
|
});
|
|
122
122
|
|
|
@@ -210,6 +210,36 @@ describe("rebuildProjection — happy path", () => {
|
|
|
210
210
|
expect(result.eventsProcessed).toBe(0);
|
|
211
211
|
expect(result.lastProcessedEventId).toBe(0n);
|
|
212
212
|
});
|
|
213
|
+
|
|
214
|
+
test("archived streams don't replay (fw#832) — stranded aggregate heals via archiveStream", async () => {
|
|
215
|
+
// Heilungs-Muster fuer eventlose Read-Side-Deletes im Bestand: ein
|
|
216
|
+
// Aggregat, dessen Live-Row (vor dem fw#832-Fix) read-side entfernt
|
|
217
|
+
// wurde, hinterlaesst ein created-Event ohne deleted — jeder Rebuild
|
|
218
|
+
// wuerde die Row resurrecten (bzw. an unique-Indexen kollidieren).
|
|
219
|
+
// archiveStream friert den Stream ein UND nimmt ihn aus dem Replay.
|
|
220
|
+
const group = "00000000-0000-4000-8000-000000000020";
|
|
221
|
+
const stranded = await executor.create({ groupId: group, name: "stranded" }, admin, tdb);
|
|
222
|
+
if (!stranded.isSuccess) throw new Error("create failed");
|
|
223
|
+
await appendCreatedEvent(group, "survivor");
|
|
224
|
+
|
|
225
|
+
await archiveStream(testDb.db, {
|
|
226
|
+
tenantId: admin.tenantId,
|
|
227
|
+
aggregateId: String(stranded.data.id),
|
|
228
|
+
aggregateType: "rebuild-item",
|
|
229
|
+
archivedBy: "fw832-heal-test",
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
const result = await rebuildProjection(qualifiedProjectionName, {
|
|
233
|
+
db: testDb.db,
|
|
234
|
+
registry,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Nur das survivor-Event replayed; das archivierte Aggregat traegt
|
|
238
|
+
// nicht mehr bei — auch nicht im #443-ground-truth-count (sonst
|
|
239
|
+
// wuerde der Recompute-Pfad endlos nachziehen).
|
|
240
|
+
expect(result.eventsProcessed).toBe(1);
|
|
241
|
+
expect(await getCount(group)).toBe(1);
|
|
242
|
+
});
|
|
213
243
|
});
|
|
214
244
|
|
|
215
245
|
describe("rebuildProjection — state table lifecycle", () => {
|
|
@@ -860,3 +890,51 @@ describe("rebuildProjection — live-tail catch-up (#363 Phase 2)", () => {
|
|
|
860
890
|
await asRawClient(testDb.db).unsafe(`DROP TABLE IF EXISTS "cutover_probe"`);
|
|
861
891
|
});
|
|
862
892
|
});
|
|
893
|
+
|
|
894
|
+
describe("rebuildProjection — stale-registry column fence (#835)", () => {
|
|
895
|
+
// Simulates the rolling-deploy race: a migration added a column to the live
|
|
896
|
+
// table, but the pod running the rebuild still carries the old registry
|
|
897
|
+
// (EntityTableMeta without that column). The rebuild must abort instead of
|
|
898
|
+
// swapping in a shadow that silently drops the migrated column (#494 class).
|
|
899
|
+
test("aborts when the live table has a column the registry meta lacks — data survives", async () => {
|
|
900
|
+
const group = "00000000-0000-4000-8000-000000000041";
|
|
901
|
+
await appendCreatedEvent(group, "item1");
|
|
902
|
+
await rebuildProjection(qualifiedProjectionName, { db: testDb.db, registry });
|
|
903
|
+
expect(await getCount(group)).toBe(1);
|
|
904
|
+
|
|
905
|
+
const raw = asRawClient(testDb.db);
|
|
906
|
+
await raw.unsafe(`ALTER TABLE "read_rebuild_items_per_group" ADD COLUMN migrated_col text`);
|
|
907
|
+
try {
|
|
908
|
+
await raw.unsafe(
|
|
909
|
+
`UPDATE "read_rebuild_items_per_group" SET migrated_col = 'must-survive' WHERE group_id = $1::uuid`,
|
|
910
|
+
[group],
|
|
911
|
+
);
|
|
912
|
+
|
|
913
|
+
await expect(
|
|
914
|
+
rebuildProjection(qualifiedProjectionName, { db: testDb.db, registry }),
|
|
915
|
+
).rejects.toThrow(/live-only: migrated_col/);
|
|
916
|
+
|
|
917
|
+
// Live table untouched: column AND its data still there.
|
|
918
|
+
const [row] = await raw.unsafe<{ migrated_col: string | null }>(
|
|
919
|
+
`SELECT migrated_col FROM "read_rebuild_items_per_group" WHERE group_id = $1::uuid`,
|
|
920
|
+
[group],
|
|
921
|
+
);
|
|
922
|
+
expect(row?.migrated_col).toBe("must-survive");
|
|
923
|
+
expect(await getCount(group)).toBe(1);
|
|
924
|
+
|
|
925
|
+
// Failure is visible to ops.
|
|
926
|
+
const state = await getProjectionState(testDb.db, qualifiedProjectionName);
|
|
927
|
+
expect(state?.status).toBe("failed");
|
|
928
|
+
expect(state?.lastError).toContain("migrated_col");
|
|
929
|
+
} finally {
|
|
930
|
+
await raw.unsafe(
|
|
931
|
+
`ALTER TABLE "read_rebuild_items_per_group" DROP COLUMN IF EXISTS migrated_col`,
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
// Column sets aligned again → rebuild works.
|
|
936
|
+
const result = await rebuildProjection(qualifiedProjectionName, { db: testDb.db, registry });
|
|
937
|
+
expect(result.eventsProcessed).toBe(1);
|
|
938
|
+
expect(await getCount(group)).toBe(1);
|
|
939
|
+
});
|
|
940
|
+
});
|