@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
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
import { createReadStream, createWriteStream } from "node:fs";
|
|
2
2
|
import { mkdir, readFile, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
3
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
4
4
|
import { pipeline } from "node:stream/promises";
|
|
5
|
-
import type
|
|
5
|
+
import { assertSafeStorageKey, type FileStorageProvider } from "./types";
|
|
6
6
|
|
|
7
7
|
// Local-filesystem backend — intended for dev + tests. Production deploys
|
|
8
8
|
// pick an object-store provider (S3/R2/…). mimeType is ignored here; the
|
|
9
9
|
// filesystem tracks no metadata beyond what the caller stores on FileRef.
|
|
10
10
|
export function createLocalProvider(basePath: string): FileStorageProvider {
|
|
11
|
+
const resolvedBase = resolve(basePath);
|
|
12
|
+
|
|
13
|
+
// Callers are expected to only ever pass keys built by buildStorageKey(),
|
|
14
|
+
// but the storage layer shouldn't rely solely on that — this resolves the
|
|
15
|
+
// real path and rejects anything that escapes basePath (e.g. a `..`
|
|
16
|
+
// segment that slipped past an upstream check), regardless of the key's
|
|
17
|
+
// source.
|
|
18
|
+
function resolveContainedPath(key: string): string {
|
|
19
|
+
assertSafeStorageKey(key);
|
|
20
|
+
// assertSafeStorageKey rejects every literal "."/".." segment, which
|
|
21
|
+
// covers all string-based traversal a join+resolve can produce; this
|
|
22
|
+
// startsWith check is a second, independent line of defense in case a
|
|
23
|
+
// future caller feeds resolveContainedPath something pre-resolved.
|
|
24
|
+
const filePath = resolve(join(basePath, key));
|
|
25
|
+
if (filePath !== resolvedBase && !filePath.startsWith(resolvedBase + sep)) {
|
|
26
|
+
throw new Error(`storage key escapes basePath: "${key}"`);
|
|
27
|
+
}
|
|
28
|
+
return filePath;
|
|
29
|
+
}
|
|
11
30
|
return {
|
|
12
31
|
async write(key: string, data: Uint8Array, _mimeType?: string): Promise<void> {
|
|
13
|
-
const filePath =
|
|
32
|
+
const filePath = resolveContainedPath(key);
|
|
14
33
|
await mkdir(dirname(filePath), { recursive: true });
|
|
15
34
|
await writeFile(filePath, data);
|
|
16
35
|
},
|
|
@@ -26,7 +45,7 @@ export function createLocalProvider(basePath: string): FileStorageProvider {
|
|
|
26
45
|
// Hygiene; ein periodischer cron-cleanup auf alten `.tmp`-Files
|
|
27
46
|
// ist die saubere Loesung wenn das in Production realistisch
|
|
28
47
|
// greift.
|
|
29
|
-
const filePath =
|
|
48
|
+
const filePath = resolveContainedPath(key);
|
|
30
49
|
const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
31
50
|
await mkdir(dirname(filePath), { recursive: true });
|
|
32
51
|
try {
|
|
@@ -41,7 +60,7 @@ export function createLocalProvider(basePath: string): FileStorageProvider {
|
|
|
41
60
|
},
|
|
42
61
|
|
|
43
62
|
async read(key: string): Promise<Uint8Array> {
|
|
44
|
-
const filePath =
|
|
63
|
+
const filePath = resolveContainedPath(key);
|
|
45
64
|
return readFile(filePath);
|
|
46
65
|
},
|
|
47
66
|
|
|
@@ -55,7 +74,7 @@ export function createLocalProvider(basePath: string): FileStorageProvider {
|
|
|
55
74
|
// (z.B. ENOENT bei Missing-File faellt erst beim ersten chunk-
|
|
56
75
|
// pull, nicht beim readStream-Aufruf — gleiches Lazy-Verhalten
|
|
57
76
|
// wie inmemory + S3).
|
|
58
|
-
const filePath =
|
|
77
|
+
const filePath = resolveContainedPath(key);
|
|
59
78
|
const stream = createReadStream(filePath);
|
|
60
79
|
return {
|
|
61
80
|
async *[Symbol.asyncIterator]() {
|
|
@@ -76,13 +95,13 @@ export function createLocalProvider(basePath: string): FileStorageProvider {
|
|
|
76
95
|
},
|
|
77
96
|
|
|
78
97
|
async delete(key: string): Promise<void> {
|
|
79
|
-
const filePath =
|
|
98
|
+
const filePath = resolveContainedPath(key);
|
|
80
99
|
await rm(filePath, { force: true });
|
|
81
100
|
},
|
|
82
101
|
|
|
83
102
|
async exists(key: string): Promise<boolean> {
|
|
84
103
|
try {
|
|
85
|
-
await stat(
|
|
104
|
+
await stat(resolveContainedPath(key));
|
|
86
105
|
return true;
|
|
87
106
|
} catch {
|
|
88
107
|
return false;
|
|
@@ -12,11 +12,12 @@
|
|
|
12
12
|
// file-foundation re-exports `createFileProviderForTenant` + the plugin types
|
|
13
13
|
// (moved here from there) so existing imports keep working.
|
|
14
14
|
|
|
15
|
+
import type { FileProviderResolver } from "@cosmicdrift/kumiko-types/file-provider-resolver-types";
|
|
15
16
|
import type { DbConnection } from "../db/connection";
|
|
16
17
|
import type { TenantDb } from "../db/tenant-db";
|
|
17
18
|
import { EXT_FILE_PROVIDER, FILE_PROVIDER_CONFIG_KEY } from "../engine/extension-names";
|
|
18
19
|
import { SYSTEM_USER_ID } from "../engine/system-user";
|
|
19
|
-
import type { ConfigAccessor, ConfigAccessorFactory, Registry
|
|
20
|
+
import type { ConfigAccessor, ConfigAccessorFactory, Registry } from "../engine/types";
|
|
20
21
|
import type { SecretsContext } from "../secrets";
|
|
21
22
|
import type { FileStorageProvider } from "./types";
|
|
22
23
|
|
|
@@ -120,10 +121,7 @@ export async function createFileProviderForTenant(
|
|
|
120
121
|
return usage.options.build(ctx, tenantId);
|
|
121
122
|
}
|
|
122
123
|
|
|
123
|
-
|
|
124
|
-
// (tenantId is the call argument) — the single spine shared by upload routes,
|
|
125
|
-
// ctx.files and the GDPR jobs.
|
|
126
|
-
export type FileProviderResolver = (tenantId: TenantId) => Promise<FileStorageProvider>;
|
|
124
|
+
export type { FileProviderResolver } from "@cosmicdrift/kumiko-types/file-provider-resolver-types";
|
|
127
125
|
|
|
128
126
|
export type FileProviderResolverDeps = {
|
|
129
127
|
readonly registry?: Registry;
|
|
@@ -138,21 +136,34 @@ export type FileProviderResolverDeps = {
|
|
|
138
136
|
// SYSTEM identity: provider construction is an infra read, distinct from the
|
|
139
137
|
// request user's authorization which stays at the route accessGuard.
|
|
140
138
|
export function makeFileProviderResolver(deps: FileProviderResolverDeps): FileProviderResolver {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
139
|
+
// Per-tenant cache (698/2) — without it, every resolveProvider(tenantId)
|
|
140
|
+
// call re-reads config + secrets from the DB, even though the provider is
|
|
141
|
+
// effectively static per tenant for the process lifetime. A rejected
|
|
142
|
+
// promise is evicted so a transient failure (or a secret genuinely not
|
|
143
|
+
// configured yet) doesn't permanently poison this tenant's entry.
|
|
144
|
+
const cache = new Map<string, Promise<FileStorageProvider>>();
|
|
145
|
+
return (tenantId) => {
|
|
146
|
+
const cached = cache.get(tenantId);
|
|
147
|
+
if (cached) return cached;
|
|
148
|
+
const built = (async () => {
|
|
149
|
+
if (!deps.registry || !deps._configAccessorFactory || !deps.db) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
"makeFileProviderResolver: registry/_configAccessorFactory/db missing — " +
|
|
152
|
+
"mount the config + file-foundation features and wire context.db",
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const config = deps._configAccessorFactory({
|
|
156
|
+
user: { id: SYSTEM_USER_ID, tenantId },
|
|
157
|
+
db: deps.db,
|
|
158
|
+
secrets: deps.secrets,
|
|
159
|
+
});
|
|
160
|
+
return createFileProviderForTenant(
|
|
161
|
+
{ config, registry: deps.registry, secrets: deps.secrets, _userId: SYSTEM_USER_ID },
|
|
162
|
+
tenantId,
|
|
146
163
|
);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
secrets: deps.secrets,
|
|
152
|
-
});
|
|
153
|
-
return createFileProviderForTenant(
|
|
154
|
-
{ config, registry: deps.registry, secrets: deps.secrets, _userId: SYSTEM_USER_ID },
|
|
155
|
-
tenantId,
|
|
156
|
-
);
|
|
164
|
+
})();
|
|
165
|
+
built.catch(() => cache.delete(tenantId));
|
|
166
|
+
cache.set(tenantId, built);
|
|
167
|
+
return built;
|
|
157
168
|
};
|
|
158
169
|
}
|
package/src/files/types.ts
CHANGED
|
@@ -6,60 +6,11 @@ export type FileMetadata = {
|
|
|
6
6
|
readonly size: number;
|
|
7
7
|
};
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
readonly contentDisposition?: string;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
// Options fuer `writeStream`. `mimeType` ist Content-Type-Hint analog zu
|
|
18
|
-
// `write`. `contentLength` ist optional fuer Provider die einen Length-
|
|
19
|
-
// Header brauchen (S3 multipart hat einen TransferManager, kann auch ohne
|
|
20
|
-
// length); local-Provider ignoriert beides.
|
|
21
|
-
export type WriteStreamOptions = {
|
|
22
|
-
readonly mimeType?: string;
|
|
23
|
-
readonly contentLength?: number;
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
// Primitive storage contract: key+bytes in, bytes out. Metadata (fileName,
|
|
27
|
-
// mimeType, size) lives on the FileRef row — the provider only needs to
|
|
28
|
-
// shuttle bytes. `mimeType` on write() is a hint for providers that need a
|
|
29
|
-
// Content-Type header (S3/R2/…); local filesystems can ignore it.
|
|
30
|
-
//
|
|
31
|
-
// **Streaming (`writeStream` + `readStream`) ist PFLICHT** — beide
|
|
32
|
-
// Methoden sind required, kein optional-feature. Begruendung:
|
|
33
|
-
// - User-Data-Export (Atom 3c) braucht beide, sonst silent fail bei
|
|
34
|
-
// erstem Job mit fileRefs in Production.
|
|
35
|
-
// - Apps die nur kleine Files (Avatar-Uploads, Profile-Pics) handeln,
|
|
36
|
-
// koennen trivial via `oneShot`-Pattern den Stream-Contract erfuellen
|
|
37
|
-
// (single-chunk yield von write/read-Bytes). 5 Zeilen pro Provider.
|
|
38
|
-
// - Optional-Type wuerde TypeScript-Lying erlauben: Type sagt "kann
|
|
39
|
-
// fehlen", Worker throws zur Runtime → App-Authors sehen den Bug
|
|
40
|
-
// erst in Production. Required + TS-enforced ist ehrlich.
|
|
41
|
-
//
|
|
42
|
-
// `getSignedUrl` BLEIBT optional: object-store backends (S3/R2/GCS)
|
|
43
|
-
// implement it so clients can download directly from the provider after
|
|
44
|
-
// the server has checked access — offloads bandwidth and enables browser-
|
|
45
|
-
// native caching. Filesystem providers leave it undefined; the route then
|
|
46
|
-
// returns 501 and the client falls back to streaming via GET /files/:id.
|
|
47
|
-
// Callers must feature-detect via `typeof provider.getSignedUrl === "function"`.
|
|
48
|
-
// Hier ist Optional korrekt weil die Fallback-Pfad existiert — kein
|
|
49
|
-
// silent-fail, sondern 501 + alternativer download.
|
|
50
|
-
export type FileStorageProvider = {
|
|
51
|
-
write(key: string, data: Uint8Array, mimeType?: string): Promise<void>;
|
|
52
|
-
writeStream(
|
|
53
|
-
key: string,
|
|
54
|
-
source: AsyncIterable<Uint8Array>,
|
|
55
|
-
options?: WriteStreamOptions,
|
|
56
|
-
): Promise<void>;
|
|
57
|
-
read(key: string): Promise<Uint8Array>;
|
|
58
|
-
readStream(key: string): AsyncIterable<Uint8Array>;
|
|
59
|
-
delete(key: string): Promise<void>;
|
|
60
|
-
exists(key: string): Promise<boolean>;
|
|
61
|
-
getSignedUrl?(key: string, expiresInSeconds: number, options?: SignedUrlOptions): Promise<string>;
|
|
62
|
-
};
|
|
9
|
+
export type {
|
|
10
|
+
FileStorageProvider,
|
|
11
|
+
SignedUrlOptions,
|
|
12
|
+
WriteStreamOptions,
|
|
13
|
+
} from "@cosmicdrift/kumiko-types/file-storage-provider-types";
|
|
63
14
|
|
|
64
15
|
export type FileValidationOptions = {
|
|
65
16
|
readonly maxSize?: string | undefined;
|
|
@@ -133,6 +84,12 @@ export function validateFile(
|
|
|
133
84
|
return null;
|
|
134
85
|
}
|
|
135
86
|
|
|
87
|
+
export function assertSafeStorageKey(key: string): void {
|
|
88
|
+
if (key.split("/").some((segment) => segment === "." || segment === "..")) {
|
|
89
|
+
throw new Error(`storage key contains a path-traversal segment: "${key}"`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
136
93
|
export function buildStorageKey(
|
|
137
94
|
tenantId: TenantId,
|
|
138
95
|
entityType: string,
|
|
@@ -141,6 +98,7 @@ export function buildStorageKey(
|
|
|
141
98
|
fileName: string,
|
|
142
99
|
uniqueId: string,
|
|
143
100
|
): string {
|
|
144
|
-
const
|
|
101
|
+
const rawExt = fileName.split(".").pop() ?? "";
|
|
102
|
+
const ext = /^[A-Za-z0-9]+$/.test(rawExt) ? rawExt.toLowerCase() : "bin";
|
|
145
103
|
return `${tenantId}/${entityType}/${entityId}/${fieldName}/${uniqueId}.${ext}`;
|
|
146
104
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { booleanFacetOptionKeys, selectFacetOptionKey } from "../required-surface-keys";
|
|
3
|
+
|
|
4
|
+
describe("required-surface-keys helpers", () => {
|
|
5
|
+
test("booleanFacetOptionKeys emits true/false option keys", () => {
|
|
6
|
+
expect(booleanFacetOptionKeys("tenant", "tenant", "isEnabled")).toEqual([
|
|
7
|
+
"tenant:entity:tenant:field:isEnabled:option:true",
|
|
8
|
+
"tenant:entity:tenant:field:isEnabled:option:false",
|
|
9
|
+
]);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("selectFacetOptionKey encodes option value", () => {
|
|
13
|
+
expect(selectFacetOptionKey("user", "user", "status", "active")).toBe(
|
|
14
|
+
"user:entity:user:field:status:option:active",
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -1,16 +1,21 @@
|
|
|
1
|
+
import { isExtensionEditSection, normalizeListColumn } from "../engine/screen-helpers";
|
|
1
2
|
import type {
|
|
2
3
|
ActionFormScreenDefinition,
|
|
3
4
|
ConfigEditScreenDefinition,
|
|
5
|
+
DashboardFilterDefinition,
|
|
6
|
+
DashboardPanelDefinition,
|
|
7
|
+
DashboardScreenDefinition,
|
|
4
8
|
EntityEditScreenDefinition,
|
|
5
9
|
EntityListScreenDefinition,
|
|
6
10
|
FeatureDefinition,
|
|
7
11
|
NavDefinition,
|
|
12
|
+
ProjectionDetailScreenDefinition,
|
|
13
|
+
ProjectionListScreenDefinition,
|
|
8
14
|
RowAction,
|
|
9
15
|
ScreenDefinition,
|
|
10
16
|
ToolbarAction,
|
|
11
17
|
WorkspaceDefinition,
|
|
12
18
|
} from "../engine/types";
|
|
13
|
-
import { isExtensionEditSection, normalizeListColumn } from "../engine/types/screen";
|
|
14
19
|
|
|
15
20
|
/** Pseudo-entity for actionForm field labels (renderer action-form-shim). */
|
|
16
21
|
export const ACTION_FORM_ENTITY = "__action-form__";
|
|
@@ -18,10 +23,31 @@ export const ACTION_FORM_ENTITY = "__action-form__";
|
|
|
18
23
|
/** Pseudo-entity for configEdit field labels (renderer config-edit-shim). */
|
|
19
24
|
export const CONFIG_EDIT_ENTITY = "__config-edit__";
|
|
20
25
|
|
|
26
|
+
/** Pseudo-entity for projectionDetail field labels (renderer projection-detail-shim). */
|
|
27
|
+
export const PROJECTION_DETAIL_ENTITY = "__projection-detail__";
|
|
28
|
+
|
|
21
29
|
export function fieldLabelKey(featureName: string, entityName: string, fieldName: string): string {
|
|
22
30
|
return `${featureName}:entity:${entityName}:field:${fieldName}`;
|
|
23
31
|
}
|
|
24
32
|
|
|
33
|
+
export function booleanFacetOptionKeys(
|
|
34
|
+
featureName: string,
|
|
35
|
+
entityName: string,
|
|
36
|
+
fieldName: string,
|
|
37
|
+
): readonly string[] {
|
|
38
|
+
const base = `${featureName}:entity:${entityName}:field:${fieldName}:option`;
|
|
39
|
+
return [`${base}:true`, `${base}:false`];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function selectFacetOptionKey(
|
|
43
|
+
featureName: string,
|
|
44
|
+
entityName: string,
|
|
45
|
+
fieldName: string,
|
|
46
|
+
value: string,
|
|
47
|
+
): string {
|
|
48
|
+
return `${featureName}:entity:${entityName}:field:${fieldName}:option:${value}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
25
51
|
export function screenTitleKey(screenId: string): string {
|
|
26
52
|
return `screen:${screenId}.title`;
|
|
27
53
|
}
|
|
@@ -46,6 +72,34 @@ function pushRowActionKeys(out: Set<string>, action: RowAction): void {
|
|
|
46
72
|
}
|
|
47
73
|
}
|
|
48
74
|
|
|
75
|
+
function pushDashboardScreenKeys(out: Set<string>, dashboard: DashboardScreenDefinition): void {
|
|
76
|
+
for (const panel of dashboard.panels) pushDashboardPanelKeys(out, panel);
|
|
77
|
+
if (dashboard.filter !== undefined) pushDashboardFilterKeys(out, dashboard.filter);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function pushDashboardPanelKeys(out: Set<string>, panel: DashboardPanelDefinition): void {
|
|
81
|
+
// skip: custom-Panel übersetzt sich selbst, kein Key hier
|
|
82
|
+
if (panel.kind === "custom") return;
|
|
83
|
+
pushKey(out, panel.label);
|
|
84
|
+
if (panel.kind === "stat-group") {
|
|
85
|
+
for (const stat of panel.stats) pushKey(out, stat.label);
|
|
86
|
+
}
|
|
87
|
+
if (panel.kind === "list") {
|
|
88
|
+
for (const col of panel.columns) {
|
|
89
|
+
const normalized = normalizeListColumn(col);
|
|
90
|
+
if (normalized.label !== undefined) pushKey(out, normalized.label);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (panel.kind === "feed" && panel.emptyLabel !== undefined) pushKey(out, panel.emptyLabel);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function pushDashboardFilterKeys(out: Set<string>, filter: DashboardFilterDefinition): void {
|
|
97
|
+
pushKey(out, filter.label);
|
|
98
|
+
if (filter.allLabel !== undefined) pushKey(out, filter.allLabel);
|
|
99
|
+
if (filter.placeholder !== undefined) pushKey(out, filter.placeholder);
|
|
100
|
+
for (const opt of filter.options ?? []) pushKey(out, opt.label);
|
|
101
|
+
}
|
|
102
|
+
|
|
49
103
|
function pushToolbarActionKeys(out: Set<string>, action: ToolbarAction): void {
|
|
50
104
|
pushKey(out, action.label);
|
|
51
105
|
if (action.kind === "writeHandler") {
|
|
@@ -76,6 +130,21 @@ export function requiredKeysFromScreen(
|
|
|
76
130
|
for (const action of list.toolbarActions ?? []) pushToolbarActionKeys(out, action);
|
|
77
131
|
break;
|
|
78
132
|
}
|
|
133
|
+
case "projectionList": {
|
|
134
|
+
const list = screen as ProjectionListScreenDefinition;
|
|
135
|
+
// Keine Entity → keine field-label-Fallbacks; nur explizite Column-Labels.
|
|
136
|
+
for (const col of list.columns) {
|
|
137
|
+
const normalized = normalizeListColumn(col);
|
|
138
|
+
if (normalized.label !== undefined) pushKey(out, normalized.label);
|
|
139
|
+
}
|
|
140
|
+
for (const action of list.rowActions ?? []) pushRowActionKeys(out, action);
|
|
141
|
+
for (const action of list.toolbarActions ?? []) pushToolbarActionKeys(out, action);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case "dashboard": {
|
|
145
|
+
pushDashboardScreenKeys(out, screen as DashboardScreenDefinition);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
79
148
|
case "entityEdit": {
|
|
80
149
|
const edit = screen as EntityEditScreenDefinition;
|
|
81
150
|
pushKey(out, edit.submitLabel);
|
|
@@ -136,6 +205,20 @@ export function requiredKeysFromScreen(
|
|
|
136
205
|
}
|
|
137
206
|
break;
|
|
138
207
|
}
|
|
208
|
+
case "projectionDetail": {
|
|
209
|
+
const detail = screen as ProjectionDetailScreenDefinition;
|
|
210
|
+
for (const section of detail.layout.sections) {
|
|
211
|
+
if (isExtensionEditSection(section)) continue; // rejected at boot, unreachable here
|
|
212
|
+
pushKey(out, section.title);
|
|
213
|
+
for (const f of section.fields) {
|
|
214
|
+
const fieldName = editFieldName(f);
|
|
215
|
+
const override = detail.fieldLabels?.[fieldName];
|
|
216
|
+
if (override !== undefined) pushKey(out, override);
|
|
217
|
+
else out.add(fieldLabelKey(featureName, PROJECTION_DETAIL_ENTITY, fieldName));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
139
222
|
case "custom":
|
|
140
223
|
break;
|
|
141
224
|
}
|
|
@@ -155,6 +238,41 @@ export function requiredKeysFromWorkspace(ws: WorkspaceDefinition): readonly str
|
|
|
155
238
|
return [...out];
|
|
156
239
|
}
|
|
157
240
|
|
|
241
|
+
function collectEntityListFilterKeys(feature: FeatureDefinition, out: Set<string>): void {
|
|
242
|
+
for (const screen of Object.values(feature.screens)) {
|
|
243
|
+
if (screen.type !== "entityList") continue;
|
|
244
|
+
const entity = feature.entities?.[screen.entity];
|
|
245
|
+
if (!entity) continue;
|
|
246
|
+
for (const [fieldName, rawDef] of Object.entries(entity.fields)) {
|
|
247
|
+
const def = rawDef as {
|
|
248
|
+
readonly filterable?: boolean;
|
|
249
|
+
readonly type?: string;
|
|
250
|
+
readonly options?: readonly string[];
|
|
251
|
+
};
|
|
252
|
+
if (def.filterable !== true) continue;
|
|
253
|
+
if (def.type === "boolean") {
|
|
254
|
+
for (const key of booleanFacetOptionKeys(feature.name, screen.entity, fieldName)) {
|
|
255
|
+
out.add(key);
|
|
256
|
+
}
|
|
257
|
+
} else if (def.type === "select" && Array.isArray(def.options)) {
|
|
258
|
+
for (const value of def.options) {
|
|
259
|
+
out.add(selectFacetOptionKey(feature.name, screen.entity, fieldName, value));
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function featureHasI18nSurface(feature: FeatureDefinition): boolean {
|
|
267
|
+
if (Object.keys(feature.screens).length > 0) return true;
|
|
268
|
+
if (Object.keys(feature.navs).length > 0) return true;
|
|
269
|
+
if (Object.keys(feature.workspaces).length > 0) return true;
|
|
270
|
+
for (const def of Object.values(feature.configKeys)) {
|
|
271
|
+
if (def.mask !== undefined) return true;
|
|
272
|
+
}
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
158
276
|
export function requiredKeysFromFeature(feature: FeatureDefinition): readonly string[] {
|
|
159
277
|
const out = new Set<string>();
|
|
160
278
|
|
|
@@ -170,6 +288,7 @@ export function requiredKeysFromFeature(feature: FeatureDefinition): readonly st
|
|
|
170
288
|
for (const def of Object.values(feature.configKeys)) {
|
|
171
289
|
pushKey(out, def.mask?.title);
|
|
172
290
|
}
|
|
291
|
+
collectEntityListFilterKeys(feature, out);
|
|
173
292
|
|
|
174
293
|
return [...out];
|
|
175
294
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { createRegistry, defineFeature } from "../../engine";
|
|
3
|
+
import type { AppContext, Registry } from "../../engine/types";
|
|
4
|
+
import { createPrometheusMeter, registerStandardMetrics } from "../../observability";
|
|
5
|
+
import { createTestRedis, type TestRedis } from "../../stack";
|
|
6
|
+
import { sleep } from "../../testing";
|
|
7
|
+
import { createJobRunner } from "../job-runner";
|
|
8
|
+
|
|
9
|
+
let testRedis: TestRedis;
|
|
10
|
+
let redisUrl: string;
|
|
11
|
+
|
|
12
|
+
const testFeature = defineFeature("test-queue-depth", (r) => {
|
|
13
|
+
r.job("noop", { trigger: { manual: true } }, async () => {});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
beforeAll(async () => {
|
|
17
|
+
testRedis = await createTestRedis();
|
|
18
|
+
redisUrl = `redis://${testRedis.redis.options.host}:${testRedis.redis.options.port}/${testRedis.redis.options.db}`;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterAll(async () => {
|
|
22
|
+
await testRedis.cleanup();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("job-runner — kumiko_job_queue_depth", () => {
|
|
26
|
+
test("start() polls BullMQ counts into the gauge, stop() tears the poller down", async () => {
|
|
27
|
+
const registry: Registry = createRegistry([testFeature]);
|
|
28
|
+
const meter = createPrometheusMeter();
|
|
29
|
+
registerStandardMetrics(meter);
|
|
30
|
+
const context: AppContext = { meter };
|
|
31
|
+
const queueNamePrefix = `kumiko-test-qd-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
32
|
+
|
|
33
|
+
const runner = createJobRunner({
|
|
34
|
+
registry,
|
|
35
|
+
context,
|
|
36
|
+
redisUrl,
|
|
37
|
+
consumerLane: "worker",
|
|
38
|
+
queueNamePrefix,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
await runner.start();
|
|
43
|
+
|
|
44
|
+
const snapshot = meter.snapshot().get("kumiko_job_queue_depth");
|
|
45
|
+
expect(snapshot).toBeDefined();
|
|
46
|
+
const waitingSlot = snapshot?.slots.find(
|
|
47
|
+
(s) => s.labels?.["lane"] === "worker" && s.labels?.["state"] === "waiting",
|
|
48
|
+
);
|
|
49
|
+
expect(waitingSlot).toBeDefined();
|
|
50
|
+
expect((waitingSlot as { value: number }).value).toBe(0);
|
|
51
|
+
// BullMQ's fresh Worker connection is still settling right after
|
|
52
|
+
// start() returns (no boot/cron job here to have already warmed it
|
|
53
|
+
// up, unlike every other scenario in this suite) — stop() immediately
|
|
54
|
+
// after start() races the Worker's own connection teardown and throws
|
|
55
|
+
// an unrelated "Connection is closed" from ioredis. Production runners
|
|
56
|
+
// live far longer than this; the race is a test-only artifact of
|
|
57
|
+
// start()+stop() with zero work in between.
|
|
58
|
+
await sleep(50);
|
|
59
|
+
} finally {
|
|
60
|
+
await runner.stop();
|
|
61
|
+
const keys = await testRedis.redis.keys(`bull:${queueNamePrefix}-worker:*`);
|
|
62
|
+
if (keys.length > 0) await testRedis.redis.del(...keys);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("enqueuer-only runner (no consumerLane) never polls — no gauge slots", async () => {
|
|
67
|
+
const registry: Registry = createRegistry([testFeature]);
|
|
68
|
+
const meter = createPrometheusMeter();
|
|
69
|
+
registerStandardMetrics(meter);
|
|
70
|
+
const context: AppContext = { meter };
|
|
71
|
+
const queueNamePrefix = `kumiko-test-qd-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
72
|
+
|
|
73
|
+
const runner = createJobRunner({ registry, context, redisUrl, queueNamePrefix });
|
|
74
|
+
try {
|
|
75
|
+
await runner.start();
|
|
76
|
+
const snapshot = meter.snapshot().get("kumiko_job_queue_depth");
|
|
77
|
+
expect(snapshot?.slots.length ?? 0).toBe(0);
|
|
78
|
+
} finally {
|
|
79
|
+
await runner.stop();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|