@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
package/src/stack/test-stack.ts
CHANGED
|
@@ -2,14 +2,15 @@ import type { Hono } from "hono";
|
|
|
2
2
|
import type { AuthRoutesConfig } from "../api/auth-routes";
|
|
3
3
|
import type { JwtHelper } from "../api/jwt";
|
|
4
4
|
import { buildServer } from "../api/server";
|
|
5
|
-
import { createSseBroker } from "../api/sse-broker";
|
|
5
|
+
import { createSseBroker, type SseBroker } from "../api/sse-broker";
|
|
6
6
|
import type { PgClient } from "../db/connection";
|
|
7
7
|
import { extractTableInfo } from "../db/query";
|
|
8
8
|
import { createRegistry } from "../engine/registry";
|
|
9
|
-
import type { FeatureDefinition, Registry, TenantId } from "../engine/types";
|
|
9
|
+
import type { FeatureDefinition, JobRunIn, Registry, TenantId } from "../engine/types";
|
|
10
10
|
import { createArchivedStreamsTable, createEventsTable } from "../event-store";
|
|
11
|
+
import { createJobRunner, type JobRunner } from "../jobs";
|
|
11
12
|
import type { Lifecycle } from "../lifecycle";
|
|
12
|
-
import type
|
|
13
|
+
import { createNoopProvider, type ObservabilityProvider } from "../observability";
|
|
13
14
|
import type { Dispatcher, EventDispatcher } from "../pipeline";
|
|
14
15
|
import { createEntityCache, createEventDedup, createIdempotencyGuard } from "../pipeline";
|
|
15
16
|
import { createInMemorySearchAdapter } from "../search";
|
|
@@ -24,13 +25,17 @@ export type TestStack = {
|
|
|
24
25
|
app: Hono;
|
|
25
26
|
jwt: JwtHelper;
|
|
26
27
|
registry: Registry;
|
|
27
|
-
|
|
28
|
-
db: any;
|
|
28
|
+
db: import("../db").DbConnection;
|
|
29
29
|
redis: TestRedis;
|
|
30
30
|
search: SearchAdapter;
|
|
31
31
|
events: EventCollector;
|
|
32
32
|
http: RequestHelper;
|
|
33
33
|
observability: ObservabilityProvider;
|
|
34
|
+
// In-memory broker backing the server's SSE routes + access-invalidation
|
|
35
|
+
// channel. Tests subscribe directly (e.g. sseBroker.subscribeAccessInvalidation)
|
|
36
|
+
// to assert a consumer pushed an invalidation without opening a real SSE
|
|
37
|
+
// connection.
|
|
38
|
+
sseBroker: SseBroker;
|
|
34
39
|
// Command-dispatcher behind the HTTP routes — for direct system-writes
|
|
35
40
|
// in tests and dev-server extraRoutes (provider-webhook wiring).
|
|
36
41
|
dispatcher: Dispatcher;
|
|
@@ -41,6 +46,10 @@ export type TestStack = {
|
|
|
41
46
|
// Only set when the caller passed `lifecycle` via options. Tests that
|
|
42
47
|
// exercise drain() / /health/ready wire one in; ordinary suites ignore it.
|
|
43
48
|
lifecycle?: Lifecycle;
|
|
49
|
+
// Only set when `options.jobs` is truthy AND ≥1 `r.job(...)` is
|
|
50
|
+
// registered in the mounted features. Lets integration tests call
|
|
51
|
+
// `stack.jobRunner.dispatch(...)` directly, mirroring `ctx.jobRunner`.
|
|
52
|
+
jobRunner?: JobRunner;
|
|
44
53
|
cleanup: () => Promise<void>;
|
|
45
54
|
};
|
|
46
55
|
|
|
@@ -61,8 +70,7 @@ export type TestStackOptions = {
|
|
|
61
70
|
| Record<string, unknown>
|
|
62
71
|
| ((deps: {
|
|
63
72
|
registry: Registry;
|
|
64
|
-
|
|
65
|
-
db: any;
|
|
73
|
+
db: import("../db").DbConnection;
|
|
66
74
|
sseBroker: import("../api/sse-broker").SseBroker;
|
|
67
75
|
redis: import("ioredis").default;
|
|
68
76
|
}) => Record<string, unknown>);
|
|
@@ -114,11 +122,31 @@ export type TestStackOptions = {
|
|
|
114
122
|
| import("../api/server").ServerOptions["anonymousAccess"]
|
|
115
123
|
| ((deps: {
|
|
116
124
|
registry: Registry;
|
|
117
|
-
|
|
118
|
-
db: any;
|
|
125
|
+
db: import("../db").DbConnection;
|
|
119
126
|
sseBroker: import("../api/sse-broker").SseBroker;
|
|
120
127
|
redis: import("ioredis").default;
|
|
121
128
|
}) => import("../api/server").ServerOptions["anonymousAccess"]);
|
|
129
|
+
/** Optional post-factory enricher (e.g. merge auth-foundation tenant
|
|
130
|
+
* providers). Keeps framework free of a bundled-features dependency. */
|
|
131
|
+
enrichAnonymousAccess?: (
|
|
132
|
+
base: import("../api/server").ServerOptions["anonymousAccess"] | undefined,
|
|
133
|
+
deps: {
|
|
134
|
+
registry: Registry;
|
|
135
|
+
db: import("../db").DbConnection;
|
|
136
|
+
},
|
|
137
|
+
) => Promise<import("../api/server").ServerOptions["anonymousAccess"] | undefined>;
|
|
138
|
+
/** Opt-in JobRunner wired into ctx.jobRunner and merged into
|
|
139
|
+
* dispatcherOptions so event-triggered jobs enqueue on commit — mirrors
|
|
140
|
+
* the prod entrypoint's `buildJobRunnerWithHook`. Unlike prod (which
|
|
141
|
+
* always builds one), this only builds a runner when `registry.getAllJobs()`
|
|
142
|
+
* is non-empty — a deliberate test-stack-only shortcut, not parity.
|
|
143
|
+
* Default `undefined`. Pass `consumerLane` only when this test IS the
|
|
144
|
+
* sole consumer (otherwise enqueuer-only, avoiding double-running
|
|
145
|
+
* `runOnBoot`/cron jobs against a caller-owned consumer). */
|
|
146
|
+
jobs?: {
|
|
147
|
+
consumerLane?: JobRunIn;
|
|
148
|
+
queueNamePrefix?: string;
|
|
149
|
+
};
|
|
122
150
|
};
|
|
123
151
|
|
|
124
152
|
const DEFAULT_JWT_SECRET = "test-stack-secret-minimum-32-characters!!";
|
|
@@ -213,40 +241,8 @@ export async function setupTestStack(options: TestStackOptions): Promise<TestSta
|
|
|
213
241
|
const events = createEventCollector();
|
|
214
242
|
const registry = createRegistry([...options.features]);
|
|
215
243
|
|
|
216
|
-
//
|
|
217
|
-
|
|
218
|
-
const searchableFields: string[] = [];
|
|
219
|
-
for (const feature of options.features) {
|
|
220
|
-
for (const [, entity] of Object.entries(feature.entities ?? {})) {
|
|
221
|
-
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
222
|
-
if (field.type === "text" && field.searchable) {
|
|
223
|
-
searchableFields.push(fieldName);
|
|
224
|
-
}
|
|
225
|
-
if (field.type === "embedded") {
|
|
226
|
-
for (const [subName, subField] of Object.entries(field.schema)) {
|
|
227
|
-
if (subField.searchable) {
|
|
228
|
-
searchableFields.push(`${fieldName}_${subName}`);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (options.searchConfig) {
|
|
237
|
-
await searchAdapter.configure(options.searchConfig.tenantId, {
|
|
238
|
-
searchableFields: options.searchConfig.searchableFields,
|
|
239
|
-
rankingFields: options.searchConfig.rankingFields,
|
|
240
|
-
});
|
|
241
|
-
} else if (searchableFields.length > 0) {
|
|
242
|
-
await searchAdapter.configure("00000000-0000-4000-8000-000000000001", {
|
|
243
|
-
searchableFields,
|
|
244
|
-
rankingFields: searchableFields,
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Wire SSE broker with event collector
|
|
244
|
+
// Wire SSE broker with event collector — built early (not just before
|
|
245
|
+
// buildServer) because extraContext + the job context below both need it.
|
|
250
246
|
const sseBroker = createSseBroker();
|
|
251
247
|
sseBroker.addClient(
|
|
252
248
|
"tenant:00000000-0000-4000-8000-000000000001",
|
|
@@ -254,8 +250,6 @@ export async function setupTestStack(options: TestStackOptions): Promise<TestSta
|
|
|
254
250
|
() => {},
|
|
255
251
|
);
|
|
256
252
|
|
|
257
|
-
const idempotency = createIdempotencyGuard(testRedis.redis, { ttlSeconds: 60 });
|
|
258
|
-
const eventDedup = createEventDedup(testRedis.redis, { ttlSeconds: 60 });
|
|
259
253
|
const entityCache = createEntityCache(testRedis.redis, { ttlSeconds: 60 });
|
|
260
254
|
|
|
261
255
|
// A static `files.storageProvider` is wired as the per-tenant resolver — the
|
|
@@ -267,97 +261,221 @@ export async function setupTestStack(options: TestStackOptions): Promise<TestSta
|
|
|
267
261
|
fileProviderResolver = () => Promise.resolve(provider);
|
|
268
262
|
}
|
|
269
263
|
|
|
270
|
-
const
|
|
264
|
+
const observability = options.observability ?? createNoopProvider();
|
|
265
|
+
|
|
266
|
+
// Same AppContext buildServer() gets below (db/redis/searchAdapter/
|
|
267
|
+
// entityCache/masterKeyProvider/fileProviderResolver/extraContext) —
|
|
268
|
+
// shared so the job context can't drift from the request-path context
|
|
269
|
+
// (kumiko-framework#1232: a reduced `{ db, registry }` literal let jobs
|
|
270
|
+
// pass in tests while reaching for fields only prod's context has).
|
|
271
|
+
//
|
|
272
|
+
// effectiveFeatures deliberately stays OUT of appContext — prod never puts
|
|
273
|
+
// it there either (only dispatcherOptions.effectiveFeatures, consumed by
|
|
274
|
+
// the command-dispatcher — see buildJobRunnerWithHook in entrypoint/
|
|
275
|
+
// index.ts). Putting it here would reintroduce the exact test-vs-prod
|
|
276
|
+
// drift #1232 fixed: a job/handler reading `ctx.effectiveFeatures` would
|
|
277
|
+
// pass in tests and break in prod (kumiko-framework#1255).
|
|
278
|
+
const appContext = {
|
|
279
|
+
db: testDb.db,
|
|
280
|
+
redis: testRedis.redis,
|
|
281
|
+
searchAdapter,
|
|
282
|
+
entityCache,
|
|
271
283
|
registry,
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
284
|
+
...(options.masterKeyProvider ? { masterKeyProvider: options.masterKeyProvider } : {}),
|
|
285
|
+
...(fileProviderResolver ? { _fileProviderResolver: fileProviderResolver } : {}),
|
|
286
|
+
...(typeof options.extraContext === "function"
|
|
287
|
+
? options.extraContext({ registry, db: testDb.db, sseBroker, redis: testRedis.redis })
|
|
288
|
+
: options.extraContext),
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// Built before buildServer() so it can be merged into dispatcherOptions
|
|
292
|
+
// (write handlers' `ctx.jobRunner.dispatch(...)` and the afterCommit
|
|
293
|
+
// event-trigger hook both need it present at dispatcher-construction
|
|
294
|
+
// time, same as the prod entrypoint's buildJobRunnerWithHook). Uses the
|
|
295
|
+
// test-stack's own ephemeral redis — no separate `redisUrl` seam needed.
|
|
296
|
+
//
|
|
297
|
+
// context = appContext + tracer/meter, mirroring the prod entrypoint's
|
|
298
|
+
// `contextWithObservability(options.context, observability)`.
|
|
299
|
+
let jobRunner: JobRunner | undefined;
|
|
300
|
+
if (options.jobs && registry.getAllJobs().size > 0) {
|
|
301
|
+
jobRunner = createJobRunner({
|
|
277
302
|
registry,
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
: {}),
|
|
315
|
-
...(options.observability ? { observability: options.observability } : {}),
|
|
316
|
-
...(options.lifecycle ? { lifecycle: options.lifecycle } : {}),
|
|
317
|
-
...(options.rateLimit ? { rateLimit: options.rateLimit } : {}),
|
|
318
|
-
...(options.anonymousAccess
|
|
319
|
-
? {
|
|
320
|
-
anonymousAccess:
|
|
321
|
-
typeof options.anonymousAccess === "function"
|
|
322
|
-
? options.anonymousAccess({
|
|
323
|
-
registry,
|
|
324
|
-
db: testDb.db,
|
|
325
|
-
sseBroker,
|
|
326
|
-
redis: testRedis.redis,
|
|
327
|
-
})
|
|
328
|
-
: options.anonymousAccess,
|
|
303
|
+
context: { ...appContext, tracer: observability.tracer, meter: observability.meter },
|
|
304
|
+
// The real REDIS_URL, not one rebuilt from `.options` — that lost
|
|
305
|
+
// password/username/tls/path (pr-review kumiko-framework #1036/2).
|
|
306
|
+
redisUrl: testRedis.redisUrl,
|
|
307
|
+
...(options.jobs.consumerLane !== undefined && { consumerLane: options.jobs.consumerLane }),
|
|
308
|
+
...(options.jobs.queueNamePrefix !== undefined && {
|
|
309
|
+
queueNamePrefix: options.jobs.queueNamePrefix,
|
|
310
|
+
}),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// From here on, any throw must stop a jobRunner that was already created
|
|
315
|
+
// above (createJobRunner() itself opens live BullMQ Queue/lock Redis
|
|
316
|
+
// connections, .start() adds a live worker) — otherwise a failing
|
|
317
|
+
// buildServer()/search-config/etc. leaks that connection and the test
|
|
318
|
+
// process hangs on exit (pr-review kumiko-framework #1036/1).
|
|
319
|
+
try {
|
|
320
|
+
if (jobRunner) await jobRunner.start();
|
|
321
|
+
|
|
322
|
+
// Auto-configure search for tenant 1 based on registry
|
|
323
|
+
if (enabledHooks.includes("search")) {
|
|
324
|
+
const searchableFields: string[] = [];
|
|
325
|
+
for (const feature of options.features) {
|
|
326
|
+
for (const [, entity] of Object.entries(feature.entities ?? {})) {
|
|
327
|
+
for (const [fieldName, field] of Object.entries(entity.fields)) {
|
|
328
|
+
if (field.type === "text" && field.searchable) {
|
|
329
|
+
searchableFields.push(fieldName);
|
|
330
|
+
}
|
|
331
|
+
if (field.type === "embedded") {
|
|
332
|
+
for (const [subName, subField] of Object.entries(field.schema)) {
|
|
333
|
+
if (subField.searchable) {
|
|
334
|
+
searchableFields.push(`${fieldName}_${subName}`);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
329
339
|
}
|
|
330
|
-
|
|
331
|
-
});
|
|
340
|
+
}
|
|
332
341
|
|
|
333
|
-
|
|
342
|
+
if (options.searchConfig) {
|
|
343
|
+
await searchAdapter.configure(options.searchConfig.tenantId, {
|
|
344
|
+
searchableFields: options.searchConfig.searchableFields,
|
|
345
|
+
rankingFields: options.searchConfig.rankingFields,
|
|
346
|
+
});
|
|
347
|
+
} else if (searchableFields.length > 0) {
|
|
348
|
+
await searchAdapter.configure("00000000-0000-4000-8000-000000000001", {
|
|
349
|
+
searchableFields,
|
|
350
|
+
rankingFields: searchableFields,
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
}
|
|
334
354
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
// but passInFlight serialises concurrent passes — tests that drain via
|
|
338
|
-
// runOnce() remain deterministic. Tests that specifically exercise the
|
|
339
|
-
// timer loop call start() again (idempotent) after setup.
|
|
340
|
-
if (eventDispatcher) await eventDispatcher.ensureRegistered();
|
|
355
|
+
const idempotency = createIdempotencyGuard(testRedis.redis, { ttlSeconds: 60 });
|
|
356
|
+
const eventDedup = createEventDedup(testRedis.redis, { ttlSeconds: 60 });
|
|
341
357
|
|
|
342
|
-
|
|
358
|
+
const server = buildServer({
|
|
359
|
+
registry,
|
|
360
|
+
context: appContext,
|
|
361
|
+
jwtSecret,
|
|
362
|
+
...(options.observability ? { observability: options.observability } : {}),
|
|
363
|
+
dispatcherOptions: {
|
|
364
|
+
idempotency,
|
|
365
|
+
...(options.effectiveFeatures && { effectiveFeatures: options.effectiveFeatures }),
|
|
366
|
+
...(jobRunner && { jobRunner }),
|
|
367
|
+
},
|
|
368
|
+
eventDedup,
|
|
369
|
+
sseBroker,
|
|
370
|
+
// Tests drive the dispatcher via stack.eventDispatcher.runOnce() for
|
|
371
|
+
// deterministic drains — no timer-induced flakiness. pollIntervalMs
|
|
372
|
+
// stays short anyway in case a test opts into `.start()`. pgClient
|
|
373
|
+
// plumbs through the LISTEN wake-up for tests that want to measure
|
|
374
|
+
// post-commit latency (Sprint E.4).
|
|
375
|
+
eventDispatcher: {
|
|
376
|
+
pollIntervalMs: 50,
|
|
377
|
+
pgClient: testDb.client as PgClient | undefined,
|
|
378
|
+
systemConsumers: {
|
|
379
|
+
sse: enabledHooks.includes("sse"),
|
|
380
|
+
search: enabledHooks.includes("search"),
|
|
381
|
+
accessInvalidation: enabledHooks.includes("sse"),
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
// Default tests to no login rate-limiter so existing suites that loop
|
|
385
|
+
// over logins don't hit a 429 after 10 attempts. Suites specifically
|
|
386
|
+
// testing the limiter can override via authConfig.loginRateLimit.
|
|
387
|
+
...(options.authConfig
|
|
388
|
+
? {
|
|
389
|
+
auth: {
|
|
390
|
+
...options.authConfig,
|
|
391
|
+
...(options.authConfig.loginRateLimit === undefined ? { loginRateLimit: null } : {}),
|
|
392
|
+
},
|
|
393
|
+
}
|
|
394
|
+
: {}),
|
|
395
|
+
...(options.lifecycle ? { lifecycle: options.lifecycle } : {}),
|
|
396
|
+
...(options.rateLimit ? { rateLimit: options.rateLimit } : {}),
|
|
397
|
+
...(await (async () => {
|
|
398
|
+
const baseAnon =
|
|
399
|
+
typeof options.anonymousAccess === "function"
|
|
400
|
+
? options.anonymousAccess({
|
|
401
|
+
registry,
|
|
402
|
+
db: testDb.db,
|
|
403
|
+
sseBroker,
|
|
404
|
+
redis: testRedis.redis,
|
|
405
|
+
})
|
|
406
|
+
: options.anonymousAccess;
|
|
407
|
+
const resolvedAnon = options.enrichAnonymousAccess
|
|
408
|
+
? await options.enrichAnonymousAccess(baseAnon, {
|
|
409
|
+
registry,
|
|
410
|
+
db: testDb.db,
|
|
411
|
+
})
|
|
412
|
+
: baseAnon;
|
|
413
|
+
return resolvedAnon ? { anonymousAccess: resolvedAnon } : {};
|
|
414
|
+
})()),
|
|
415
|
+
});
|
|
343
416
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
417
|
+
const eventDispatcher: EventDispatcher | undefined = server.eventDispatcher;
|
|
418
|
+
|
|
419
|
+
// Pre-register consumer state rows so tests can call runOnce() directly
|
|
420
|
+
// without a preceding explicit start(). Timer fires at pollIntervalMs=50
|
|
421
|
+
// but passInFlight serialises concurrent passes — tests that drain via
|
|
422
|
+
// runOnce() remain deterministic. Tests that specifically exercise the
|
|
423
|
+
// timer loop call start() again (idempotent) after setup.
|
|
424
|
+
if (eventDispatcher) await eventDispatcher.ensureRegistered();
|
|
425
|
+
|
|
426
|
+
const http = createRequestHelper(server.app, server.jwt, {
|
|
427
|
+
...(options.authConfig?.sessionCreator !== undefined && {
|
|
428
|
+
sessionCreator: options.authConfig.sessionCreator,
|
|
429
|
+
}),
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
app: server.app,
|
|
434
|
+
jwt: server.jwt,
|
|
435
|
+
registry,
|
|
436
|
+
db: testDb.db,
|
|
437
|
+
redis: testRedis,
|
|
438
|
+
search: searchAdapter,
|
|
439
|
+
events,
|
|
440
|
+
http,
|
|
441
|
+
observability: server.observability,
|
|
442
|
+
sseBroker,
|
|
443
|
+
dispatcher: server.dispatcher,
|
|
444
|
+
...(eventDispatcher ? { eventDispatcher } : {}),
|
|
445
|
+
...(server.lifecycle ? { lifecycle: server.lifecycle } : {}),
|
|
446
|
+
...(jobRunner ? { jobRunner } : {}),
|
|
447
|
+
cleanup: async () => {
|
|
448
|
+
if (jobRunner) await jobRunner.stop();
|
|
449
|
+
if (eventDispatcher) await eventDispatcher.stop();
|
|
450
|
+
await server.observability.shutdown();
|
|
451
|
+
await Promise.all([testDb.cleanup(), testRedis.cleanup()]);
|
|
452
|
+
},
|
|
453
|
+
};
|
|
454
|
+
} catch (error) {
|
|
455
|
+
// Best-effort — a broken jobRunner.stop()/cleanup() must never mask the
|
|
456
|
+
// real setup failure below. testDb/testRedis are created before this
|
|
457
|
+
// try even starts, so a throw anywhere in setup (buildServer,
|
|
458
|
+
// search-config, ...) would otherwise leak their open pg/ioredis
|
|
459
|
+
// sockets — the caller never gets the stack object back to call
|
|
460
|
+
// cleanup() itself, which is the same "hangs on exit" failure class
|
|
461
|
+
// this function exists to prevent.
|
|
462
|
+
if (jobRunner) {
|
|
463
|
+
try {
|
|
464
|
+
await jobRunner.stop();
|
|
465
|
+
} catch {
|
|
466
|
+
// ignore — `error` is the one that matters
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
await testDb.cleanup();
|
|
471
|
+
} catch {
|
|
472
|
+
// ignore — `error` is the one that matters
|
|
473
|
+
}
|
|
474
|
+
try {
|
|
475
|
+
await testRedis.cleanup();
|
|
476
|
+
} catch {
|
|
477
|
+
// ignore — `error` is the one that matters
|
|
478
|
+
}
|
|
479
|
+
throw error;
|
|
480
|
+
}
|
|
363
481
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createLateBoundHolder } from "../late-bound";
|
|
3
|
+
|
|
4
|
+
describe("createLateBoundHolder", () => {
|
|
5
|
+
test("isReady is false and get() throws before set()", () => {
|
|
6
|
+
const holder = createLateBoundHolder<number>("thing");
|
|
7
|
+
expect(holder.isReady()).toBe(false);
|
|
8
|
+
expect(() => holder.get()).toThrow(/thing accessed before set\(\) was called/);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("get() returns the stored value after set()", () => {
|
|
12
|
+
const holder = createLateBoundHolder<{ n: number }>();
|
|
13
|
+
const value = { n: 42 };
|
|
14
|
+
holder.set(value);
|
|
15
|
+
expect(holder.isReady()).toBe(true);
|
|
16
|
+
expect(holder.get()).toBe(value);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("set() can be called again to replace the value", () => {
|
|
20
|
+
const holder = createLateBoundHolder<string>();
|
|
21
|
+
holder.set("first");
|
|
22
|
+
holder.set("second");
|
|
23
|
+
expect(holder.get()).toBe("second");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("set(undefined) still marks the holder ready", () => {
|
|
27
|
+
const holder = createLateBoundHolder<number | undefined>("opt");
|
|
28
|
+
holder.set(undefined);
|
|
29
|
+
expect(holder.isReady()).toBe(true);
|
|
30
|
+
expect(holder.get()).toBeUndefined();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { waitFor } from "../wait-for";
|
|
3
|
+
|
|
4
|
+
describe("waitFor", () => {
|
|
5
|
+
test("returns immediately once fn succeeds on the first attempt", async () => {
|
|
6
|
+
let calls = 0;
|
|
7
|
+
await waitFor(
|
|
8
|
+
() => {
|
|
9
|
+
calls++;
|
|
10
|
+
},
|
|
11
|
+
{ delays: [1, 1, 1] },
|
|
12
|
+
);
|
|
13
|
+
expect(calls).toBe(1);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("retries on failure and succeeds once fn passes", async () => {
|
|
17
|
+
let calls = 0;
|
|
18
|
+
await waitFor(
|
|
19
|
+
() => {
|
|
20
|
+
calls++;
|
|
21
|
+
if (calls < 3) throw new Error(`not yet (${calls})`);
|
|
22
|
+
},
|
|
23
|
+
{ delays: [1, 1, 1] },
|
|
24
|
+
);
|
|
25
|
+
expect(calls).toBe(3);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("throws the last error once every attempt in the schedule fails", async () => {
|
|
29
|
+
let calls = 0;
|
|
30
|
+
await expect(
|
|
31
|
+
waitFor(
|
|
32
|
+
() => {
|
|
33
|
+
calls++;
|
|
34
|
+
throw new Error(`fail-${calls}`);
|
|
35
|
+
},
|
|
36
|
+
{ delays: [1, 1] },
|
|
37
|
+
),
|
|
38
|
+
).rejects.toThrow("fail-2");
|
|
39
|
+
expect(calls).toBe(2);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("throws a descriptive error for an empty delay schedule", async () => {
|
|
43
|
+
await expect(waitFor(() => {}, { delays: [] })).rejects.toThrow(
|
|
44
|
+
"waitFor: empty delay schedule",
|
|
45
|
+
);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("supports an async fn", async () => {
|
|
49
|
+
let calls = 0;
|
|
50
|
+
await waitFor(
|
|
51
|
+
async () => {
|
|
52
|
+
calls++;
|
|
53
|
+
if (calls < 2) throw new Error("not yet");
|
|
54
|
+
},
|
|
55
|
+
{ delays: [1, 1] },
|
|
56
|
+
);
|
|
57
|
+
expect(calls).toBe(2);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { SEARCHABLE_FALSE_WHITELIST } from "../engine/boot-validator/entity-list-screens";
|
|
2
|
+
import { normalizeListColumn } from "../engine/screen-helpers";
|
|
3
|
+
import type {
|
|
4
|
+
EntityDefinition,
|
|
5
|
+
EntityListScreenDefinition,
|
|
6
|
+
FeatureDefinition,
|
|
7
|
+
TranslationEntry,
|
|
8
|
+
} from "../engine/types";
|
|
9
|
+
import { featureHasI18nSurface, requiredKeysFromFeature } from "../i18n/required-surface-keys";
|
|
10
|
+
|
|
11
|
+
function ensureEntityListSortable(feature: FeatureDefinition): FeatureDefinition {
|
|
12
|
+
if (!feature.entities) return feature;
|
|
13
|
+
const entities: Record<string, EntityDefinition> = { ...feature.entities };
|
|
14
|
+
|
|
15
|
+
for (const screen of Object.values(feature.screens)) {
|
|
16
|
+
if (screen.type !== "entityList") continue;
|
|
17
|
+
const entity = entities[screen.entity];
|
|
18
|
+
if (!entity) continue;
|
|
19
|
+
|
|
20
|
+
const hasSortable = Object.values(entity.fields).some(
|
|
21
|
+
(field) => "sortable" in field && field.sortable === true,
|
|
22
|
+
);
|
|
23
|
+
if (hasSortable) continue;
|
|
24
|
+
|
|
25
|
+
const firstCol = screen.columns[0];
|
|
26
|
+
if (firstCol === undefined) continue;
|
|
27
|
+
const fieldName = normalizeListColumn(firstCol).field;
|
|
28
|
+
const fieldDef = entity.fields[fieldName];
|
|
29
|
+
if (fieldDef === undefined) continue;
|
|
30
|
+
|
|
31
|
+
if (fieldDef.type !== "text" && fieldDef.type !== "number") continue;
|
|
32
|
+
|
|
33
|
+
entities[screen.entity] = {
|
|
34
|
+
...entity,
|
|
35
|
+
fields: {
|
|
36
|
+
...entity.fields,
|
|
37
|
+
[fieldName]: { ...fieldDef, sortable: true },
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { ...feature, entities };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function ensureEntityListRowNavigation(
|
|
46
|
+
feature: FeatureDefinition,
|
|
47
|
+
screen: EntityListScreenDefinition,
|
|
48
|
+
): EntityListScreenDefinition {
|
|
49
|
+
const hasEdit = Object.values(feature.screens).some(
|
|
50
|
+
(s) => s.type === "entityEdit" && s.entity === screen.entity,
|
|
51
|
+
);
|
|
52
|
+
if (!hasEdit) return screen;
|
|
53
|
+
const hasNavigate = (screen.rowActions ?? []).some(
|
|
54
|
+
(a) => a.kind === "navigate" && (a.id === "view" || a.id === "edit"),
|
|
55
|
+
);
|
|
56
|
+
if (hasNavigate) return screen;
|
|
57
|
+
const editScreen = Object.values(feature.screens).find(
|
|
58
|
+
(s) => s.type === "entityEdit" && s.entity === screen.entity,
|
|
59
|
+
);
|
|
60
|
+
if (editScreen === undefined) return screen;
|
|
61
|
+
return {
|
|
62
|
+
...screen,
|
|
63
|
+
rowActions: [
|
|
64
|
+
...(screen.rowActions ?? []),
|
|
65
|
+
{
|
|
66
|
+
kind: "navigate",
|
|
67
|
+
id: "edit",
|
|
68
|
+
label: "stub:edit",
|
|
69
|
+
screen: editScreen.id,
|
|
70
|
+
params: { pick: ["id"] },
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function defaultSortForEntityList(
|
|
77
|
+
feature: FeatureDefinition,
|
|
78
|
+
screen: EntityListScreenDefinition,
|
|
79
|
+
): EntityListScreenDefinition {
|
|
80
|
+
if (screen.defaultSort !== undefined || screen.searchable === false) return screen;
|
|
81
|
+
if (SEARCHABLE_FALSE_WHITELIST.has(screen.id)) return screen;
|
|
82
|
+
|
|
83
|
+
const entity = feature.entities?.[screen.entity];
|
|
84
|
+
const sortableColumn = screen.columns.map(normalizeListColumn).find((col) => {
|
|
85
|
+
const fieldDef = entity?.fields[col.field];
|
|
86
|
+
return fieldDef !== undefined && "sortable" in fieldDef && fieldDef.sortable === true;
|
|
87
|
+
});
|
|
88
|
+
if (sortableColumn === undefined) return screen;
|
|
89
|
+
|
|
90
|
+
return { ...screen, defaultSort: { field: sortableColumn.field, dir: "asc" as const } };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function stubTranslations(feature: FeatureDefinition): FeatureDefinition {
|
|
94
|
+
if (!featureHasI18nSurface(feature)) return feature;
|
|
95
|
+
const keys: Record<string, TranslationEntry> = {
|
|
96
|
+
...Object.fromEntries(
|
|
97
|
+
Object.entries(feature.translations ?? {}).map(([key, value]) => [key, { ...value }]),
|
|
98
|
+
),
|
|
99
|
+
};
|
|
100
|
+
for (const key of requiredKeysFromFeature(feature)) {
|
|
101
|
+
if (keys[key] === undefined) keys[key] = { de: "stub", en: "stub" };
|
|
102
|
+
}
|
|
103
|
+
return { ...feature, translations: keys };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Test-fixture helper: satisfy entityList + i18n boot rules without bloating every makeFeature. */
|
|
107
|
+
export function withBootValidatorFixture(
|
|
108
|
+
features: readonly FeatureDefinition[],
|
|
109
|
+
): FeatureDefinition[] {
|
|
110
|
+
return features.map((feature) => {
|
|
111
|
+
const sortable = ensureEntityListSortable(feature);
|
|
112
|
+
const screens = Object.fromEntries(
|
|
113
|
+
Object.entries(sortable.screens).map(([id, screen]) => {
|
|
114
|
+
if (screen.type !== "entityList") return [id, screen];
|
|
115
|
+
const withDefaults = defaultSortForEntityList(sortable, screen);
|
|
116
|
+
return [id, ensureEntityListRowNavigation(sortable, withDefaults)];
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
return stubTranslations({ ...sortable, screens });
|
|
120
|
+
});
|
|
121
|
+
}
|