@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
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// 5. Snapshot erneut nehmen
|
|
16
16
|
// 6. deep-equal: identische Rows in identischer Reihenfolge
|
|
17
17
|
|
|
18
|
-
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
18
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
19
19
|
import { type BunTestDb, createTestDb } from "../../bun-db/__tests__/bun-test-db";
|
|
20
20
|
import { createBooleanField, createEntity, createTextField, defineFeature } from "../../engine";
|
|
21
21
|
import { createRegistry } from "../../engine/registry";
|
|
@@ -222,26 +222,33 @@ describe("implicit-projection / Live==Rebuild equivalence", () => {
|
|
|
222
222
|
});
|
|
223
223
|
});
|
|
224
224
|
|
|
225
|
-
// Sensitive-
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
225
|
+
// Sensitive-Rebuild-Parität (#967): das Event-Log trägt für sensitive-Felder
|
|
226
|
+
// den Tabellen-Ciphertext (boot-validiert pii/encrypted) — Live==Rebuild gilt
|
|
227
|
+
// damit auch für sensitive Spalten + Blind-Index. Einzige legitime Divergenz
|
|
228
|
+
// bleibt Crypto-Shredding: DEK erased → bidx NULL, Wert unlesbar.
|
|
229
|
+
|
|
230
|
+
import {
|
|
231
|
+
computeBlindIndex,
|
|
232
|
+
configureBlindIndexKey,
|
|
233
|
+
configurePiiSubjectKms,
|
|
234
|
+
decodeBlindIndexKey,
|
|
235
|
+
decryptPiiFieldValues,
|
|
236
|
+
InMemoryKmsAdapter,
|
|
237
|
+
isPiiCiphertext,
|
|
238
|
+
resetBlindIndexKeyForTests,
|
|
239
|
+
resetPiiSubjectKmsForTests,
|
|
240
|
+
} from "../../crypto";
|
|
236
241
|
import { asRawClient, selectMany } from "../../db/query";
|
|
237
242
|
|
|
238
243
|
const sensitiveTable = "read_implicit_sensitive_users";
|
|
244
|
+
const SENSITIVE_BIDX_KEY_B64 = Buffer.alloc(32, 9).toString("base64");
|
|
245
|
+
const SENSITIVE_BIDX_KEY = decodeBlindIndexKey(SENSITIVE_BIDX_KEY_B64);
|
|
239
246
|
|
|
240
247
|
const sensitiveEntity = createEntity({
|
|
241
248
|
table: sensitiveTable,
|
|
242
249
|
fields: {
|
|
243
250
|
email: createTextField({ required: true }),
|
|
244
|
-
apiKey: createTextField({ sensitive: true }),
|
|
251
|
+
apiKey: createTextField({ sensitive: true, pii: true, lookupable: true }),
|
|
245
252
|
},
|
|
246
253
|
});
|
|
247
254
|
|
|
@@ -250,8 +257,11 @@ const sensitiveFeature = defineFeature("implicitsensitive", (r) => {
|
|
|
250
257
|
});
|
|
251
258
|
|
|
252
259
|
const sensitiveEntityTable = buildEntityTable("sensitive-user", sensitiveEntity);
|
|
260
|
+
const sensitiveProjection = "implicitsensitive:projection:sensitive-user-entity";
|
|
261
|
+
|
|
262
|
+
describe("implicit-projection / sensitive Rebuild-Parität (#967)", () => {
|
|
263
|
+
let kms: InMemoryKmsAdapter;
|
|
253
264
|
|
|
254
|
-
describe("implicit-projection / dokumentierte Sensitive-Drift", () => {
|
|
255
265
|
beforeAll(async () => {
|
|
256
266
|
await unsafeCreateEntityTable(testDb.db, sensitiveEntity, "sensitive-user");
|
|
257
267
|
});
|
|
@@ -260,29 +270,46 @@ describe("implicit-projection / dokumentierte Sensitive-Drift", () => {
|
|
|
260
270
|
await asRawClient(testDb.db).unsafe(
|
|
261
271
|
`TRUNCATE ${sensitiveTable}, kumiko_events, kumiko_projections RESTART IDENTITY CASCADE`,
|
|
262
272
|
);
|
|
273
|
+
kms = new InMemoryKmsAdapter();
|
|
274
|
+
configurePiiSubjectKms(kms);
|
|
275
|
+
configureBlindIndexKey(SENSITIVE_BIDX_KEY_B64);
|
|
263
276
|
});
|
|
264
277
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
278
|
+
afterEach(() => {
|
|
279
|
+
resetPiiSubjectKmsForTests();
|
|
280
|
+
resetBlindIndexKeyForTests();
|
|
281
|
+
});
|
|
269
282
|
|
|
270
|
-
|
|
271
|
-
|
|
283
|
+
const crud = createEventStoreExecutor(sensitiveEntityTable, sensitiveEntity, {
|
|
284
|
+
entityName: "sensitive-user",
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
async function rawSensitiveRow(id: string): Promise<Record<string, unknown>> {
|
|
288
|
+
const rows = await asRawClient(testDb.db).unsafe<Record<string, unknown>>(
|
|
289
|
+
`SELECT * FROM ${sensitiveTable} WHERE id = $1`,
|
|
290
|
+
[id],
|
|
291
|
+
);
|
|
292
|
+
const row = rows[0];
|
|
293
|
+
if (!row) throw new Error(`no row for ${id}`);
|
|
294
|
+
return row;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
test("Live==Rebuild inkl. sensitive-Ciphertext + bidx (byte-gleiche Kopie aus dem Event)", async () => {
|
|
272
298
|
const created = await crud.create(
|
|
273
299
|
{ email: "x@test.de", apiKey: "secret-token-abc" },
|
|
274
300
|
adminUser,
|
|
275
301
|
tdb,
|
|
276
302
|
);
|
|
277
303
|
if (!created.isSuccess) throw new Error("setup failed");
|
|
304
|
+
const id = String(created.data.id);
|
|
278
305
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
expect(
|
|
283
|
-
expect(liveRow
|
|
306
|
+
// Live-Row: Ciphertext + bidx, nie Klartext.
|
|
307
|
+
const liveRow = await rawSensitiveRow(id);
|
|
308
|
+
const liveCipher = liveRow["api_key"];
|
|
309
|
+
expect(isPiiCiphertext(liveCipher)).toBe(true);
|
|
310
|
+
expect(liveRow["api_key_bidx"]).toBe(computeBlindIndex(SENSITIVE_BIDX_KEY, "secret-token-abc"));
|
|
284
311
|
|
|
285
|
-
//
|
|
312
|
+
// Event-Payload trägt exakt den Tabellen-Ciphertext (byte-gleich).
|
|
286
313
|
const { eventsTable } = await import("../../event-store");
|
|
287
314
|
const [event] = await selectMany(
|
|
288
315
|
testDb.db,
|
|
@@ -290,23 +317,75 @@ describe("implicit-projection / dokumentierte Sensitive-Drift", () => {
|
|
|
290
317
|
{ aggregateId: created.data.id },
|
|
291
318
|
{ orderBy: { col: "version", direction: "asc" } },
|
|
292
319
|
);
|
|
293
|
-
expect(event?.payload?.["apiKey"]).
|
|
320
|
+
expect(event?.payload?.["apiKey"]).toBe(liveCipher);
|
|
294
321
|
expect(event?.payload?.["email"]).toBe("x@test.de");
|
|
322
|
+
expect(JSON.stringify(event?.payload)).not.toContain("secret-token-abc");
|
|
295
323
|
|
|
296
|
-
// 3. Rebuild über die ImplicitProjection. Read-Tabelle wird aus
|
|
297
|
-
// event.payload neu materialisiert — apiKey ist nicht im Log,
|
|
298
|
-
// landet also als NULL/undefined in der rebuilt Row.
|
|
299
324
|
const registry = createRegistry([sensitiveFeature]);
|
|
300
|
-
await rebuildProjection(
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
325
|
+
await rebuildProjection(sensitiveProjection, { db: testDb.db, registry });
|
|
326
|
+
|
|
327
|
+
const rebuiltRow = await rawSensitiveRow(id);
|
|
328
|
+
expect(rebuiltRow["email"]).toBe("x@test.de");
|
|
329
|
+
expect(rebuiltRow["api_key"]).toBe(liveCipher);
|
|
330
|
+
expect(rebuiltRow["api_key_bidx"]).toBe(liveRow["api_key_bidx"]);
|
|
331
|
+
});
|
|
304
332
|
|
|
305
|
-
|
|
306
|
-
|
|
333
|
+
test("DEK-Stabilität: Update re-encryptet mit demselben Subject-DEK — alter Event-Ciphertext bleibt lesbar", async () => {
|
|
334
|
+
const created = await crud.create(
|
|
335
|
+
{ email: "y@test.de", apiKey: "first-secret" },
|
|
336
|
+
adminUser,
|
|
337
|
+
tdb,
|
|
338
|
+
);
|
|
339
|
+
if (!created.isSuccess) throw new Error("setup failed");
|
|
340
|
+
const updated = await crud.update(
|
|
341
|
+
{ id: created.data.id, version: 1, changes: { apiKey: "second-secret" } },
|
|
342
|
+
adminUser,
|
|
343
|
+
tdb,
|
|
344
|
+
);
|
|
345
|
+
if (!updated.isSuccess) throw new Error("update failed");
|
|
346
|
+
|
|
347
|
+
// Created-Event (v1-Ciphertext) muss mit dem aktuellen DEK lesbar bleiben —
|
|
348
|
+
// eine DEK-Rotation beim Update würde das immutable Log unlesbar machen.
|
|
349
|
+
const { eventsTable } = await import("../../event-store");
|
|
350
|
+
const [createdEvent] = await selectMany(
|
|
351
|
+
testDb.db,
|
|
352
|
+
eventsTable,
|
|
353
|
+
{ aggregateId: created.data.id },
|
|
354
|
+
{ orderBy: { col: "version", direction: "asc" } },
|
|
355
|
+
);
|
|
356
|
+
const v1Cipher = createdEvent?.payload?.["apiKey"];
|
|
357
|
+
expect(isPiiCiphertext(v1Cipher)).toBe(true);
|
|
358
|
+
const decrypted = await decryptPiiFieldValues({ apiKey: v1Cipher }, ["apiKey"], kms, {
|
|
359
|
+
requestId: "test",
|
|
307
360
|
});
|
|
308
|
-
expect(
|
|
309
|
-
|
|
310
|
-
|
|
361
|
+
expect(decrypted["apiKey"]).toBe("first-secret");
|
|
362
|
+
|
|
363
|
+
// Rebuild spielt created+updated: bidx-Recompute entschlüsselt beide
|
|
364
|
+
// Ciphertexte — schlägt bei rotiertem DEK fehl statt still zu heilen.
|
|
365
|
+
const registry = createRegistry([sensitiveFeature]);
|
|
366
|
+
await rebuildProjection(sensitiveProjection, { db: testDb.db, registry });
|
|
367
|
+
const rebuiltRow = await rawSensitiveRow(String(created.data.id));
|
|
368
|
+
expect(rebuiltRow["api_key_bidx"]).toBe(computeBlindIndex(SENSITIVE_BIDX_KEY, "second-secret"));
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("Erase-Divergenz bleibt die einzige legitime: DEK erased → Rebuild bidx NULL, Wert unlesbar", async () => {
|
|
372
|
+
const created = await crud.create(
|
|
373
|
+
{ email: "z@test.de", apiKey: "gone-after-forget" },
|
|
374
|
+
adminUser,
|
|
375
|
+
tdb,
|
|
376
|
+
);
|
|
377
|
+
if (!created.isSuccess) throw new Error("setup failed");
|
|
378
|
+
const id = String(created.data.id);
|
|
379
|
+
|
|
380
|
+
await kms.eraseKey({ kind: "user", userId: id });
|
|
381
|
+
|
|
382
|
+
const registry = createRegistry([sensitiveFeature]);
|
|
383
|
+
await rebuildProjection(sensitiveProjection, { db: testDb.db, registry });
|
|
384
|
+
|
|
385
|
+
const rebuiltRow = await rawSensitiveRow(id);
|
|
386
|
+
expect(rebuiltRow["api_key_bidx"]).toBeNull();
|
|
387
|
+
// Ciphertext-Bytes werden kopiert, sind aber ohne DEK unlesbar.
|
|
388
|
+
expect(isPiiCiphertext(rebuiltRow["api_key"])).toBe(true);
|
|
389
|
+
expect(rebuiltRow["api_key"]).not.toBe("gone-after-forget");
|
|
311
390
|
});
|
|
312
391
|
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// kumiko-framework#1480 twin bug: instantToDriver referenced the global
|
|
2
|
+
// `Temporal` the same way instantFromDriver in bun-db/query.ts did. Fixed
|
|
3
|
+
// the same way — static `import { Temporal } from "temporal-polyfill"` in
|
|
4
|
+
// dialect.ts. This test deletes globalThis.Temporal before calling it,
|
|
5
|
+
// proving the write path no longer depends on the global either.
|
|
6
|
+
|
|
7
|
+
import { describe, expect, test } from "bun:test";
|
|
8
|
+
import { instantToDriver } from "../dialect";
|
|
9
|
+
|
|
10
|
+
describe("instantToDriver — without a global Temporal", () => {
|
|
11
|
+
test("coerces an ISO string without relying on a global Temporal", () => {
|
|
12
|
+
const savedGlobal = (globalThis as { Temporal?: unknown }).Temporal;
|
|
13
|
+
delete (globalThis as { Temporal?: unknown }).Temporal;
|
|
14
|
+
try {
|
|
15
|
+
expect(instantToDriver("2026-04-18T10:00:00Z")).toBe("2026-04-18T10:00:00Z");
|
|
16
|
+
} finally {
|
|
17
|
+
if (savedGlobal === undefined) delete (globalThis as { Temporal?: unknown }).Temporal;
|
|
18
|
+
else (globalThis as { Temporal?: unknown }).Temporal = savedGlobal;
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -103,6 +103,25 @@ describe("flattenLocatedTimestamp — Insert/Update Convert", () => {
|
|
|
103
103
|
});
|
|
104
104
|
});
|
|
105
105
|
|
|
106
|
+
describe("rehydrateLocatedTimestamp — kumiko-framework#1490: no ambient Temporal global", () => {
|
|
107
|
+
test("rehydrates without relying on globalThis.Temporal", () => {
|
|
108
|
+
const savedGlobal = (globalThis as { Temporal?: unknown }).Temporal;
|
|
109
|
+
delete (globalThis as { Temporal?: unknown }).Temporal;
|
|
110
|
+
try {
|
|
111
|
+
const out = rehydrateLocatedTimestamp(
|
|
112
|
+
{ pickupUtc: "2026-04-15T09:00:00Z", pickupTz: "Europe/Lisbon" },
|
|
113
|
+
orderEntity,
|
|
114
|
+
);
|
|
115
|
+
expect(out).toEqual({
|
|
116
|
+
pickup: { at: "2026-04-15T10:00:00", tz: "Europe/Lisbon", utc: "2026-04-15T09:00:00Z" },
|
|
117
|
+
});
|
|
118
|
+
} finally {
|
|
119
|
+
if (savedGlobal === undefined) delete (globalThis as { Temporal?: unknown }).Temporal;
|
|
120
|
+
else (globalThis as { Temporal?: unknown }).Temporal = savedGlobal;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
106
125
|
describe("rehydrateLocatedTimestamp — Read Convert", () => {
|
|
107
126
|
test("{ <name>Utc, <name>Tz } DB-Form → { at, tz, utc } API-Form (Pickup-Ort-lokal)", () => {
|
|
108
127
|
const out = rehydrateLocatedTimestamp(
|
|
@@ -95,6 +95,10 @@ describe("renderMigrationSql — managed recreate vs unmanaged in-place", () =>
|
|
|
95
95
|
expect(sql).toContain('DROP TABLE IF EXISTS "read_secrets";');
|
|
96
96
|
expect(sql).toContain('CREATE TABLE IF NOT EXISTS "read_secrets"');
|
|
97
97
|
expect(sql).not.toContain("ADD COLUMN");
|
|
98
|
+
expect(sql).toContain(
|
|
99
|
+
"-- WARN: destructive change (new NOT NULL column(s) without default: envelope) forces DROP+CREATE + full event replay.",
|
|
100
|
+
);
|
|
101
|
+
expect(sql).toContain("Consider an Expand/Contract split across two releases");
|
|
98
102
|
});
|
|
99
103
|
|
|
100
104
|
test("managed: column rename (drop + add NOT NULL) → DROP+CREATE with new shape", () => {
|
|
@@ -121,6 +125,7 @@ describe("renderMigrationSql — managed recreate vs unmanaged in-place", () =>
|
|
|
121
125
|
const sql = renderMigrationSql(diffSnapshots(prev, next), { name: "note", sequenceNumber: 4 });
|
|
122
126
|
expect(sql).toContain('ALTER TABLE "read_a" ADD COLUMN "note"');
|
|
123
127
|
expect(sql).not.toContain("DROP TABLE");
|
|
128
|
+
expect(sql).not.toContain("WARN: destructive change");
|
|
124
129
|
});
|
|
125
130
|
|
|
126
131
|
test("unmanaged: NOT NULL column without default → in-place ADD (real data, never recreated)", () => {
|
|
@@ -134,5 +139,21 @@ describe("renderMigrationSql — managed recreate vs unmanaged in-place", () =>
|
|
|
134
139
|
});
|
|
135
140
|
expect(sql).toContain('ALTER TABLE "app_data" ADD COLUMN "envelope"');
|
|
136
141
|
expect(sql).not.toContain("DROP TABLE");
|
|
142
|
+
expect(sql).not.toContain("WARN: destructive change");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("managed: multiple recreate reasons at once → all named in the warning", () => {
|
|
146
|
+
const prev = snapshotFromMetas([
|
|
147
|
+
meta("read_b", { name: "old_col", pgType: "text", notNull: false }, "managed"),
|
|
148
|
+
]);
|
|
149
|
+
const next = snapshotFromMetas([
|
|
150
|
+
meta("read_b", { name: "envelope", pgType: "jsonb", notNull: true }, "managed"),
|
|
151
|
+
]);
|
|
152
|
+
const sql = renderMigrationSql(diffSnapshots(prev, next), {
|
|
153
|
+
name: "multi",
|
|
154
|
+
sequenceNumber: 6,
|
|
155
|
+
});
|
|
156
|
+
expect(sql).toContain("dropped column(s): old_col");
|
|
157
|
+
expect(sql).toContain("new NOT NULL column(s) without default: envelope");
|
|
137
158
|
});
|
|
138
159
|
});
|
|
@@ -16,4 +16,65 @@ describe("splitSqlStatements", () => {
|
|
|
16
16
|
test("filters empty segments", () => {
|
|
17
17
|
expect(splitSqlStatements("-- only comments\n; ;")).toEqual([]);
|
|
18
18
|
});
|
|
19
|
+
|
|
20
|
+
test("does not split mid-statement on a semicolon inside a line comment", () => {
|
|
21
|
+
const sql = `
|
|
22
|
+
-- sets default on the entity; the create/update handlers fill it
|
|
23
|
+
CREATE TABLE "a" ("id" uuid);
|
|
24
|
+
`;
|
|
25
|
+
expect(splitSqlStatements(sql)).toEqual(['CREATE TABLE "a" ("id" uuid);']);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("does not split on a semicolon inside a block comment", () => {
|
|
29
|
+
const sql = `
|
|
30
|
+
/* multi
|
|
31
|
+
line; with semi */
|
|
32
|
+
CREATE TABLE "a" ("id" uuid);
|
|
33
|
+
`;
|
|
34
|
+
expect(splitSqlStatements(sql)).toEqual(['CREATE TABLE "a" ("id" uuid);']);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("a block-comment opener inside a line comment does not swallow the next statement", () => {
|
|
38
|
+
const sql = `
|
|
39
|
+
-- note: see /* details below
|
|
40
|
+
CREATE TABLE "a" ("id" uuid);
|
|
41
|
+
/* real block comment */
|
|
42
|
+
CREATE TABLE "b" ("id" uuid);
|
|
43
|
+
`;
|
|
44
|
+
expect(splitSqlStatements(sql)).toEqual([
|
|
45
|
+
'CREATE TABLE "a" ("id" uuid);',
|
|
46
|
+
'CREATE TABLE "b" ("id" uuid);',
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("does not split on a semicolon inside a single-quoted string literal", () => {
|
|
51
|
+
const sql = `INSERT INTO "a" ("v") VALUES ('a;b');`;
|
|
52
|
+
expect(splitSqlStatements(sql)).toEqual([`INSERT INTO "a" ("v") VALUES ('a;b');`]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("does not treat a double-dash inside a string literal as a comment", () => {
|
|
56
|
+
const sql = `INSERT INTO "a" ("v") VALUES ('a--b');`;
|
|
57
|
+
expect(splitSqlStatements(sql)).toEqual([`INSERT INTO "a" ("v") VALUES ('a--b');`]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("handles an escaped quote inside a single-quoted string literal ('')", () => {
|
|
61
|
+
const sql = `INSERT INTO "a" ("v") VALUES ('a'';b');`;
|
|
62
|
+
expect(splitSqlStatements(sql)).toEqual([`INSERT INTO "a" ("v") VALUES ('a'';b');`]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("does not split on a semicolon inside a double-quoted identifier", () => {
|
|
66
|
+
const sql = `CREATE TABLE "weird;name" ("id" uuid);`;
|
|
67
|
+
expect(splitSqlStatements(sql)).toEqual([`CREATE TABLE "weird;name" ("id" uuid);`]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("throws fail-loud on an unterminated block comment instead of silently dropping statements", () => {
|
|
71
|
+
const sql = `/* oops\nCREATE TABLE "a" ("id" uuid);`;
|
|
72
|
+
expect(() => splitSqlStatements(sql)).toThrow();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("a trailing line comment without a newline terminates cleanly", () => {
|
|
76
|
+
expect(splitSqlStatements('CREATE TABLE "a" ("id" uuid);\n-- done')).toEqual([
|
|
77
|
+
'CREATE TABLE "a" ("id" uuid);',
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
19
80
|
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Prod-relevant bug: NumberFieldDef mapped unconditionally to a Postgres
|
|
2
|
+
// `integer` column regardless of the `integer` flag. `createNumberField()`
|
|
3
|
+
// (no `integer: true`) accepts fractional values at the Zod boundary but
|
|
4
|
+
// the column rejected them at the DB with "invalid input syntax for type
|
|
5
|
+
// integer" — Monte-Carlo-style stats fields (phronexsis simulation results)
|
|
6
|
+
// hit this on every non-integer result. Fix: `integer: true` → `integer`
|
|
7
|
+
// column, otherwise → `double precision` (fractional values round-trip).
|
|
8
|
+
|
|
9
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
10
|
+
import { seedRow } from "@cosmicdrift/kumiko-framework/testing";
|
|
11
|
+
import { selectMany } from "../../db/query";
|
|
12
|
+
import { buildEntityTable } from "../../db/table-builder";
|
|
13
|
+
import { createEntity, createNumberField } from "../../engine";
|
|
14
|
+
import { setupTestStack, type TestStack, unsafeCreateEntityTable } from "../../stack";
|
|
15
|
+
|
|
16
|
+
const statsEntity = createEntity({
|
|
17
|
+
table: "nff_stats",
|
|
18
|
+
fields: {
|
|
19
|
+
expectedValue: createNumberField({ sortable: true }),
|
|
20
|
+
wholeCount: createNumberField({ integer: true }),
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
const statsTable = buildEntityTable("nff-stats", statsEntity);
|
|
24
|
+
|
|
25
|
+
let stack: TestStack;
|
|
26
|
+
|
|
27
|
+
beforeAll(async () => {
|
|
28
|
+
stack = await setupTestStack({ features: [] });
|
|
29
|
+
await unsafeCreateEntityTable(stack.db, statsEntity, "nff-stats");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterAll(async () => stack?.cleanup());
|
|
33
|
+
|
|
34
|
+
describe("createNumberField() without integer:true — fractional values round-trip", () => {
|
|
35
|
+
test("INSERT accepts a non-integer value and SELECT returns it unchanged", async () => {
|
|
36
|
+
await seedRow(stack.db, statsTable, {
|
|
37
|
+
expectedValue: 77.02269129478736,
|
|
38
|
+
wholeCount: 3,
|
|
39
|
+
tenantId: "00000000-0000-4000-8000-000000000001",
|
|
40
|
+
});
|
|
41
|
+
const rows = await selectMany(stack.db, statsTable);
|
|
42
|
+
expect(rows).toHaveLength(1);
|
|
43
|
+
expect(rows[0]?.["expectedValue"]).toBe(77.02269129478736);
|
|
44
|
+
expect(rows[0]?.["wholeCount"]).toBe(3);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("createNumberField({ integer: true }) — still rejects fractional values at the DB", () => {
|
|
49
|
+
test("INSERT with a non-integer value into an integer-flagged field throws", async () => {
|
|
50
|
+
await expect(
|
|
51
|
+
seedRow(stack.db, statsTable, {
|
|
52
|
+
expectedValue: 1,
|
|
53
|
+
wholeCount: 3.5,
|
|
54
|
+
tenantId: "00000000-0000-4000-8000-000000000001",
|
|
55
|
+
}),
|
|
56
|
+
).rejects.toThrow(/invalid input syntax for type integer/);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -28,14 +28,16 @@ function tmpDir(): string {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
describe("rebuildTablesFromDiff", () => {
|
|
31
|
-
test("managed: new
|
|
31
|
+
test("managed: new table rebuilt, additive-only change NOT rebuilt, dropped excluded", () => {
|
|
32
|
+
// read_a: nur eine nullable Spalte dazu → in-place ALTER, kein Rebuild.
|
|
33
|
+
// read_b: neue Tabelle → Rebuild. read_c gedroppt → nie im Marker.
|
|
32
34
|
const prev = snapshotFromMetas([meta("read_a"), meta("read_c")]);
|
|
33
35
|
const next = snapshotFromMetas([
|
|
34
36
|
meta("read_a", { name: "title", pgType: "text", notNull: false }),
|
|
35
37
|
meta("read_b"),
|
|
36
38
|
]);
|
|
37
39
|
const diff = diffSnapshots(prev, next);
|
|
38
|
-
expect(rebuildTablesFromDiff(diff)).toEqual(["
|
|
40
|
+
expect(rebuildTablesFromDiff(diff)).toEqual(["read_b"]);
|
|
39
41
|
});
|
|
40
42
|
|
|
41
43
|
test("no schema change → empty", () => {
|
|
@@ -51,11 +53,19 @@ describe("rebuildTablesFromDiff", () => {
|
|
|
51
53
|
expect(rebuildTablesFromDiff(diffSnapshots(prev, next))).toEqual([]);
|
|
52
54
|
});
|
|
53
55
|
|
|
54
|
-
test("managed new
|
|
56
|
+
test("managed new NULLABLE column → NO rebuild (in-place ADD COLUMN reicht; Rebuild würde die Spalte wischen, #835)", () => {
|
|
55
57
|
const prev = snapshotFromMetas([meta("read_a")]);
|
|
56
58
|
const next = snapshotFromMetas([
|
|
57
59
|
meta("read_a", { name: "title", pgType: "text", notNull: false }),
|
|
58
60
|
]);
|
|
61
|
+
expect(rebuildTablesFromDiff(diffSnapshots(prev, next))).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("managed new NOT-NULL column ohne Default → rebuild (recreate, kann nicht in-place)", () => {
|
|
65
|
+
const prev = snapshotFromMetas([meta("read_a")]);
|
|
66
|
+
const next = snapshotFromMetas([
|
|
67
|
+
meta("read_a", { name: "title", pgType: "text", notNull: true }),
|
|
68
|
+
]);
|
|
59
69
|
expect(rebuildTablesFromDiff(diffSnapshots(prev, next))).toEqual(["read_a"]);
|
|
60
70
|
});
|
|
61
71
|
|