@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,26 @@
|
|
|
1
|
+
// Screen validation, including the dashboard-panel sub-validators.
|
|
2
|
+
// Dashboard stays here (not a separate dashboard.ts) — validateScreens
|
|
3
|
+
// calls validateDashboardScreen and the dashboard validators call back
|
|
4
|
+
// into validateColumnRendererForm, splitting them would create a
|
|
5
|
+
// same-folder require cycle.
|
|
6
|
+
|
|
1
7
|
import { rowMetaFieldNames } from "../../db/table-builder";
|
|
2
|
-
import { SETTINGS_HUB_AUDIENCE_NAV_QNS } from "../build-config-feature-schema";
|
|
3
8
|
import { qualifyEntityName } from "../qualified-name";
|
|
4
9
|
import { getAllowedFilterOps, isFieldFilterable } from "../screen-filter-ops";
|
|
5
|
-
import
|
|
10
|
+
import { isExtensionEditSection, normalizeEditField, normalizeListColumn } from "../screen-helpers";
|
|
11
|
+
import type { FeatureDefinition } from "../types";
|
|
6
12
|
import type {
|
|
13
|
+
DashboardCustomPanel,
|
|
14
|
+
DashboardFilterDefinition,
|
|
15
|
+
DashboardPanelDefinition,
|
|
16
|
+
DashboardScreenDefinition,
|
|
17
|
+
DashboardStatGroupPanel,
|
|
7
18
|
FieldCondition,
|
|
8
19
|
RowAction,
|
|
9
20
|
RowFieldExtractor,
|
|
10
21
|
ScreenDefinition,
|
|
11
22
|
ToolbarAction,
|
|
12
23
|
} from "../types/screen";
|
|
13
|
-
import { isExtensionEditSection, normalizeEditField, normalizeListColumn } from "../types/screen";
|
|
14
24
|
|
|
15
25
|
// --- Screen validation ---
|
|
16
26
|
//
|
|
@@ -141,6 +151,65 @@ export function validateScreens(
|
|
|
141
151
|
continue;
|
|
142
152
|
}
|
|
143
153
|
|
|
154
|
+
if (screen.type === "projectionList") {
|
|
155
|
+
// Query-getrieben, keine Entity → nur query + columns prüfen (die
|
|
156
|
+
// Column-Felder können nicht gegen eine Entity gecheckt werden; sie
|
|
157
|
+
// werden zur Render-Zeit gegen die Projection-Rows aufgelöst).
|
|
158
|
+
if (!screen.query || typeof screen.query !== "string") {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`[Feature ${feature.name}] Screen "${screenId}" (projectionList) has empty or non-string query.`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (screen.columns.length === 0) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`[Feature ${feature.name}] Screen "${screenId}" (projectionList) has an empty columns list — ` +
|
|
166
|
+
`declare at least one column.`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
for (const col of screen.columns) {
|
|
170
|
+
validateColumnRendererForm(feature.name, screenId, normalizeListColumn(col));
|
|
171
|
+
}
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (screen.type === "projectionDetail") {
|
|
176
|
+
// Query-getrieben wie projectionList, aber Single-Row + Layout statt
|
|
177
|
+
// Columns. Kein Entity-Check möglich — Felder werden render-seitig
|
|
178
|
+
// gegen die Query-Response aufgelöst, nicht gegen eine Entity.
|
|
179
|
+
if (!screen.query || typeof screen.query !== "string") {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) has empty or non-string query.`,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
if (screen.layout.sections.length === 0) {
|
|
185
|
+
throw new Error(
|
|
186
|
+
`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) has an empty sections list — ` +
|
|
187
|
+
`declare at least one section.`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
for (const section of screen.layout.sections) {
|
|
191
|
+
if (isExtensionEditSection(section)) {
|
|
192
|
+
throw new Error(
|
|
193
|
+
`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) extension section ` +
|
|
194
|
+
`"${section.title}" is not supported — projectionDetail has no entity for an extension ` +
|
|
195
|
+
`section to persist against.`,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
if (section.fields.length === 0) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`[Feature ${feature.name}] Screen "${screenId}" (projectionDetail) has a section "${section.title}" ` +
|
|
201
|
+
`with zero fields — drop the section or add fields to it.`,
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (screen.type === "dashboard") {
|
|
209
|
+
validateDashboardScreen(feature.name, screenId, screen);
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
144
213
|
if (screen.type === "configEdit") {
|
|
145
214
|
// configEdit: layout/fields wie actionForm validieren, plus
|
|
146
215
|
// Cross-Check dass jeder qualifizierte Config-Key registriert
|
|
@@ -613,6 +682,148 @@ export function validateScreens(
|
|
|
613
682
|
}
|
|
614
683
|
}
|
|
615
684
|
|
|
685
|
+
// Panel-getrieben, keine Entity — Struktur-Checks (eindeutige Panel-Ids,
|
|
686
|
+
// non-empty Queries/Columns/valueField); die Query-Contracts (Stat-Record,
|
|
687
|
+
// Points-Envelope, Paged-Rows) werden zur Render-Zeit aufgelöst.
|
|
688
|
+
function validateDashboardScreen(
|
|
689
|
+
featureName: string,
|
|
690
|
+
screenId: string,
|
|
691
|
+
screen: DashboardScreenDefinition,
|
|
692
|
+
): void {
|
|
693
|
+
if (screen.panels.length === 0) {
|
|
694
|
+
throw new Error(
|
|
695
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) has an empty panels list — ` +
|
|
696
|
+
`declare at least one panel.`,
|
|
697
|
+
);
|
|
698
|
+
}
|
|
699
|
+
const panelIds = new Set<string>();
|
|
700
|
+
const addPanelId = (id: string, context: string): void => {
|
|
701
|
+
if (panelIds.has(id)) {
|
|
702
|
+
throw new Error(
|
|
703
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) has duplicate panel id "${id}" (${context}).`,
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
panelIds.add(id);
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
for (const panel of screen.panels) {
|
|
710
|
+
addPanelId(panel.id, panel.kind);
|
|
711
|
+
if (panel.kind === "stat-group") {
|
|
712
|
+
validateDashboardStatGroupPanel(featureName, screenId, panel, addPanelId);
|
|
713
|
+
} else if (panel.kind === "custom") {
|
|
714
|
+
validateDashboardCustomPanel(featureName, screenId, panel);
|
|
715
|
+
} else {
|
|
716
|
+
validateDashboardQueryPanel(featureName, screenId, panel);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (screen.filter !== undefined) {
|
|
721
|
+
validateDashboardFilterDefinition(featureName, screenId, screen.filter);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function validateDashboardStatGroupPanel(
|
|
726
|
+
featureName: string,
|
|
727
|
+
screenId: string,
|
|
728
|
+
panel: DashboardStatGroupPanel,
|
|
729
|
+
addPanelId: (id: string, context: string) => void,
|
|
730
|
+
): void {
|
|
731
|
+
if (panel.stats.length === 0) {
|
|
732
|
+
throw new Error(
|
|
733
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}" has an empty stats list.`,
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
for (const stat of panel.stats) {
|
|
737
|
+
addPanelId(stat.id, "stat-group child");
|
|
738
|
+
if (!stat.query || typeof stat.query !== "string") {
|
|
739
|
+
throw new Error(
|
|
740
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}" child "${stat.id}" has empty or non-string query.`,
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
if (stat.valueField.length === 0) {
|
|
744
|
+
throw new Error(
|
|
745
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-group "${panel.id}" child "${stat.id}" has empty valueField.`,
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function validateDashboardCustomPanel(
|
|
752
|
+
featureName: string,
|
|
753
|
+
screenId: string,
|
|
754
|
+
panel: DashboardCustomPanel,
|
|
755
|
+
): void {
|
|
756
|
+
if (panel.component.react === undefined && panel.component.native === undefined) {
|
|
757
|
+
throw new Error(
|
|
758
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) custom-panel "${panel.id}" has no component — ` +
|
|
759
|
+
`declare a react/native component marker.`,
|
|
760
|
+
);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function validateDashboardQueryPanel(
|
|
765
|
+
featureName: string,
|
|
766
|
+
screenId: string,
|
|
767
|
+
panel: Exclude<DashboardPanelDefinition, DashboardStatGroupPanel | DashboardCustomPanel>,
|
|
768
|
+
): void {
|
|
769
|
+
if (!panel.query || typeof panel.query !== "string") {
|
|
770
|
+
throw new Error(
|
|
771
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) panel "${panel.id}" has empty or non-string query.`,
|
|
772
|
+
);
|
|
773
|
+
}
|
|
774
|
+
if (panel.kind === "stat" && panel.valueField.length === 0) {
|
|
775
|
+
throw new Error(
|
|
776
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) stat-panel "${panel.id}" has empty valueField.`,
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
if (panel.kind === "list") {
|
|
780
|
+
if (panel.columns.length === 0) {
|
|
781
|
+
throw new Error(
|
|
782
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) list-panel "${panel.id}" has an empty columns list.`,
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
for (const col of panel.columns) {
|
|
786
|
+
validateColumnRendererForm(featureName, screenId, normalizeListColumn(col));
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function validateDashboardFilterDefinition(
|
|
792
|
+
featureName: string,
|
|
793
|
+
screenId: string,
|
|
794
|
+
filter: DashboardFilterDefinition,
|
|
795
|
+
): void {
|
|
796
|
+
if (filter.id.length === 0) {
|
|
797
|
+
throw new Error(
|
|
798
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) filter has an empty id.`,
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
if (filter.label.length === 0) {
|
|
802
|
+
throw new Error(
|
|
803
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) filter has an empty label.`,
|
|
804
|
+
);
|
|
805
|
+
}
|
|
806
|
+
const hasOptions = filter.options !== undefined;
|
|
807
|
+
const hasOptionsQuery = filter.optionsQuery !== undefined;
|
|
808
|
+
if (hasOptions === hasOptionsQuery) {
|
|
809
|
+
throw new Error(
|
|
810
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) filter must set exactly one of ` +
|
|
811
|
+
`options/optionsQuery.`,
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
if (hasOptions && (filter.options?.length ?? 0) === 0) {
|
|
815
|
+
throw new Error(
|
|
816
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) filter.options is empty — ` +
|
|
817
|
+
`declare at least one option or use optionsQuery instead.`,
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
if (hasOptionsQuery && filter.optionsQuery?.length === 0) {
|
|
821
|
+
throw new Error(
|
|
822
|
+
`[Feature ${featureName}] Screen "${screenId}" (dashboard) filter.optionsQuery is empty.`,
|
|
823
|
+
);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
616
827
|
// Form-check für ListColumn-Renderer in der PlatformComponent-Form
|
|
617
828
|
// (`{ react: { __component: "Name" } }`). Der Server kennt die client-
|
|
618
829
|
// seitige columnRenderers-Map nicht — also nur prüfen ob die Struktur
|
|
@@ -678,13 +889,6 @@ export function buildUnknownFieldMessage(
|
|
|
678
889
|
);
|
|
679
890
|
}
|
|
680
891
|
|
|
681
|
-
// --- Nav validation ---
|
|
682
|
-
//
|
|
683
|
-
// The boot-validator runs BEFORE createRegistry builds the final maps, so we
|
|
684
|
-
// pre-build the qualified name sets for screens + navs here. `qualifyEntityName`
|
|
685
|
-
// is the shared helper with the registry — changing the qualification rule
|
|
686
|
-
// in one place flows through both ingest paths.
|
|
687
|
-
|
|
688
892
|
export function collectScreenQns(features: readonly FeatureDefinition[]): Set<string> {
|
|
689
893
|
const set = new Set<string>();
|
|
690
894
|
for (const f of features) {
|
|
@@ -731,184 +935,3 @@ export function collectScreensByShortId(
|
|
|
731
935
|
}
|
|
732
936
|
return map;
|
|
733
937
|
}
|
|
734
|
-
|
|
735
|
-
// Sammelt alle qualifizierten Write-Handler-QNs (`<feature>:write:<short>`).
|
|
736
|
-
// Wird vom actionForm-Screen-Validator genutzt um zu prüfen ob der
|
|
737
|
-
// im Schema deklarierte handler tatsächlich registriert ist —
|
|
738
|
-
// Tippfehler/umbenannte Handler fallen sonst erst zur Laufzeit auf.
|
|
739
|
-
export function collectWriteHandlerQns(features: readonly FeatureDefinition[]): Set<string> {
|
|
740
|
-
const set = new Set<string>();
|
|
741
|
-
for (const f of features) {
|
|
742
|
-
for (const handlerName of Object.keys(f.writeHandlers)) {
|
|
743
|
-
set.add(qualifyEntityName(f.name, "write", handlerName));
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
return set;
|
|
747
|
-
}
|
|
748
|
-
|
|
749
|
-
export function collectNavQns(
|
|
750
|
-
features: readonly FeatureDefinition[],
|
|
751
|
-
): Map<string, NavDefinition & { readonly featureName: string }> {
|
|
752
|
-
const map = new Map<string, NavDefinition & { readonly featureName: string }>();
|
|
753
|
-
for (const f of features) {
|
|
754
|
-
for (const [navId, navDef] of Object.entries(f.navs)) {
|
|
755
|
-
const qualified = qualifyEntityName(f.name, "nav", navId);
|
|
756
|
-
map.set(qualified, { ...navDef, featureName: f.name });
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
return map;
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
// Per-feature ref validation: screen + parent refs point at real QNs. Cycle
|
|
763
|
-
// detection runs once globally afterwards (it's cheaper to do a single DFS
|
|
764
|
-
// over the merged graph than restart it per feature).
|
|
765
|
-
export function validateNavs(
|
|
766
|
-
feature: FeatureDefinition,
|
|
767
|
-
allScreenQns: ReadonlySet<string>,
|
|
768
|
-
allNavQns: ReadonlyMap<string, NavDefinition & { readonly featureName: string }>,
|
|
769
|
-
allWorkspaceQns: ReadonlyMap<string, WorkspaceDefinition & { readonly featureName: string }>,
|
|
770
|
-
): void {
|
|
771
|
-
for (const [navId, navDef] of Object.entries(feature.navs)) {
|
|
772
|
-
if (navDef.screen !== undefined && !allScreenQns.has(navDef.screen)) {
|
|
773
|
-
throw new Error(
|
|
774
|
-
`[Feature ${feature.name}] Nav entry "${navId}" references screen "${navDef.screen}" ` +
|
|
775
|
-
`which is not registered. Expected a qualified name of the form ` +
|
|
776
|
-
`"<feature>:screen:<id>" pointing at an r.screen() declaration.`,
|
|
777
|
-
);
|
|
778
|
-
}
|
|
779
|
-
if (navDef.parent !== undefined && !allNavQns.has(navDef.parent)) {
|
|
780
|
-
throw new Error(
|
|
781
|
-
`[Feature ${feature.name}] Nav entry "${navId}" references parent "${navDef.parent}" ` +
|
|
782
|
-
`which is not a registered nav entry. Expected a qualified name of the form ` +
|
|
783
|
-
`"<feature>:nav:<id>".`,
|
|
784
|
-
);
|
|
785
|
-
}
|
|
786
|
-
if (navDef.workspaces !== undefined) {
|
|
787
|
-
for (const wsQn of navDef.workspaces) {
|
|
788
|
-
if (!allWorkspaceQns.has(wsQn)) {
|
|
789
|
-
throw new Error(
|
|
790
|
-
`[Feature ${feature.name}] Nav entry "${navId}" self-assigns to workspace "${wsQn}" ` +
|
|
791
|
-
`which is not registered. Expected a qualified name of the form ` +
|
|
792
|
-
`"<feature>:workspace:<id>" pointing at an r.workspace() declaration.`,
|
|
793
|
-
);
|
|
794
|
-
}
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
// Walks parent-refs across ALL nav entries (cross-feature). A cycle here
|
|
801
|
-
// would crash client-side tree assembly — easier to fail loud at boot than
|
|
802
|
-
// to debug a React "Maximum update depth exceeded" stack trace.
|
|
803
|
-
export function validateNavCycles(
|
|
804
|
-
allNavQns: ReadonlyMap<string, NavDefinition & { readonly featureName: string }>,
|
|
805
|
-
): void {
|
|
806
|
-
const visited = new Set<string>();
|
|
807
|
-
const stack = new Set<string>();
|
|
808
|
-
|
|
809
|
-
function visit(qualified: string, path: string[]): void {
|
|
810
|
-
if (stack.has(qualified)) {
|
|
811
|
-
throw new Error(
|
|
812
|
-
`[Kumiko Nav] Nav entry parent cycle detected: ${[...path, qualified].join(" → ")}`,
|
|
813
|
-
);
|
|
814
|
-
}
|
|
815
|
-
// skip: already visited — cycle-detection only needs to traverse each
|
|
816
|
-
// node once, and the `stack` check above catches any actual cycles
|
|
817
|
-
// reached via a different path.
|
|
818
|
-
if (visited.has(qualified)) return;
|
|
819
|
-
visited.add(qualified);
|
|
820
|
-
stack.add(qualified);
|
|
821
|
-
const navDef = allNavQns.get(qualified);
|
|
822
|
-
if (navDef?.parent) {
|
|
823
|
-
visit(navDef.parent, [...path, qualified]);
|
|
824
|
-
}
|
|
825
|
-
stack.delete(qualified);
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
for (const qualified of allNavQns.keys()) {
|
|
829
|
-
visit(qualified, []);
|
|
830
|
-
}
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
// Roles we recognise at boot time. The framework has no explicit
|
|
834
|
-
// role-registry (r.defineRoles is a type helper only), so we synthesise
|
|
835
|
-
// one from every handler-access rule plus the "all"/"system" built-ins.
|
|
836
|
-
export function collectKnownRoles(features: readonly FeatureDefinition[]): Set<string> {
|
|
837
|
-
const roles = new Set<string>(["all", "system"]);
|
|
838
|
-
for (const f of features) {
|
|
839
|
-
for (const def of Object.values(f.writeHandlers)) {
|
|
840
|
-
if (def.access && "roles" in def.access) {
|
|
841
|
-
for (const r of def.access.roles) roles.add(r);
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
for (const def of Object.values(f.queryHandlers)) {
|
|
845
|
-
if (def.access && "roles" in def.access) {
|
|
846
|
-
for (const r of def.access.roles) roles.add(r);
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
return roles;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// --- Workspace validation ---
|
|
854
|
-
//
|
|
855
|
-
// Per-app workspace registry, built once up front. Carries `featureName`
|
|
856
|
-
// alongside the definition so error messages can point at the offending
|
|
857
|
-
// feature without a parallel reverse index.
|
|
858
|
-
|
|
859
|
-
export function collectWorkspaceQns(
|
|
860
|
-
features: readonly FeatureDefinition[],
|
|
861
|
-
): Map<string, WorkspaceDefinition & { readonly featureName: string }> {
|
|
862
|
-
const map = new Map<string, WorkspaceDefinition & { readonly featureName: string }>();
|
|
863
|
-
for (const f of features) {
|
|
864
|
-
for (const [wsId, wsDef] of Object.entries(f.workspaces)) {
|
|
865
|
-
const qualified = qualifyEntityName(f.name, "workspace", wsId);
|
|
866
|
-
map.set(qualified, { ...wsDef, featureName: f.name });
|
|
867
|
-
}
|
|
868
|
-
}
|
|
869
|
-
return map;
|
|
870
|
-
}
|
|
871
|
-
|
|
872
|
-
export function validateWorkspaces(
|
|
873
|
-
feature: FeatureDefinition,
|
|
874
|
-
allNavQns: ReadonlyMap<string, NavDefinition & { readonly featureName: string }>,
|
|
875
|
-
): void {
|
|
876
|
-
for (const [wsId, wsDef] of Object.entries(feature.workspaces)) {
|
|
877
|
-
if (wsDef.nav !== undefined) {
|
|
878
|
-
for (const navQn of wsDef.nav) {
|
|
879
|
-
// Settings-Hub audience navs are generated post-boot (buildAppSchema), never via r.nav() — exempt so an inline-placement reference doesn't trip the boot validator.
|
|
880
|
-
if (SETTINGS_HUB_AUDIENCE_NAV_QN_SET.has(navQn)) continue;
|
|
881
|
-
if (!allNavQns.has(navQn)) {
|
|
882
|
-
throw new Error(
|
|
883
|
-
`[Feature ${feature.name}] Workspace "${wsId}" references nav "${navQn}" ` +
|
|
884
|
-
`which is not registered. Expected a qualified name of the form ` +
|
|
885
|
-
`"<feature>:nav:<id>" pointing at an r.nav() declaration.`,
|
|
886
|
-
);
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
const SETTINGS_HUB_AUDIENCE_NAV_QN_SET: ReadonlySet<string> = new Set(
|
|
894
|
-
SETTINGS_HUB_AUDIENCE_NAV_QNS,
|
|
895
|
-
);
|
|
896
|
-
|
|
897
|
-
// Single-default rule across the entire app. Mirrors how createApp validates
|
|
898
|
-
// roles up front — a second `default: true` is a configuration error, not a
|
|
899
|
-
// runtime fallback. Apps without any default fall back to "first workspace
|
|
900
|
-
// the user has access to" at render time (handled by shellWorkspaces).
|
|
901
|
-
export function validateDefaultWorkspaceUniqueness(
|
|
902
|
-
allWorkspaceQns: ReadonlyMap<string, WorkspaceDefinition & { readonly featureName: string }>,
|
|
903
|
-
): void {
|
|
904
|
-
const defaults: string[] = [];
|
|
905
|
-
for (const [qn, ws] of allWorkspaceQns) {
|
|
906
|
-
if (ws.default === true) defaults.push(qn);
|
|
907
|
-
}
|
|
908
|
-
if (defaults.length > 1) {
|
|
909
|
-
throw new Error(
|
|
910
|
-
`[Kumiko Workspaces] Multiple workspaces declare default: true — ` +
|
|
911
|
-
`${defaults.join(", ")}. At most one workspace per app may be the default.`,
|
|
912
|
-
);
|
|
913
|
-
}
|
|
914
|
-
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Workspace validation.
|
|
2
|
+
|
|
3
|
+
import { SETTINGS_HUB_AUDIENCE_NAV_QNS } from "../build-config-feature-schema";
|
|
4
|
+
import { qualifyEntityName } from "../qualified-name";
|
|
5
|
+
import type { FeatureDefinition, NavDefinition, WorkspaceDefinition } from "../types";
|
|
6
|
+
|
|
7
|
+
// --- Workspace validation ---
|
|
8
|
+
//
|
|
9
|
+
// Per-app workspace registry, built once up front. Carries `featureName`
|
|
10
|
+
// alongside the definition so error messages can point at the offending
|
|
11
|
+
// feature without a parallel reverse index.
|
|
12
|
+
|
|
13
|
+
export function collectWorkspaceQns(
|
|
14
|
+
features: readonly FeatureDefinition[],
|
|
15
|
+
): Map<string, WorkspaceDefinition & { readonly featureName: string }> {
|
|
16
|
+
const map = new Map<string, WorkspaceDefinition & { readonly featureName: string }>();
|
|
17
|
+
for (const f of features) {
|
|
18
|
+
for (const [wsId, wsDef] of Object.entries(f.workspaces)) {
|
|
19
|
+
const qualified = qualifyEntityName(f.name, "workspace", wsId);
|
|
20
|
+
map.set(qualified, { ...wsDef, featureName: f.name });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return map;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function validateWorkspaces(
|
|
27
|
+
feature: FeatureDefinition,
|
|
28
|
+
allNavQns: ReadonlyMap<string, NavDefinition & { readonly featureName: string }>,
|
|
29
|
+
): void {
|
|
30
|
+
for (const [wsId, wsDef] of Object.entries(feature.workspaces)) {
|
|
31
|
+
if (wsDef.nav !== undefined) {
|
|
32
|
+
for (const navQn of wsDef.nav) {
|
|
33
|
+
// Settings-Hub audience navs are generated post-boot (buildAppSchema), never via r.nav() — exempt so an inline-placement reference doesn't trip the boot validator.
|
|
34
|
+
if (SETTINGS_HUB_AUDIENCE_NAV_QN_SET.has(navQn)) continue;
|
|
35
|
+
if (!allNavQns.has(navQn)) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`[Feature ${feature.name}] Workspace "${wsId}" references nav "${navQn}" ` +
|
|
38
|
+
`which is not registered. Expected a qualified name of the form ` +
|
|
39
|
+
`"<feature>:nav:<id>" pointing at an r.nav() declaration.`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const SETTINGS_HUB_AUDIENCE_NAV_QN_SET: ReadonlySet<string> = new Set(
|
|
48
|
+
SETTINGS_HUB_AUDIENCE_NAV_QNS,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// Single-default rule across the entire app. Mirrors how createApp validates
|
|
52
|
+
// roles up front — a second `default: true` is a configuration error, not a
|
|
53
|
+
// runtime fallback. Apps without any default fall back to "first workspace
|
|
54
|
+
// the user has access to" at render time (handled by shellWorkspaces).
|
|
55
|
+
export function validateDefaultWorkspaceUniqueness(
|
|
56
|
+
allWorkspaceQns: ReadonlyMap<string, WorkspaceDefinition & { readonly featureName: string }>,
|
|
57
|
+
): void {
|
|
58
|
+
const defaults: string[] = [];
|
|
59
|
+
for (const [qn, ws] of allWorkspaceQns) {
|
|
60
|
+
if (ws.default === true) defaults.push(qn);
|
|
61
|
+
}
|
|
62
|
+
if (defaults.length > 1) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`[Kumiko Workspaces] Multiple workspaces declare default: true — ` +
|
|
65
|
+
`${defaults.join(", ")}. At most one workspace per app may be the default.`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
// sind plain literals)
|
|
18
18
|
// Nav: verbatim (NavDefinition ist nur strings + literals)
|
|
19
19
|
// Workspace: verbatim definition + getWorkspaceNavs() von der Registry
|
|
20
|
+
// Translations: verbatim feature.translations (raw r.translations keys,
|
|
21
|
+
// unfiltered — see FeatureSchema.translations doc)
|
|
20
22
|
//
|
|
21
23
|
// Feature-Toggles: BISHER NICHT GEFILTERT. Wenn ein Feature über die
|
|
22
24
|
// feature-toggles-bundled-feature global deaktiviert ist, erscheint es
|
|
@@ -49,6 +51,12 @@ export function buildAppSchema(registry: Registry, options: BuildAppSchemaOption
|
|
|
49
51
|
entities: projectEntities(feature.entities ?? {}),
|
|
50
52
|
screens: Object.values(feature.screens),
|
|
51
53
|
...(navs.length > 0 && { navs }),
|
|
54
|
+
// #1059: verbatim r.translations({keys}) — see FeatureSchema.translations
|
|
55
|
+
// doc for why this must NOT go through registry.getAllTranslations()
|
|
56
|
+
// (double-prefixes features that already qualify their own keys).
|
|
57
|
+
...(Object.keys(feature.translations ?? {}).length > 0 && {
|
|
58
|
+
translations: feature.translations,
|
|
59
|
+
}),
|
|
52
60
|
};
|
|
53
61
|
features.push(featureSchema);
|
|
54
62
|
}
|
|
@@ -340,9 +348,17 @@ function projectField(fieldDef: FieldDefinition): FieldDefinition {
|
|
|
340
348
|
// filterable steuert die Faceted-Filter-Dropdowns im Renderer (select/
|
|
341
349
|
// boolean) — muss daher ins Client-Schema.
|
|
342
350
|
if (typeof def["filterable"] === "boolean") out["filterable"] = def["filterable"];
|
|
351
|
+
if (typeof def["searchable"] === "boolean") out["searchable"] = def["searchable"];
|
|
343
352
|
if (isLiteral(def["default"])) out["default"] = def["default"];
|
|
344
353
|
// Select: options-Liste ist plain JSON, durchschicken.
|
|
345
354
|
if (Array.isArray(def["options"])) out["options"] = def["options"];
|
|
355
|
+
// Reference: entity-Target + labelField + multiple müssen zum Renderer.
|
|
356
|
+
// Der ReferenceInput baut die Options-Query aus refEntity/refFeature und
|
|
357
|
+
// resolved das Label über labelField — ohne diese Properties fällt das
|
|
358
|
+
// Dropdown leer aus (QN wird `<feature>:query::list` → 404).
|
|
359
|
+
if (typeof def["entity"] === "string") out["entity"] = def["entity"];
|
|
360
|
+
if (typeof def["labelField"] === "string") out["labelField"] = def["labelField"];
|
|
361
|
+
if (typeof def["multiple"] === "boolean") out["multiple"] = def["multiple"];
|
|
346
362
|
return out as FieldDefinition; // @cast-boundary schema-walk
|
|
347
363
|
}
|
|
348
364
|
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
createSelectField,
|
|
24
24
|
createTextField,
|
|
25
25
|
} from "./factories";
|
|
26
|
+
import { isKebabSegment } from "./qualified-name";
|
|
26
27
|
import type { ConfigKeyDefinition } from "./types/config";
|
|
27
28
|
import type { Registry } from "./types/feature";
|
|
28
29
|
import type { FieldDefinition } from "./types/fields";
|
|
@@ -56,6 +57,11 @@ export type ConfigFeatureSchema = {
|
|
|
56
57
|
|
|
57
58
|
// Audience-Reihenfolge im Sidebar: Plattform vor Tenant vor Benutzer.
|
|
58
59
|
const SCOPE_ORDER: Record<ConfigScope, number> = { system: 10, tenant: 20, user: 30 };
|
|
60
|
+
const SCOPE_ICON: Record<ConfigScope, string> = {
|
|
61
|
+
system: "shield",
|
|
62
|
+
tenant: "building",
|
|
63
|
+
user: "user",
|
|
64
|
+
};
|
|
59
65
|
const SCOPES_BROAD_TO_DEEP: readonly ConfigScope[] = ["system", "tenant", "user"];
|
|
60
66
|
|
|
61
67
|
const audienceNavShortId = (scope: ConfigScope): string => `audience-${scope}`;
|
|
@@ -85,7 +91,14 @@ const MACHINE_WRITE_ROLE = "system" as const;
|
|
|
85
91
|
|
|
86
92
|
type MaskedKey = {
|
|
87
93
|
readonly qn: string;
|
|
94
|
+
// Effective group this key is bucketed under for the Settings-Hub —
|
|
95
|
+
// `def.group` if set, else `ownerFeature`. This is what screen/nav
|
|
96
|
+
// generation groups by.
|
|
88
97
|
readonly feature: string;
|
|
98
|
+
// The feature that actually declared this key (always derived from `qn`).
|
|
99
|
+
// Needed to disambiguate field ids when `feature !== ownerFeature` (a
|
|
100
|
+
// cross-feature `group`) and for collision error messages.
|
|
101
|
+
readonly ownerFeature: string;
|
|
89
102
|
readonly shortKey: string;
|
|
90
103
|
readonly def: ConfigKeyDefinition;
|
|
91
104
|
};
|
|
@@ -107,6 +120,7 @@ export function buildConfigFeatureSchema(registry: Registry): ConfigFeatureSchem
|
|
|
107
120
|
navs.push({
|
|
108
121
|
id: audienceNavShortId(scope),
|
|
109
122
|
label: `config.settings.${scope}`,
|
|
123
|
+
icon: SCOPE_ICON[scope],
|
|
110
124
|
order: SCOPE_ORDER[scope],
|
|
111
125
|
access: rolesToAccess(visible.flatMap((v) => v.roles)),
|
|
112
126
|
});
|
|
@@ -123,6 +137,7 @@ export function buildConfigFeatureSchema(registry: Registry): ConfigFeatureSchem
|
|
|
123
137
|
label: `${feature}.settings`,
|
|
124
138
|
parent: audienceNavShortId(scope),
|
|
125
139
|
screen: shortId,
|
|
140
|
+
icon: ordered[0]?.def.mask?.icon ?? "settings",
|
|
126
141
|
order: minMaskOrder(ordered),
|
|
127
142
|
access,
|
|
128
143
|
});
|
|
@@ -190,15 +205,30 @@ function buildScreen(
|
|
|
190
205
|
const configKeys: Record<string, string> = {};
|
|
191
206
|
const fields: Record<string, FieldDefinition> = {};
|
|
192
207
|
const fieldLabels: Record<string, string> = {};
|
|
208
|
+
// Field id collapses to the plain shortKey when the key stays in its own
|
|
209
|
+
// feature's group (100% of today's apps — byte-identical output). Only a
|
|
210
|
+
// cross-feature `group` (feature !== ownerFeature) needs the owner prefix,
|
|
211
|
+
// to keep two features sharing a group from clobbering the same shortKey.
|
|
212
|
+
const fieldId = (k: MaskedKey): string =>
|
|
213
|
+
k.feature === k.ownerFeature ? k.shortKey : `${k.ownerFeature}-${k.shortKey}`;
|
|
214
|
+
const seenFieldIds = new Map<string, string>();
|
|
193
215
|
for (const k of keys) {
|
|
194
|
-
|
|
195
|
-
|
|
216
|
+
const id = fieldId(k);
|
|
217
|
+
const prevQn = seenFieldIds.get(id);
|
|
218
|
+
if (prevQn !== undefined) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
`[Settings-Hub] group "${feature}" scope "${scope}": config keys "${prevQn}" and "${k.qn}" both resolve to field id "${id}" — rename one key or its group.`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
seenFieldIds.set(id, k.qn);
|
|
224
|
+
configKeys[id] = k.qn;
|
|
225
|
+
fields[id] = deriveField(k.def);
|
|
196
226
|
// mask is the visibility gate, so collectMaskedKeys guarantees it here.
|
|
197
|
-
if (k.def.mask) fieldLabels[
|
|
227
|
+
if (k.def.mask) fieldLabels[id] = k.def.mask.title;
|
|
198
228
|
}
|
|
199
229
|
const section: EditFieldsSection = {
|
|
200
230
|
title: `${feature}.settings`,
|
|
201
|
-
fields: keys.map(
|
|
231
|
+
fields: keys.map(fieldId),
|
|
202
232
|
};
|
|
203
233
|
return {
|
|
204
234
|
id: shortId,
|
|
@@ -230,14 +260,21 @@ function deriveField(def: ConfigKeyDefinition): FieldDefinition {
|
|
|
230
260
|
function collectMaskedKeys(registry: Registry): MaskedKey[] {
|
|
231
261
|
const out: MaskedKey[] = [];
|
|
232
262
|
for (const [qn, def] of registry.getAllConfigKeys()) {
|
|
263
|
+
const sep = qn.indexOf(":config:");
|
|
264
|
+
if (sep === -1) continue;
|
|
265
|
+
const ownerFeature = qn.slice(0, sep);
|
|
266
|
+
if (def.group !== undefined && !isKebabSegment(def.group)) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`[Feature ${ownerFeature}] config key "${qn.slice(sep + ":config:".length)}" has invalid group "${def.group}" — must be kebab-case.`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
233
271
|
// computed keys derive their value — there is no row to set, so a
|
|
234
272
|
// configEdit screen could not write them. Skip even when masked.
|
|
235
273
|
if (def.mask === undefined || def.computed !== undefined) continue;
|
|
236
|
-
const sep = qn.indexOf(":config:");
|
|
237
|
-
if (sep === -1) continue;
|
|
238
274
|
out.push({
|
|
239
275
|
qn,
|
|
240
|
-
feature:
|
|
276
|
+
feature: def.group ?? ownerFeature,
|
|
277
|
+
ownerFeature,
|
|
241
278
|
shortKey: qn.slice(sep + ":config:".length),
|
|
242
279
|
def,
|
|
243
280
|
});
|