@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
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { EntityTableMeta } from "../entity-table-meta";
|
|
6
|
+
import { snapshotFromMetas } from "../migrate-generator";
|
|
7
|
+
import { diffReplayAgainstSnapshot, replayMigrationsDir } from "../replay-migration-sql";
|
|
8
|
+
|
|
9
|
+
function tmpMigrationsDir(): string {
|
|
10
|
+
return mkdtempSync(join(tmpdir(), "replay-migration-sql-"));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function write(dir: string, filename: string, sql: string): void {
|
|
14
|
+
writeFileSync(join(dir, filename), sql);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function meta(tableName: string, columns: EntityTableMeta["columns"]): EntityTableMeta {
|
|
18
|
+
return { tableName, source: "managed", indexes: [], columns };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("replayMigrationsDir", () => {
|
|
22
|
+
test("reconstructs table+column shape from CREATE TABLE", () => {
|
|
23
|
+
const dir = tmpMigrationsDir();
|
|
24
|
+
try {
|
|
25
|
+
write(
|
|
26
|
+
dir,
|
|
27
|
+
"0001_init.sql",
|
|
28
|
+
`CREATE TABLE IF NOT EXISTS "read_accounts" (
|
|
29
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
30
|
+
"tenant_id" uuid NOT NULL,
|
|
31
|
+
"stripe_customer_id" text
|
|
32
|
+
);
|
|
33
|
+
CREATE INDEX IF NOT EXISTS "read_accounts_tenant_id_idx" ON "read_accounts" ("tenant_id");`,
|
|
34
|
+
);
|
|
35
|
+
const replayed = replayMigrationsDir(dir);
|
|
36
|
+
expect([...replayed.keys()]).toEqual(["read_accounts"]);
|
|
37
|
+
expect([...(replayed.get("read_accounts")?.columns ?? [])].sort()).toEqual([
|
|
38
|
+
"id",
|
|
39
|
+
"stripe_customer_id",
|
|
40
|
+
"tenant_id",
|
|
41
|
+
]);
|
|
42
|
+
} finally {
|
|
43
|
+
rmSync(dir, { recursive: true, force: true });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("ADD COLUMN extends an already-created table across files", () => {
|
|
48
|
+
const dir = tmpMigrationsDir();
|
|
49
|
+
try {
|
|
50
|
+
write(dir, "0001_init.sql", `CREATE TABLE IF NOT EXISTS "read_a" ("id" uuid PRIMARY KEY);`);
|
|
51
|
+
write(dir, "0002_add-col.sql", `ALTER TABLE "read_a" ADD COLUMN "title" text;`);
|
|
52
|
+
const replayed = replayMigrationsDir(dir);
|
|
53
|
+
expect([...(replayed.get("read_a")?.columns ?? [])].sort()).toEqual(["id", "title"]);
|
|
54
|
+
} finally {
|
|
55
|
+
rmSync(dir, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Hand-edited migrations legitimately add "IF NOT EXISTS"/"IF EXISTS" to
|
|
60
|
+
// ADD/DROP COLUMN (the generator itself never emits it, but app authors
|
|
61
|
+
// are explicitly allowed to hand-edit before committing — see the header
|
|
62
|
+
// comment every generated migration carries).
|
|
63
|
+
test("ADD COLUMN IF NOT EXISTS (hand-edited) still extends the table", () => {
|
|
64
|
+
const dir = tmpMigrationsDir();
|
|
65
|
+
try {
|
|
66
|
+
write(dir, "0001_init.sql", `CREATE TABLE IF NOT EXISTS "read_a" ("id" uuid PRIMARY KEY);`);
|
|
67
|
+
write(dir, "0002_add-col.sql", `ALTER TABLE "read_a" ADD COLUMN IF NOT EXISTS "title" text;`);
|
|
68
|
+
const replayed = replayMigrationsDir(dir);
|
|
69
|
+
expect([...(replayed.get("read_a")?.columns ?? [])].sort()).toEqual(["id", "title"]);
|
|
70
|
+
} finally {
|
|
71
|
+
rmSync(dir, { recursive: true, force: true });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Regression: publicstatus#0007_fix-secrets-table-columns adds three
|
|
76
|
+
// columns in ONE statement (comma-separated ADD COLUMN clauses) — the
|
|
77
|
+
// replay used to only pick up the first, reporting "metadata" and
|
|
78
|
+
// "last_rotated_at" as missing even though the migration creates them.
|
|
79
|
+
test("multiple ADD COLUMN clauses in a single ALTER TABLE statement all extend the table", () => {
|
|
80
|
+
const dir = tmpMigrationsDir();
|
|
81
|
+
try {
|
|
82
|
+
write(dir, "0001_init.sql", `CREATE TABLE IF NOT EXISTS "read_a" ("id" uuid PRIMARY KEY);`);
|
|
83
|
+
write(
|
|
84
|
+
dir,
|
|
85
|
+
"0002_add-cols.sql",
|
|
86
|
+
`ALTER TABLE "read_a"
|
|
87
|
+
ADD COLUMN IF NOT EXISTS "envelope" jsonb NOT NULL,
|
|
88
|
+
ADD COLUMN IF NOT EXISTS "metadata" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
|
89
|
+
ADD COLUMN IF NOT EXISTS "last_rotated_at" timestamp with time zone DEFAULT now() NOT NULL;`,
|
|
90
|
+
);
|
|
91
|
+
const replayed = replayMigrationsDir(dir);
|
|
92
|
+
expect([...(replayed.get("read_a")?.columns ?? [])].sort()).toEqual([
|
|
93
|
+
"envelope",
|
|
94
|
+
"id",
|
|
95
|
+
"last_rotated_at",
|
|
96
|
+
"metadata",
|
|
97
|
+
]);
|
|
98
|
+
} finally {
|
|
99
|
+
rmSync(dir, { recursive: true, force: true });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("mixed ADD + DROP COLUMN clauses in one statement apply in order", () => {
|
|
104
|
+
const dir = tmpMigrationsDir();
|
|
105
|
+
try {
|
|
106
|
+
write(
|
|
107
|
+
dir,
|
|
108
|
+
"0001_init.sql",
|
|
109
|
+
`CREATE TABLE IF NOT EXISTS "read_a" ("id" uuid PRIMARY KEY, "legacy" text);`,
|
|
110
|
+
);
|
|
111
|
+
write(
|
|
112
|
+
dir,
|
|
113
|
+
"0002_migrate-cols.sql",
|
|
114
|
+
`ALTER TABLE "read_a" DROP COLUMN IF EXISTS "legacy", ADD COLUMN IF NOT EXISTS "title" text;`,
|
|
115
|
+
);
|
|
116
|
+
const replayed = replayMigrationsDir(dir);
|
|
117
|
+
expect([...(replayed.get("read_a")?.columns ?? [])].sort()).toEqual(["id", "title"]);
|
|
118
|
+
} finally {
|
|
119
|
+
rmSync(dir, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("DROP COLUMN IF EXISTS (hand-edited) still removes the column", () => {
|
|
124
|
+
const dir = tmpMigrationsDir();
|
|
125
|
+
try {
|
|
126
|
+
write(
|
|
127
|
+
dir,
|
|
128
|
+
"0001_init.sql",
|
|
129
|
+
`CREATE TABLE IF NOT EXISTS "read_a" ("id" uuid PRIMARY KEY, "title" text);`,
|
|
130
|
+
);
|
|
131
|
+
write(dir, "0002_drop-col.sql", `ALTER TABLE "read_a" DROP COLUMN IF EXISTS "title";`);
|
|
132
|
+
const replayed = replayMigrationsDir(dir);
|
|
133
|
+
expect([...(replayed.get("read_a")?.columns ?? [])]).toEqual(["id"]);
|
|
134
|
+
} finally {
|
|
135
|
+
rmSync(dir, { recursive: true, force: true });
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// The real migrate-runner never executes a DESTRUCTIVE marker (it's a
|
|
140
|
+
// commented-out suggestion an operator uncomments deliberately) — but the
|
|
141
|
+
// snapshot already reflects the post-drop state as the intended end state,
|
|
142
|
+
// so replay must count the marker as applied or "unexpected-table"/
|
|
143
|
+
// "unexpected columns" fires forever against a table the snapshot
|
|
144
|
+
// correctly omits.
|
|
145
|
+
// #1473/2: CREATE TABLE IF NOT EXISTS is a no-op in real Postgres when
|
|
146
|
+
// the table already exists — treating it as an overwrite here silently
|
|
147
|
+
// discards whatever an in-between ALTER TABLE added, exactly the
|
|
148
|
+
// copy-paste-migration class this replay exists to catch.
|
|
149
|
+
test("CREATE TABLE IF NOT EXISTS for an already-created table is a no-op, not an overwrite", () => {
|
|
150
|
+
const dir = tmpMigrationsDir();
|
|
151
|
+
try {
|
|
152
|
+
write(dir, "0001_init.sql", `CREATE TABLE IF NOT EXISTS "read_a" ("id" uuid PRIMARY KEY);`);
|
|
153
|
+
write(dir, "0002_add-col.sql", `ALTER TABLE "read_a" ADD COLUMN "title" text;`);
|
|
154
|
+
// Accidental copy-paste of 0001 — same CREATE TABLE IF NOT EXISTS text.
|
|
155
|
+
write(
|
|
156
|
+
dir,
|
|
157
|
+
"0003_copy-paste.sql",
|
|
158
|
+
`CREATE TABLE IF NOT EXISTS "read_a" ("id" uuid PRIMARY KEY);`,
|
|
159
|
+
);
|
|
160
|
+
const replayed = replayMigrationsDir(dir);
|
|
161
|
+
expect([...(replayed.get("read_a")?.columns ?? [])].sort()).toEqual(["id", "title"]);
|
|
162
|
+
} finally {
|
|
163
|
+
rmSync(dir, { recursive: true, force: true });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("commented-out destructive DROP TABLE is replayed as applied", () => {
|
|
168
|
+
const dir = tmpMigrationsDir();
|
|
169
|
+
try {
|
|
170
|
+
write(dir, "0001_init.sql", `CREATE TABLE IF NOT EXISTS "read_a" ("id" uuid PRIMARY KEY);`);
|
|
171
|
+
write(
|
|
172
|
+
dir,
|
|
173
|
+
"0002_drop.sql",
|
|
174
|
+
`-- DESTRUCTIVE: DROP TABLE IF EXISTS "read_a"; -- uncomment + ensure backup`,
|
|
175
|
+
);
|
|
176
|
+
const replayed = replayMigrationsDir(dir);
|
|
177
|
+
expect([...replayed.keys()]).toEqual([]);
|
|
178
|
+
} finally {
|
|
179
|
+
rmSync(dir, { recursive: true, force: true });
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("commented-out destructive DROP COLUMN is replayed as applied", () => {
|
|
184
|
+
const dir = tmpMigrationsDir();
|
|
185
|
+
try {
|
|
186
|
+
write(
|
|
187
|
+
dir,
|
|
188
|
+
"0001_init.sql",
|
|
189
|
+
`CREATE TABLE IF NOT EXISTS "read_a" ("id" uuid PRIMARY KEY, "legacy" text);`,
|
|
190
|
+
);
|
|
191
|
+
write(
|
|
192
|
+
dir,
|
|
193
|
+
"0002_drop-col.sql",
|
|
194
|
+
`-- DESTRUCTIVE: ALTER TABLE "read_a" DROP COLUMN "legacy"; -- uncomment + ensure backup`,
|
|
195
|
+
);
|
|
196
|
+
const replayed = replayMigrationsDir(dir);
|
|
197
|
+
expect([...(replayed.get("read_a")?.columns ?? [])]).toEqual(["id"]);
|
|
198
|
+
} finally {
|
|
199
|
+
rmSync(dir, { recursive: true, force: true });
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("diffReplayAgainstSnapshot", () => {
|
|
205
|
+
test("clean: replayed schema matches snapshot exactly", () => {
|
|
206
|
+
const dir = tmpMigrationsDir();
|
|
207
|
+
try {
|
|
208
|
+
write(
|
|
209
|
+
dir,
|
|
210
|
+
"0001_init.sql",
|
|
211
|
+
`CREATE TABLE IF NOT EXISTS "read_a" ("id" uuid PRIMARY KEY, "title" text);`,
|
|
212
|
+
);
|
|
213
|
+
const snapshot = snapshotFromMetas([
|
|
214
|
+
meta("read_a", [
|
|
215
|
+
{ name: "id", pgType: "uuid", notNull: true, primaryKey: true },
|
|
216
|
+
{ name: "title", pgType: "text", notNull: false },
|
|
217
|
+
]),
|
|
218
|
+
]);
|
|
219
|
+
const mismatches = diffReplayAgainstSnapshot(replayMigrationsDir(dir), snapshot);
|
|
220
|
+
expect(mismatches).toEqual([]);
|
|
221
|
+
} finally {
|
|
222
|
+
rmSync(dir, { recursive: true, force: true });
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// Reproduces the kumiko-studio 0016 incident: the snapshot correctly
|
|
227
|
+
// records store_api_tokens, but the migration file that's supposed to
|
|
228
|
+
// create it is an accidental copy of an earlier file (creates read_accounts
|
|
229
|
+
// again instead) — CI must fail loud instead of shipping a table that only
|
|
230
|
+
// ever exists on paper.
|
|
231
|
+
test("misgenerated migration: snapshot expects a table no file actually creates", () => {
|
|
232
|
+
const dir = tmpMigrationsDir();
|
|
233
|
+
try {
|
|
234
|
+
write(
|
|
235
|
+
dir,
|
|
236
|
+
"0001_account-entity.sql",
|
|
237
|
+
`CREATE TABLE IF NOT EXISTS "read_accounts" ("id" uuid PRIMARY KEY, "stripe_customer_id" text);`,
|
|
238
|
+
);
|
|
239
|
+
write(
|
|
240
|
+
dir,
|
|
241
|
+
"0002_add-personal-access-tokens.sql",
|
|
242
|
+
// bug: this should create store_api_tokens, but it's a copy of 0001
|
|
243
|
+
`CREATE TABLE IF NOT EXISTS "read_accounts" ("id" uuid PRIMARY KEY, "stripe_customer_id" text);`,
|
|
244
|
+
);
|
|
245
|
+
const snapshot = snapshotFromMetas([
|
|
246
|
+
meta("read_accounts", [
|
|
247
|
+
{ name: "id", pgType: "uuid", notNull: true, primaryKey: true },
|
|
248
|
+
{ name: "stripe_customer_id", pgType: "text", notNull: false },
|
|
249
|
+
]),
|
|
250
|
+
meta("store_api_tokens", [
|
|
251
|
+
{ name: "id", pgType: "uuid", notNull: true, primaryKey: true },
|
|
252
|
+
{ name: "token_hash", pgType: "text", notNull: true },
|
|
253
|
+
]),
|
|
254
|
+
]);
|
|
255
|
+
const mismatches = diffReplayAgainstSnapshot(replayMigrationsDir(dir), snapshot);
|
|
256
|
+
expect(mismatches).toEqual([
|
|
257
|
+
{
|
|
258
|
+
tableName: "store_api_tokens",
|
|
259
|
+
kind: "missing-table",
|
|
260
|
+
detail: 'snapshot expects "store_api_tokens" but no migration file creates it',
|
|
261
|
+
},
|
|
262
|
+
]);
|
|
263
|
+
} finally {
|
|
264
|
+
rmSync(dir, { recursive: true, force: true });
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("column drift: migration creates a table with the wrong columns", () => {
|
|
269
|
+
const dir = tmpMigrationsDir();
|
|
270
|
+
try {
|
|
271
|
+
write(dir, "0001_init.sql", `CREATE TABLE IF NOT EXISTS "read_a" ("id" uuid PRIMARY KEY);`);
|
|
272
|
+
const snapshot = snapshotFromMetas([
|
|
273
|
+
meta("read_a", [
|
|
274
|
+
{ name: "id", pgType: "uuid", notNull: true, primaryKey: true },
|
|
275
|
+
{ name: "title", pgType: "text", notNull: false },
|
|
276
|
+
]),
|
|
277
|
+
]);
|
|
278
|
+
const mismatches = diffReplayAgainstSnapshot(replayMigrationsDir(dir), snapshot);
|
|
279
|
+
expect(mismatches).toEqual([
|
|
280
|
+
{ tableName: "read_a", kind: "column-drift", detail: "missing columns: title" },
|
|
281
|
+
]);
|
|
282
|
+
} finally {
|
|
283
|
+
rmSync(dir, { recursive: true, force: true });
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("kumiko-framework#1473: an unparsed ALTER TABLE RENAME statement fails loud instead of vanishing", () => {
|
|
288
|
+
const dir = tmpMigrationsDir();
|
|
289
|
+
try {
|
|
290
|
+
write(
|
|
291
|
+
dir,
|
|
292
|
+
"0001_init.sql",
|
|
293
|
+
`CREATE TABLE IF NOT EXISTS "widgets" ("id" uuid PRIMARY KEY);
|
|
294
|
+
ALTER TABLE "widgets" RENAME TO "gadgets";`,
|
|
295
|
+
);
|
|
296
|
+
expect(() => replayMigrationsDir(dir)).toThrow(/unparsed table-DDL statement/);
|
|
297
|
+
} finally {
|
|
298
|
+
rmSync(dir, { recursive: true, force: true });
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
test("kumiko-framework#1535: an unquoted CREATE TABLE identifier parses instead of failing loud", () => {
|
|
303
|
+
// Was "fails loud" pre-#1535 — but real committed migrations do this
|
|
304
|
+
// (publicstatus's 0014_marketing-waitlist.sql), and the old behavior
|
|
305
|
+
// turned an already-graceful missing-table report into a hard abort.
|
|
306
|
+
// Unquoted identifiers must parse like quoted ones.
|
|
307
|
+
const dir = tmpMigrationsDir();
|
|
308
|
+
try {
|
|
309
|
+
write(dir, "0001_init.sql", `CREATE TABLE widgets ("id" uuid PRIMARY KEY);`);
|
|
310
|
+
const replayed = replayMigrationsDir(dir);
|
|
311
|
+
expect([...replayed.keys()]).toEqual(["widgets"]);
|
|
312
|
+
expect([...(replayed.get("widgets")?.columns ?? [])]).toEqual(["id"]);
|
|
313
|
+
} finally {
|
|
314
|
+
rmSync(dir, { recursive: true, force: true });
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
test("schema-qualified CREATE TABLE (public.widgets) fails loud — IDENT rejects '.'", () => {
|
|
319
|
+
const dir = tmpMigrationsDir();
|
|
320
|
+
try {
|
|
321
|
+
write(dir, "0001_init.sql", "CREATE TABLE public.widgets (id uuid PRIMARY KEY);\n");
|
|
322
|
+
expect(() => replayMigrationsDir(dir)).toThrow(/unparsed table-DDL statement/);
|
|
323
|
+
} finally {
|
|
324
|
+
rmSync(dir, { recursive: true, force: true });
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("kumiko-framework#1535: a bare table-level PRIMARY KEY/UNIQUE clause isn't parsed as a column", () => {
|
|
329
|
+
const dir = tmpMigrationsDir();
|
|
330
|
+
try {
|
|
331
|
+
write(
|
|
332
|
+
dir,
|
|
333
|
+
"0001_init.sql",
|
|
334
|
+
`CREATE TABLE widgets (id uuid, sku text, name text, UNIQUE (sku), PRIMARY KEY (id));`,
|
|
335
|
+
);
|
|
336
|
+
const replayed = replayMigrationsDir(dir);
|
|
337
|
+
expect([...(replayed.get("widgets")?.columns ?? [])].sort()).toEqual(["id", "name", "sku"]);
|
|
338
|
+
} finally {
|
|
339
|
+
rmSync(dir, { recursive: true, force: true });
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("kumiko-framework#1535: shape-neutral ALTER COLUMN/CONSTRAINT clauses don't fail loud", () => {
|
|
344
|
+
// Regression for the false-positive class: these are all real,
|
|
345
|
+
// committed, shape-neutral DDL shapes (kumiko-studio's
|
|
346
|
+
// 0002_file_refs_soft_delete.sql, publicstatus's
|
|
347
|
+
// 0020_add-monitor-checks.sql) that the #1535 fix must recognize
|
|
348
|
+
// instead of throwing "unparsed table-DDL statement".
|
|
349
|
+
const dir = tmpMigrationsDir();
|
|
350
|
+
try {
|
|
351
|
+
write(
|
|
352
|
+
dir,
|
|
353
|
+
"0001_init.sql",
|
|
354
|
+
`CREATE TABLE IF NOT EXISTS "widgets" ("id" uuid PRIMARY KEY, "title" text);
|
|
355
|
+
ALTER TABLE "widgets" ALTER COLUMN "title" SET NOT NULL;
|
|
356
|
+
ALTER TABLE "widgets" ALTER COLUMN "title" SET DEFAULT 'untitled';
|
|
357
|
+
ALTER TABLE "widgets" ADD CONSTRAINT "widgets_title_check" CHECK (length("title") > 0);
|
|
358
|
+
ALTER TABLE "widgets" ENABLE ROW LEVEL SECURITY;
|
|
359
|
+
ALTER TABLE "widgets" OWNER TO app_user;`,
|
|
360
|
+
);
|
|
361
|
+
const replayed = replayMigrationsDir(dir);
|
|
362
|
+
expect([...(replayed.get("widgets")?.columns ?? [])].sort()).toEqual(["id", "title"]);
|
|
363
|
+
} finally {
|
|
364
|
+
rmSync(dir, { recursive: true, force: true });
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("unexpected table: migrations create a table the snapshot doesn't know about", () => {
|
|
369
|
+
const dir = tmpMigrationsDir();
|
|
370
|
+
try {
|
|
371
|
+
write(dir, "0001_init.sql", `CREATE TABLE IF NOT EXISTS "orphan" ("id" uuid PRIMARY KEY);`);
|
|
372
|
+
const mismatches = diffReplayAgainstSnapshot(replayMigrationsDir(dir), snapshotFromMetas([]));
|
|
373
|
+
expect(mismatches).toEqual([
|
|
374
|
+
{
|
|
375
|
+
tableName: "orphan",
|
|
376
|
+
kind: "unexpected-table",
|
|
377
|
+
detail: 'migrations create "orphan" but .snapshot.json has no entry for it',
|
|
378
|
+
},
|
|
379
|
+
]);
|
|
380
|
+
} finally {
|
|
381
|
+
rmSync(dir, { recursive: true, force: true });
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
});
|
|
@@ -17,4 +17,11 @@ describe("schema-inspection — resolveUnsafeClient guard", () => {
|
|
|
17
17
|
test("columnNamesOf throws the same named error", async () => {
|
|
18
18
|
await expect(columnNamesOf(noUnsafe, "x")).rejects.toThrow(/resolveUnsafeClient: no `\.unsafe/);
|
|
19
19
|
});
|
|
20
|
+
|
|
21
|
+
test("tableExists throws the same named error when .unsafe is a non-function value", async () => {
|
|
22
|
+
const notAFunction = { unsafe: "notafunction" } as unknown as DbConnection;
|
|
23
|
+
await expect(tableExists(notAFunction, "public.x")).rejects.toThrow(
|
|
24
|
+
/resolveUnsafeClient: no `\.unsafe/,
|
|
25
|
+
);
|
|
26
|
+
});
|
|
20
27
|
});
|
|
@@ -105,7 +105,7 @@ describe("schema migration workflows", () => {
|
|
|
105
105
|
// Entity fields
|
|
106
106
|
expect(columns.get("title")?.dataType).toBe("text");
|
|
107
107
|
expect(columns.get("body")?.dataType).toBe("text");
|
|
108
|
-
expect(columns.get("view_count")?.dataType).toBe("
|
|
108
|
+
expect(columns.get("view_count")?.dataType).toBe("double precision");
|
|
109
109
|
expect(columns.get("published_at")?.dataType).toContain("timestamp");
|
|
110
110
|
expect(columns.get("is_draft")?.dataType).toBe("boolean");
|
|
111
111
|
expect(columns.get("is_draft")?.isNullable).toBe(false); // has default → NOT NULL
|
|
@@ -109,3 +109,42 @@ describe("lock-step — softDelete + explizite Indexes", () => {
|
|
|
109
109
|
expect((fromBuilder?.indexes ?? []).length).toBeGreaterThanOrEqual(3);
|
|
110
110
|
});
|
|
111
111
|
});
|
|
112
|
+
|
|
113
|
+
// Dritte Probe: lookupable-Feld (#818) — bidx-Spalte, bidx-Index und das
|
|
114
|
+
// partielle Unique-Pendant müssen auf beiden Pfaden identisch entstehen.
|
|
115
|
+
const entityWithLookupable = createEntity({
|
|
116
|
+
table: "read_lockstep_probe_bidx",
|
|
117
|
+
fields: {
|
|
118
|
+
email: { type: "text", required: true, pii: true, lookupable: true },
|
|
119
|
+
tenantSlug: { type: "text", required: true },
|
|
120
|
+
},
|
|
121
|
+
indexes: [{ columns: ["tenantSlug", "email"], unique: true }],
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("lock-step — lookupable / blind-index (#818)", () => {
|
|
125
|
+
const fromBuilder = asEntityTableMeta(
|
|
126
|
+
buildEntityTable("lockstepProbeBidx", entityWithLookupable),
|
|
127
|
+
);
|
|
128
|
+
const fromMeta = buildEntityTableMeta("lockstepProbeBidx", entityWithLookupable);
|
|
129
|
+
|
|
130
|
+
test("identical columns inkl. nullable bidx-Spalte", () => {
|
|
131
|
+
expect(byName<ColumnMeta>(fromBuilder?.columns ?? [])).toEqual(
|
|
132
|
+
byName<ColumnMeta>(fromMeta.columns),
|
|
133
|
+
);
|
|
134
|
+
const bidx = (fromMeta.columns ?? []).find((c) => c.name === "email_bidx");
|
|
135
|
+
expect(bidx).toEqual({ name: "email_bidx", pgType: "text", notNull: false });
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("identical indexes inkl. bidx-Index + partiellem Unique-Pendant", () => {
|
|
139
|
+
expect(byName<IndexMeta>(fromBuilder?.indexes ?? [])).toEqual(
|
|
140
|
+
byName<IndexMeta>(fromMeta.indexes),
|
|
141
|
+
);
|
|
142
|
+
const names = fromMeta.indexes.map((i) => i.name);
|
|
143
|
+
expect(names).toContain("read_lockstep_probe_bidx_email_bidx_idx");
|
|
144
|
+
const partial = fromMeta.indexes.find((i) => i.name.endsWith("_tenant_slug_email_unique_bidx"));
|
|
145
|
+
expect(partial).toBeDefined();
|
|
146
|
+
expect(partial?.unique).toBe(true);
|
|
147
|
+
expect(partial?.columns).toEqual(["tenant_slug", "email_bidx"]);
|
|
148
|
+
expect(partial?.whereSql).toBe('"email_bidx" IS NOT NULL');
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { createEntity, createTextField } from "../../engine";
|
|
3
3
|
import { testTenantId } from "../../stack";
|
|
4
|
+
import type { DbRunner } from "../connection";
|
|
5
|
+
import type { TableColumns } from "../dialect";
|
|
6
|
+
import { buildEntityTableMeta } from "../entity-table-meta";
|
|
4
7
|
import { buildEntityTable } from "../table-builder";
|
|
5
8
|
import { createTenantDb } from "../tenant-db";
|
|
6
9
|
|
|
@@ -13,20 +16,25 @@ const entity = createEntity({
|
|
|
13
16
|
table: "merge_items",
|
|
14
17
|
fields: { name: createTextField({ required: true }) },
|
|
15
18
|
});
|
|
16
|
-
|
|
19
|
+
// Brand (#742) is compile-time-only; hold the handle at the unbranded TableColumns
|
|
20
|
+
// view so the method-form scoping test still compiles (runtime shape is identical).
|
|
21
|
+
const table: TableColumns = buildEntityTable("mergeItem", entity);
|
|
17
22
|
|
|
18
23
|
const own = testTenantId(1);
|
|
19
24
|
const foreign = testTenantId(2);
|
|
20
25
|
|
|
21
26
|
type Captured = { sql: string; values: readonly unknown[] };
|
|
22
27
|
|
|
23
|
-
function recordingDb(captured: Captured[]) {
|
|
28
|
+
function recordingDb(captured: Captured[]): DbRunner {
|
|
24
29
|
return {
|
|
25
30
|
unsafe: async (sql: string, values: readonly unknown[]) => {
|
|
26
31
|
captured.push({ sql, values });
|
|
27
32
|
return [] as unknown[];
|
|
28
33
|
},
|
|
29
|
-
|
|
34
|
+
begin: async () => {
|
|
35
|
+
throw new Error("begin not used in these tests");
|
|
36
|
+
},
|
|
37
|
+
} as DbRunner;
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
describe("tenant-db WHERE merge — caller cannot override tenant scope", () => {
|
|
@@ -113,3 +121,41 @@ describe("tenant-db WHERE merge — narrowing within the enforced scope", () =>
|
|
|
113
121
|
expect(captured[0]?.values).not.toContain(foreign);
|
|
114
122
|
});
|
|
115
123
|
});
|
|
124
|
+
|
|
125
|
+
// Root-cause regression for the cross-tenant leak fixed in hasTenantColumn:
|
|
126
|
+
// unmanaged direct-write stores (buildEntityTableMeta, e.g. userSessionTable,
|
|
127
|
+
// apiTokenTable) store tenantId as a snake_case column-meta entry, not a
|
|
128
|
+
// direct `table.tenantId` property — a naive property check silently treated
|
|
129
|
+
// them as tenant-less and skipped the WHERE-scope entirely.
|
|
130
|
+
describe("tenant-db WHERE merge — unmanaged EntityTableMeta tables (buildEntityTableMeta)", () => {
|
|
131
|
+
const unmanagedEntity = createEntity({
|
|
132
|
+
table: "merge_meta_items",
|
|
133
|
+
fields: { tenantId: createTextField({ required: true }), name: createTextField() },
|
|
134
|
+
});
|
|
135
|
+
const unmanagedTable = buildEntityTableMeta("merge-meta-item", unmanagedEntity);
|
|
136
|
+
|
|
137
|
+
test("selectMany still applies the tenant scope (pre-fix: no WHERE at all)", async () => {
|
|
138
|
+
const captured: Captured[] = [];
|
|
139
|
+
const tdb = createTenantDb(recordingDb(captured), own);
|
|
140
|
+
|
|
141
|
+
// selectMany's Table param types the branded-EntityTable shape only;
|
|
142
|
+
// EntityTableMeta reads work identically at runtime (both normalize via
|
|
143
|
+
// asEntityTableMeta) — same unbranded-view cast as `table` above.
|
|
144
|
+
await tdb.selectMany(unmanagedTable as unknown as TableColumns, { name: "needle" });
|
|
145
|
+
|
|
146
|
+
expect(captured[0]?.sql).toMatch(/tenant_id" IN /i);
|
|
147
|
+
expect(captured[0]?.values).toContain(own);
|
|
148
|
+
expect(captured[0]?.values).not.toContain(foreign);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("updateMany still forces own tenantId (pre-fix: no scope on writes either)", async () => {
|
|
152
|
+
const captured: Captured[] = [];
|
|
153
|
+
const tdb = createTenantDb(recordingDb(captured), own);
|
|
154
|
+
|
|
155
|
+
await tdb.updateMany(unmanagedTable, { name: "x" }, { tenantId: foreign });
|
|
156
|
+
|
|
157
|
+
const update = captured.find((c) => /UPDATE/i.test(c.sql));
|
|
158
|
+
expect(update?.values).toContain(own);
|
|
159
|
+
expect(update?.values).not.toContain(foreign);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
unsafeCreateEntityTable,
|
|
11
11
|
unsafePushTables,
|
|
12
12
|
} from "../../stack";
|
|
13
|
-
import { table as pgTable, serial, text, timestamp } from "../dialect";
|
|
13
|
+
import { table as pgTable, serial, type TableColumns, text, timestamp } from "../dialect";
|
|
14
14
|
import { buildEntityTable } from "../table-builder";
|
|
15
15
|
import { createTenantDb } from "../tenant-db";
|
|
16
16
|
|
|
@@ -26,7 +26,11 @@ const entity = createEntity({
|
|
|
26
26
|
softDelete: true,
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
// The ES-write brand (#742) is a compile-time phantom — no runtime property. This
|
|
30
|
+
// suite exercises TenantDb method-form scoping on a real entity table, so the handle
|
|
31
|
+
// is held at the unbranded TableColumns view: identical runtime shape, and both reads
|
|
32
|
+
// and method-form writes accept it (a branded EntityTable is now rejected).
|
|
33
|
+
const table: TableColumns = buildEntityTable("tenantDbItem", entity);
|
|
30
34
|
|
|
31
35
|
// --- System table (no tenantId — like job_runs) ---
|
|
32
36
|
|
package/src/db/api.ts
CHANGED
|
@@ -15,7 +15,7 @@ export type DbConnectionOptions = {
|
|
|
15
15
|
|
|
16
16
|
// Connection-Handle: db für Queries, client für Legacy-Zugriff (LISTEN/NOTIFY-Peer
|
|
17
17
|
// bei Bun.SQL), close für Pool-Shutdown.
|
|
18
|
-
export type
|
|
18
|
+
export type DbPoolHandle = {
|
|
19
19
|
/** Provider Connection — Calls gehen über asRawClient() oder direkt. */
|
|
20
20
|
// biome-ignore lint/suspicious/noExplicitAny: cross-provider connection — postgres-js | Bun.SQL
|
|
21
21
|
readonly db: any;
|
|
@@ -34,7 +34,7 @@ let _provider: undefined | (() => Promise<typeof import("./postgres-provider")>)
|
|
|
34
34
|
export async function createConnection(
|
|
35
35
|
url: string,
|
|
36
36
|
options: DbConnectionOptions = {},
|
|
37
|
-
): Promise<
|
|
37
|
+
): Promise<DbPoolHandle> {
|
|
38
38
|
const p = process.env["DB_PROVIDER"];
|
|
39
39
|
if (p === "bun" || p === "bun-sql") {
|
|
40
40
|
const { createBunConnection } = await import("./bun-provider");
|
|
@@ -1,22 +1,18 @@
|
|
|
1
1
|
// applyEntityEvent — die EINZIGE Schreib-Logik für r.entity-Tabellen aus
|
|
2
2
|
// Stored-Events. Beide Aufrufer benutzen sie:
|
|
3
3
|
//
|
|
4
|
-
// - createEventStoreExecutor (live, im Write-TX) — übergibt
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// bleibt aber stripped (siehe append-Site im Executor).
|
|
4
|
+
// - createEventStoreExecutor (live, im Write-TX) — übergibt das gerade
|
|
5
|
+
// appendete StoredEvent direkt; sensitive Felder liegen als Tabellen-
|
|
6
|
+
// Ciphertext im Payload (boot-validiert pii/encrypted, #967).
|
|
8
7
|
// - rebuildProjection via ImplicitProjection (replay, im Rebuild-TX) —
|
|
9
|
-
// übergibt
|
|
10
|
-
//
|
|
11
|
-
// ist als load-bearing Backlog-Item gepinnt — siehe
|
|
12
|
-
// docs/plans/architecture/migrations.md Sektion "Backlog (Welle 3+)"
|
|
13
|
-
// → "Sensitive-Field-Persistenz im Rebuild" für Optionen a/b/c).
|
|
8
|
+
// übergibt dasselbe StoredEvent; apply kopiert Ciphertext byte-gleich
|
|
9
|
+
// zurück, bidx wird unten neu berechnet.
|
|
14
10
|
//
|
|
15
|
-
// Live==Rebuild-Equivalence ist damit by-construction für
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// db/__tests__/implicit-projection-equivalence.integration.ts.
|
|
11
|
+
// Live==Rebuild-Equivalence ist damit by-construction für ALLE Felder —
|
|
12
|
+
// eine geänderte Schreib-Logik muss nur an EINER Stelle gepflegt werden,
|
|
13
|
+
// kein Sync-Contract mehr. Einzige legitime Divergenz: Crypto-Shredding
|
|
14
|
+
// (DEK erased → Wert unlesbar, bidx NULL). Load-bearing Test:
|
|
15
|
+
// db/__tests__/implicit-projection-equivalence.integration.test.ts.
|
|
20
16
|
//
|
|
21
17
|
// Tenant-Isolation: applyEntityEvent erwartet einen rohen DbRunner (TX
|
|
22
18
|
// oder pool), KEINEN TenantDb-Wrapper. Schutz kommt aus zwei Quellen:
|
|
@@ -49,6 +45,7 @@
|
|
|
49
45
|
// - "skipped" → Event ist kein Auto-Verb (Domain-Event auf demselben
|
|
50
46
|
// Aggregate). Caller no-op.
|
|
51
47
|
|
|
48
|
+
import { collectLookupableFields, computeBlindIndexValues } from "../crypto/blind-index";
|
|
52
49
|
import { deleteMany, insertOne, updateMany } from "../db/query";
|
|
53
50
|
import type { EntityDefinition } from "../engine/types";
|
|
54
51
|
import { InternalError } from "../errors";
|
|
@@ -116,6 +113,11 @@ export async function applyEntityEvent(
|
|
|
116
113
|
}
|
|
117
114
|
const row = await insertOne<DbRow>(tx, table, {
|
|
118
115
|
...event.payload,
|
|
116
|
+
// Blind-Index-Spalten aus dem Payload-Wert (ciphertext → decrypt →
|
|
117
|
+
// HMAC, plaintext → HMAC, erased → NULL). Hier statt im Executor,
|
|
118
|
+
// damit Live-Write und Rebuild denselben Wert produzieren und der
|
|
119
|
+
// deterministische HMAC nie im Event-Log landet.
|
|
120
|
+
...(await computeBlindIndexValues(event.payload, collectLookupableFields(entity))),
|
|
119
121
|
tenantId,
|
|
120
122
|
id: event.aggregateId,
|
|
121
123
|
version: event.version,
|
|
@@ -142,6 +144,8 @@ export async function applyEntityEvent(
|
|
|
142
144
|
table,
|
|
143
145
|
{
|
|
144
146
|
...changes,
|
|
147
|
+
// bidx nur für die geänderten lookupable-Felder (siehe created).
|
|
148
|
+
...(await computeBlindIndexValues(changes, collectLookupableFields(entity))),
|
|
145
149
|
version: event.version,
|
|
146
150
|
modifiedAt: event.createdAt,
|
|
147
151
|
modifiedById: event.createdBy,
|