@cosmicdrift/kumiko-framework 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -38
- package/package.json +21 -2
- package/src/__tests__/anonymous-access.integration.test.ts +185 -0
- package/src/__tests__/consumer-cli.integration.test.ts +172 -0
- package/src/__tests__/entity-permalink-open.integration.test.ts +94 -0
- package/src/__tests__/full-stack.integration.test.ts +1 -1
- package/src/__tests__/schema-cli-temporal-polyfill.test.ts +48 -0
- package/src/__tests__/schema-cli.integration.test.ts +50 -0
- package/src/__tests__/store-table.integration.test.ts +93 -0
- package/src/api/__tests__/api.test.ts +373 -0
- package/src/api/__tests__/auth-middleware-anonymous-access-boot.test.ts +40 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +17 -1
- package/src/api/__tests__/auth-routes-invalid-body-invite.test.ts +253 -0
- package/src/api/__tests__/auth-routes-mfa-preauth-confirm.test.ts +222 -0
- package/src/api/__tests__/auth-routes-mfa-preauth-enable-start.test.ts +249 -0
- package/src/api/__tests__/auth-routes-mfa-verify.test.ts +202 -0
- package/src/api/__tests__/auth-routes-trusted-proxy.test.ts +135 -0
- package/src/api/__tests__/batch.integration.test.ts +101 -11
- package/src/api/__tests__/csrf-constants-sync.test.ts +20 -0
- package/src/api/__tests__/dispatcher-live.integration.test.ts +74 -0
- package/src/api/__tests__/jwt.test.ts +200 -1
- package/src/api/__tests__/login-rate-limiter-sweep.test.ts +50 -0
- package/src/api/__tests__/pat-scope.test.ts +36 -0
- package/src/api/__tests__/pii-leak-guard.integration.test.ts +103 -0
- package/src/api/__tests__/redis-login-rate-limiter.integration.test.ts +138 -0
- package/src/api/__tests__/request-id-middleware.test.ts +51 -0
- package/src/api/__tests__/server-boot-guards.test.ts +71 -0
- package/src/api/__tests__/server-jwt-ttl.test.ts +58 -0
- package/src/api/__tests__/sse-broker.test.ts +57 -0
- package/src/api/__tests__/sse-route.test.ts +4 -0
- package/src/api/api-constants.ts +11 -0
- package/src/api/auth-middleware.ts +248 -40
- package/src/api/auth-routes.ts +576 -95
- package/src/api/index.ts +13 -4
- package/src/api/jwt.ts +170 -11
- package/src/api/pat-scope.ts +14 -0
- package/src/api/pii-leak-guard.ts +47 -0
- package/src/api/request-context.ts +3 -0
- package/src/api/request-id-middleware.ts +2 -0
- package/src/api/routes.ts +169 -2
- package/src/api/server.ts +112 -16
- package/src/api/sse-broker.ts +39 -0
- package/src/bun-db/__tests__/PATTERN.md +0 -1
- package/src/bun-db/__tests__/coerce-row-temporal.test.ts +41 -0
- package/src/bun-db/__tests__/select-many-retry.test.ts +79 -0
- package/src/bun-db/__tests__/write-brand.test.ts +21 -0
- package/src/bun-db/connection.ts +3 -3
- package/src/bun-db/index.ts +2 -0
- package/src/bun-db/query.ts +105 -32
- package/src/consumer-cli.ts +134 -0
- package/src/crypto/__tests__/blind-index.test.ts +130 -0
- package/src/crypto/__tests__/event-pii.test.ts +161 -0
- package/src/crypto/__tests__/kek-rotation.integration.test.ts +180 -0
- package/src/crypto/__tests__/kms-adapter-contract.ts +134 -0
- package/src/crypto/__tests__/kms-adapter.contract.test.ts +4 -0
- package/src/crypto/__tests__/pg-kms-adapter.integration.test.ts +117 -0
- package/src/crypto/__tests__/pii-field-encryption.test.ts +376 -0
- package/src/crypto/__tests__/request-kms-cache.test.ts +74 -0
- package/src/crypto/__tests__/subject-resolver.test.ts +108 -0
- package/src/crypto/blind-index.ts +118 -0
- package/src/crypto/event-pii.ts +70 -0
- package/src/crypto/in-memory-kms-adapter.ts +50 -0
- package/src/crypto/index.ts +67 -0
- package/src/crypto/kms-adapter.ts +2 -0
- package/src/crypto/pg-kms-adapter.ts +295 -0
- package/src/crypto/pii-field-encryption.ts +248 -0
- package/src/crypto/request-kms-cache.ts +38 -0
- package/src/crypto/subject-resolver.ts +91 -0
- package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +191 -0
- package/src/db/__tests__/blind-index.integration.test.ts +248 -0
- package/src/db/__tests__/build-filter-where.test.ts +34 -0
- package/src/db/__tests__/collect-table-metas.test.ts +10 -10
- package/src/db/__tests__/config-seed.integration.test.ts +13 -5
- package/src/db/__tests__/dialect-instant.test.ts +1 -4
- package/src/db/__tests__/entity-field-encryption.test.ts +7 -7
- package/src/db/__tests__/entity-table-meta-source.test.ts +50 -0
- package/src/db/__tests__/event-store-executor-context.pii-roundtrip.test.ts +67 -0
- package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +402 -0
- package/src/db/__tests__/event-store-executor.integration.test.ts +299 -29
- package/src/db/__tests__/feature-table-sources.test.ts +34 -0
- package/src/db/__tests__/implicit-projection-equivalence.integration.test.ts +118 -39
- package/src/db/__tests__/instant-to-driver-temporal.test.ts +21 -0
- package/src/db/__tests__/located-timestamp.test.ts +19 -0
- package/src/db/__tests__/migrate-generator.test.ts +21 -0
- package/src/db/__tests__/migrate-runner.test.ts +61 -0
- package/src/db/__tests__/number-field-fractional.integration.test.ts +58 -0
- package/src/db/__tests__/rebuild-marker.test.ts +13 -3
- package/src/db/__tests__/replay-migration-sql.test.ts +384 -0
- package/src/db/__tests__/schema-inspection.test.ts +7 -0
- package/src/db/__tests__/schema-migration.integration.test.ts +1 -1
- package/src/db/__tests__/table-builder-meta-lockstep.test.ts +39 -0
- package/src/db/__tests__/tenant-db-where-merge.test.ts +49 -3
- package/src/db/__tests__/tenant-db.integration.test.ts +6 -2
- package/src/db/api.ts +2 -2
- package/src/db/apply-entity-event.ts +18 -14
- package/src/db/blind-index-cleanup.ts +55 -0
- package/src/db/bun-provider.ts +2 -2
- package/src/db/collect-table-metas.ts +6 -7
- package/src/db/config-seed.ts +9 -9
- package/src/db/connection.ts +7 -12
- package/src/db/cursor.ts +1 -18
- package/src/db/dialect.ts +20 -26
- package/src/db/entity-field-encryption.ts +81 -24
- package/src/db/entity-table-meta-types.ts +2 -0
- package/src/db/entity-table-meta.ts +63 -90
- package/src/db/event-store-executor-context.ts +404 -0
- package/src/db/event-store-executor-read.ts +275 -0
- package/src/db/event-store-executor-write.ts +644 -0
- package/src/db/event-store-executor.ts +28 -1155
- package/src/db/feature-table-sources.ts +6 -5
- package/src/db/index.ts +20 -3
- package/src/db/located-timestamp.ts +4 -0
- package/src/db/migrate-generator.ts +39 -6
- package/src/db/migrate-runner.ts +107 -11
- package/src/db/pg-error.ts +9 -1
- package/src/db/postgres-provider.ts +2 -2
- package/src/db/queries/__tests__/event-store-idempotency-index.integration.test.ts +80 -0
- package/src/db/queries/backfill-pii.ts +277 -0
- package/src/db/queries/ddl.ts +45 -0
- package/src/db/queries/event-consumer.ts +35 -2
- package/src/db/queries/event-store.ts +126 -19
- package/src/db/queries/projection-rebuild.ts +22 -8
- package/src/db/queries/shadow-swap.ts +183 -0
- package/src/db/queries/test-stack.ts +4 -30
- package/src/db/query-api.ts +1 -0
- package/src/db/query.ts +1 -0
- package/src/db/rebuild-marker.ts +18 -2
- package/src/db/reference-data.ts +2 -3
- package/src/db/replay-migration-sql.ts +257 -0
- package/src/db/schema-inspection.ts +1 -1
- package/src/db/table-builder.ts +102 -71
- package/src/db/tenant-db.ts +15 -53
- package/src/engine/__tests__/boot-validator-action-wiring.test.ts +242 -0
- package/src/engine/__tests__/boot-validator-boot-check.test.ts +99 -0
- package/src/engine/__tests__/boot-validator-dashboard.test.ts +237 -0
- package/src/engine/__tests__/boot-validator-entity-list.test.ts +104 -0
- package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +7 -69
- package/src/engine/__tests__/boot-validator-i18n-keys.test.ts +26 -5
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +3 -19
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +246 -4
- package/src/engine/__tests__/boot-validator.test.ts +219 -15
- package/src/engine/__tests__/build-app-schema.test.ts +82 -0
- package/src/engine/__tests__/build-config-feature-schema.test.ts +125 -0
- package/src/engine/__tests__/build-target.test.ts +3 -11
- package/src/engine/__tests__/codemod-pipeline.test.ts +152 -21
- package/src/engine/__tests__/config-helpers.test.ts +16 -0
- package/src/engine/__tests__/define-feature-entity-mapping.test.ts +6 -0
- package/src/engine/__tests__/define-roles.test.ts +21 -0
- package/src/engine/__tests__/engine.test.ts +163 -1
- package/src/engine/__tests__/entity-handlers.test.ts +80 -0
- package/src/engine/__tests__/event-migration-declarative.test.ts +65 -0
- package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
- package/src/engine/__tests__/extend-entity-projection.test.ts +123 -0
- package/src/engine/__tests__/factories-time.test.ts +2 -66
- package/src/engine/__tests__/feature-crud-shorthand.test.ts +28 -0
- package/src/engine/__tests__/feature-manifest.test.ts +31 -1
- package/src/engine/__tests__/field-access.test.ts +23 -1
- package/src/engine/__tests__/hook-phases.test.ts +5 -5
- package/src/engine/__tests__/membership-roles.test.ts +4 -10
- package/src/engine/__tests__/nav.test.ts +86 -1
- package/src/engine/__tests__/pipeline-engine.test.ts +9 -9
- package/src/engine/__tests__/pipeline-handler.integration.test.ts +18 -18
- package/src/engine/__tests__/pipeline-observability.integration.test.ts +2 -2
- package/src/engine/__tests__/pipeline-performance.integration.test.ts +3 -3
- package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +9 -9
- package/src/engine/__tests__/post-query-hook.test.ts +7 -7
- package/src/engine/__tests__/registrar-object-form.test.ts +141 -0
- package/src/engine/__tests__/registry.test.ts +144 -0
- package/src/engine/__tests__/schema-builder.test.ts +18 -0
- package/src/engine/__tests__/screen.test.ts +147 -1
- package/src/engine/__tests__/soft-delete-cleanup.test.ts +3 -0
- package/src/engine/__tests__/store-table.test.ts +229 -0
- package/src/engine/__tests__/tier-resolver-extension.test.ts +19 -1
- package/src/engine/__tests__/{visual-tree-patterns.test.ts → tree-actions-patterns.test.ts} +7 -95
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +15 -15
- package/src/engine/boot-validator/__tests__/config-deps.test.ts +92 -0
- package/src/engine/boot-validator/action-wiring.ts +149 -0
- package/src/engine/boot-validator/boot-check.ts +21 -0
- package/src/engine/boot-validator/config-deps.ts +40 -4
- package/src/engine/boot-validator/entity-handler.ts +20 -21
- package/src/engine/boot-validator/entity-list-screens.ts +88 -0
- package/src/engine/boot-validator/gdpr-storage.ts +0 -20
- package/src/engine/boot-validator/i18n-keys.ts +58 -4
- package/src/engine/boot-validator/index.ts +33 -12
- package/src/engine/boot-validator/nav.ts +125 -0
- package/src/engine/boot-validator/pii-retention.ts +95 -10
- package/src/engine/boot-validator/{screens-nav.ts → screens.ts} +214 -191
- package/src/engine/boot-validator/workspaces.ts +68 -0
- package/src/engine/build-app-schema.ts +16 -0
- package/src/engine/build-config-feature-schema.ts +44 -7
- package/src/engine/codemod/pipeline-codemod.ts +5 -5
- package/src/engine/config-helpers.ts +15 -0
- package/src/engine/constants.ts +32 -6
- package/src/engine/create-app.ts +11 -0
- package/src/engine/define-feature.ts +95 -947
- package/src/engine/define-handler.ts +28 -94
- package/src/engine/define-workflow.ts +1 -1
- package/src/engine/effective-features.ts +12 -2
- package/src/engine/entity-handlers.ts +76 -11
- package/src/engine/extensions/tenant-data.ts +19 -0
- package/src/engine/extensions/user-data.ts +29 -2
- package/src/engine/factories.ts +8 -49
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +26 -29
- package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/feature.ts +9 -0
- package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/screens.ts +4 -0
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -2
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +18 -8
- package/src/engine/feature-ast/__tests__/parse.test.ts +1640 -160
- package/src/engine/feature-ast/__tests__/patch.test.ts +206 -12
- package/src/engine/feature-ast/__tests__/patcher.test.ts +20 -23
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +380 -5
- package/src/engine/feature-ast/extractors/events.ts +322 -0
- package/src/engine/feature-ast/extractors/handlers.ts +234 -0
- package/src/engine/feature-ast/extractors/hooks.ts +243 -0
- package/src/engine/feature-ast/extractors/index.ts +34 -31
- package/src/engine/feature-ast/extractors/jobs-routes.ts +221 -0
- package/src/engine/feature-ast/extractors/projections-screens.ts +269 -0
- package/src/engine/feature-ast/extractors/round1.ts +3 -3
- package/src/engine/feature-ast/extractors/round5.ts +3 -3
- package/src/engine/feature-ast/extractors/round6.ts +2 -36
- package/src/engine/feature-ast/extractors/shared.ts +64 -6
- package/src/engine/feature-ast/index.ts +2 -4
- package/src/engine/feature-ast/parse.ts +130 -23
- package/src/engine/feature-ast/patch.ts +39 -50
- package/src/engine/feature-ast/patcher.ts +37 -38
- package/src/engine/feature-ast/patterns.ts +50 -62
- package/src/engine/feature-ast/render.ts +67 -44
- package/src/engine/feature-builder-state.ts +168 -0
- package/src/engine/feature-config-events-jobs.ts +403 -0
- package/src/engine/feature-entity-handlers.ts +208 -0
- package/src/engine/feature-manifest.ts +2 -1
- package/src/engine/feature-ui-extensions.ts +500 -0
- package/src/engine/field-access.ts +13 -2
- package/src/engine/field-helpers.ts +31 -0
- package/src/engine/handler-helpers.ts +26 -0
- package/src/engine/hook-helpers.ts +16 -0
- package/src/engine/index.ts +23 -7
- package/src/engine/membership-roles.ts +13 -0
- package/src/engine/object-form.ts +27 -0
- package/src/engine/ownership.ts +26 -79
- package/src/engine/pattern-library/__tests__/library.test.ts +7 -20
- package/src/engine/pattern-library/library.ts +44 -1152
- package/src/engine/pattern-library/mixed-schemas.ts +450 -0
- package/src/engine/pattern-library/opaque-schemas.ts +124 -0
- package/src/engine/pattern-library/shared-fields.ts +75 -0
- package/src/engine/pattern-library/static-schemas.ts +456 -0
- package/src/engine/pipeline.ts +6 -11
- package/src/engine/registry-facade.ts +362 -0
- package/src/engine/registry-ingest.ts +478 -0
- package/src/engine/registry-state.ts +388 -0
- package/src/engine/registry-validate.ts +642 -0
- package/src/engine/registry.ts +78 -1658
- package/src/engine/run-pipeline.ts +1 -1
- package/src/engine/schema-builder.ts +1 -0
- package/src/engine/screen-helpers.ts +54 -0
- package/src/engine/soft-delete-cleanup.ts +6 -2
- package/src/engine/steps/__tests__/duration-utils.test.ts +20 -0
- package/src/engine/steps/_duration-utils.ts +2 -0
- package/src/engine/steps/unsafe-projection-upsert.ts +1 -4
- package/src/engine/tier-resolver-extension.ts +3 -2
- package/src/engine/types/config.ts +2 -482
- package/src/engine/types/define-handler.ts +2 -0
- package/src/engine/types/entity-handlers.ts +2 -0
- package/src/engine/types/event-type-map.ts +1 -37
- package/src/engine/types/feature.ts +2 -972
- package/src/engine/types/fields.ts +2 -675
- package/src/engine/types/handlers.ts +2 -774
- package/src/engine/types/hooks.ts +2 -184
- package/src/engine/types/http-route.ts +1 -54
- package/src/engine/types/identifiers.ts +1 -47
- package/src/engine/types/index.ts +86 -40
- package/src/engine/types/nav.ts +2 -63
- package/src/engine/types/ownership.ts +2 -0
- package/src/engine/types/projection.ts +2 -138
- package/src/engine/types/relations.ts +1 -51
- package/src/engine/types/screen.ts +2 -574
- package/src/engine/types/step.ts +2 -334
- package/src/engine/types/target-ref.ts +1 -21
- package/src/engine/types/tree-node.ts +1 -132
- package/src/engine/types/workspace.ts +2 -49
- package/src/engine/validate-projection-allowlist.ts +6 -6
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.test.ts +120 -1
- package/src/entrypoint/index.ts +49 -6
- package/src/errors/classes.ts +22 -1
- package/src/errors/field-issue.ts +0 -3
- package/src/errors/index.ts +1 -1
- package/src/errors/write-error-info.ts +10 -22
- package/src/es-ops/README.md +1 -1
- package/src/es-ops/__tests__/runner.integration.test.ts +74 -0
- package/src/es-ops/context.ts +4 -2
- package/src/es-ops/types.ts +8 -1
- package/src/event-store/__tests__/admin-api.integration.test.ts +27 -1
- package/src/event-store/__tests__/backfill-pii.integration.test.ts +279 -0
- package/src/event-store/__tests__/event-store.integration.test.ts +201 -0
- package/src/event-store/__tests__/row-to-stored-event.test.ts +2 -2
- package/src/event-store/__tests__/snapshot.integration.test.ts +86 -0
- package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +58 -0
- package/src/event-store/__tests__/upcaster.integration.test.ts +77 -45
- package/src/event-store/admin-api.ts +11 -4
- package/src/event-store/errors.ts +2 -35
- package/src/event-store/event-store.ts +64 -78
- package/src/event-store/events-schema.ts +11 -13
- package/src/event-store/index.ts +18 -3
- package/src/event-store/rebuild-dead-letter.ts +111 -0
- package/src/event-store/snapshot.ts +63 -40
- package/src/event-store/types.ts +2 -0
- package/src/files/__tests__/build-storage-key.test.ts +28 -0
- package/src/files/__tests__/file-ref-entity.test.ts +8 -0
- package/src/files/__tests__/files.integration.test.ts +24 -0
- package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
- package/src/files/__tests__/local-provider.test.ts +31 -0
- package/src/files/__tests__/provider-resolver.test.ts +70 -0
- package/src/files/__tests__/write-stream.test.ts +13 -0
- package/src/files/file-handle.ts +2 -19
- package/src/files/file-ref-entity.ts +2 -2
- package/src/files/file-routes.ts +13 -0
- package/src/files/index.ts +1 -1
- package/src/files/local-provider.ts +27 -8
- package/src/files/provider-resolver.ts +31 -20
- package/src/files/types.ts +13 -55
- package/src/i18n/__tests__/required-surface-keys.test.ts +17 -0
- package/src/i18n/required-surface-keys.ts +120 -1
- package/src/jobs/__tests__/job-queue-depth.integration.test.ts +82 -0
- package/src/jobs/__tests__/jobs.integration.test.ts +329 -3
- package/src/jobs/job-runner.ts +82 -12
- package/src/logging/types.ts +1 -7
- package/src/migrations/pending-rebuilds.ts +1 -1
- package/src/observability/__tests__/observability.integration.test.ts +4 -1
- package/src/observability/__tests__/recording-tracer.test.ts +6 -4
- package/src/observability/index.ts +2 -0
- package/src/observability/noop-provider.ts +0 -9
- package/src/observability/recording-tracer.ts +0 -12
- package/src/observability/standard-metrics.ts +64 -2
- package/src/observability/types/index.ts +1 -29
- package/src/observability/types/metric.ts +1 -56
- package/src/observability/types/provider.ts +1 -32
- package/src/observability/types/span.ts +1 -64
- package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +2 -2
- package/src/pipeline/__tests__/dispatcher.test.ts +366 -1
- package/src/pipeline/__tests__/event-consumer-state.integration.test.ts +31 -0
- package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +126 -0
- package/src/pipeline/__tests__/event-dispatcher-rearm.integration.test.ts +263 -0
- package/src/pipeline/__tests__/event-dispatcher.integration.test.ts +10 -0
- package/src/pipeline/__tests__/job-trigger-consumer.integration.test.ts +106 -0
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +308 -77
- package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +16 -8
- package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
- package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +80 -2
- package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +274 -0
- package/src/pipeline/__tests__/try-append-event.integration.test.ts +166 -0
- package/src/pipeline/dispatch-batch.ts +187 -0
- package/src/pipeline/dispatch-query.ts +165 -0
- package/src/pipeline/dispatch-shared.ts +826 -0
- package/src/pipeline/dispatch-stream.ts +90 -0
- package/src/pipeline/dispatch-write.ts +451 -0
- package/src/pipeline/dispatcher-utils.ts +1 -1
- package/src/pipeline/dispatcher.ts +44 -1372
- package/src/pipeline/entity-cache.ts +2 -33
- package/src/pipeline/event-consumer-state.ts +30 -2
- package/src/pipeline/event-dispatcher-admin.ts +293 -0
- package/src/pipeline/event-dispatcher-delivery.ts +305 -0
- package/src/pipeline/event-dispatcher.ts +85 -542
- package/src/pipeline/index.ts +2 -0
- package/src/pipeline/msp-rebuild.ts +42 -3
- package/src/pipeline/multi-stream-apply-context.ts +4 -42
- package/src/pipeline/projection-rebuild.ts +105 -3
- package/src/pipeline/system-hooks.ts +144 -1
- package/src/rate-limit/__tests__/resolver.integration.test.ts +18 -0
- package/src/rate-limit/resolver.ts +16 -32
- package/src/schema-cli.ts +76 -16
- package/src/search/__tests__/meilisearch-adapter.integration.test.ts +207 -185
- package/src/search/__tests__/meilisearch-ids.test.ts +30 -0
- package/src/search/__tests__/reindex-entity.integration.test.ts +144 -0
- package/src/search/index.ts +6 -0
- package/src/search/meilisearch-adapter.ts +13 -12
- package/src/search/reindex-entity.ts +177 -0
- package/src/search/types.ts +1 -39
- package/src/secrets/__tests__/contains-secret.test.ts +34 -0
- package/src/secrets/__tests__/envelope-cipher.test.ts +59 -0
- package/src/secrets/__tests__/envelope.test.ts +1 -1
- package/src/secrets/dek-cache.ts +26 -5
- package/src/secrets/envelope-cipher.ts +53 -0
- package/src/secrets/envelope.ts +5 -3
- package/src/secrets/index.ts +13 -1
- package/src/secrets/stored-envelope.ts +46 -0
- package/src/secrets/types.ts +2 -162
- package/src/stack/__tests__/event-collector.test.ts +42 -0
- package/src/stack/__tests__/setup-test-stack-jobs.integration.test.ts +125 -0
- package/src/stack/db.ts +2 -1
- package/src/stack/push-entity-projection-tables.ts +4 -3
- package/src/stack/redis.ts +8 -0
- package/src/stack/request-helper.ts +38 -2
- package/src/stack/table-helpers.ts +6 -4
- package/src/stack/test-stack.ts +249 -131
- package/src/testing/__tests__/late-bound.test.ts +32 -0
- package/src/testing/__tests__/wait-for.test.ts +59 -0
- package/src/testing/boot-validator-fixture.ts +121 -0
- package/src/testing/e2e-generator.ts +8 -0
- package/src/testing/file-provider-contract.ts +104 -0
- package/src/testing/handler-context.ts +3 -1
- package/src/testing/index.ts +5 -0
- package/src/testing/late-bound.ts +5 -3
- package/src/testing/mutable-master-key-provider.ts +29 -3
- package/src/testing/wait-for.ts +3 -0
- package/src/testing/without-ambient-temporal.ts +14 -0
- package/src/time/__tests__/tz-dateline.test.ts +8 -10
- package/src/time/geo-tz.ts +1 -32
- package/src/time/index.ts +1 -0
- package/src/time/legacy-date.ts +18 -0
- package/src/time/polyfill.ts +21 -38
- package/src/time/tz-context.ts +44 -85
- package/src/ui-types/app-schema.ts +12 -0
- package/src/ui-types/index.ts +20 -7
- package/src/utils/__tests__/safe-json-temporal.test.ts +18 -0
- package/src/utils/safe-json.ts +13 -1
- package/src/__tests__/raw-table.integration.test.ts +0 -116
- package/src/bun-db/__tests__/bun-test-stack.ts +0 -6
- package/src/db/__tests__/encryption.test.ts +0 -39
- package/src/db/encryption.ts +0 -39
- package/src/db/row-helpers.ts +0 -4
- package/src/engine/__tests__/raw-table.test.ts +0 -150
- package/src/engine/__tests__/unmanaged-table.test.ts +0 -127
- package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +0 -184
- package/src/engine/feature-ast/extractors/round4.ts +0 -1366
package/src/db/table-builder.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ExecutorOnly } from "@cosmicdrift/kumiko-types/executor-brand";
|
|
1
2
|
import type {
|
|
2
3
|
EntityDefinition,
|
|
3
4
|
EntityRelations,
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
type ColumnBuilder,
|
|
16
17
|
type ColumnHandle,
|
|
17
18
|
decimalColumn,
|
|
19
|
+
doublePrecision,
|
|
18
20
|
type IndexBuilderWithCols,
|
|
19
21
|
index,
|
|
20
22
|
instant,
|
|
@@ -69,7 +71,13 @@ function fieldToColumns(
|
|
|
69
71
|
const base = text(snakeName);
|
|
70
72
|
const withDefault =
|
|
71
73
|
"default" in field && field.default !== undefined ? base.default(field.default) : base;
|
|
72
|
-
|
|
74
|
+
const mainCol = { [name]: field.required ? withDefault.notNull() : withDefault };
|
|
75
|
+
// lookupable → nullable HMAC-Blind-Index-Spalte (lock-step mit
|
|
76
|
+
// fieldToColumnMeta in entity-table-meta.ts).
|
|
77
|
+
if (field.type === "text" && field.lookupable === true) {
|
|
78
|
+
return { ...mainCol, [`${name}Bidx`]: text(`${snakeName}_bidx`) };
|
|
79
|
+
}
|
|
80
|
+
return mainCol;
|
|
73
81
|
}
|
|
74
82
|
case "boolean":
|
|
75
83
|
return {
|
|
@@ -101,7 +109,7 @@ function fieldToColumns(
|
|
|
101
109
|
// multi-select.
|
|
102
110
|
return { [name]: jsonb(snakeName).default([]).notNull() };
|
|
103
111
|
case "number": {
|
|
104
|
-
const base = integer(snakeName);
|
|
112
|
+
const base = field.integer === true ? integer(snakeName) : doublePrecision(snakeName);
|
|
105
113
|
const col = field.default !== undefined ? base.default(field.default) : base;
|
|
106
114
|
return { [name]: field.required ? col.notNull() : col };
|
|
107
115
|
}
|
|
@@ -272,63 +280,72 @@ type NullCol<_T> = ColumnHandle & { readonly __notNull: false };
|
|
|
272
280
|
// columns (resolved via FileRef table). `notNull` propagiert von
|
|
273
281
|
// `field.required` (literal preserved by createXField generics).
|
|
274
282
|
type ColumnsForField<K extends string, F extends FieldDefinition> = F extends {
|
|
275
|
-
type: "text"
|
|
283
|
+
type: "text";
|
|
284
|
+
lookupable: true;
|
|
276
285
|
}
|
|
277
|
-
? F extends { required: true }
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
?
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
:
|
|
286
|
+
? (F extends { required: true }
|
|
287
|
+
? { readonly [P in K]: Col<string> }
|
|
288
|
+
: { readonly [P in K]: NullCol<string> }) & {
|
|
289
|
+
readonly [P in `${K}Bidx`]: NullCol<string>;
|
|
290
|
+
}
|
|
291
|
+
: F extends {
|
|
292
|
+
type: "text" | "select" | "tz";
|
|
293
|
+
}
|
|
294
|
+
? F extends { required: true }
|
|
295
|
+
? { readonly [P in K]: Col<string> }
|
|
296
|
+
: { readonly [P in K]: NullCol<string> }
|
|
297
|
+
: F extends { type: "boolean" }
|
|
298
|
+
? // boolean default OR required → notNull (DB has DEFAULT, structurally never-null)
|
|
299
|
+
F extends { default: boolean } | { required: true }
|
|
300
|
+
? { readonly [P in K]: Col<boolean> }
|
|
301
|
+
: { readonly [P in K]: NullCol<boolean> }
|
|
302
|
+
: F extends { type: "multiSelect" }
|
|
303
|
+
? // jsonb default `[]`, immer notNull
|
|
304
|
+
{ readonly [P in K]: Col<readonly string[]> }
|
|
305
|
+
: F extends { type: "number" }
|
|
293
306
|
? F extends { required: true }
|
|
294
307
|
? { readonly [P in K]: Col<number> }
|
|
295
308
|
: { readonly [P in K]: NullCol<number> }
|
|
296
|
-
: F extends { type: "
|
|
309
|
+
: F extends { type: "decimal" }
|
|
297
310
|
? F extends { required: true }
|
|
298
|
-
? { readonly [P in K]: Col<number> }
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
: F extends { type: "
|
|
311
|
-
?
|
|
312
|
-
{ readonly [P in K]: Col<
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
: F extends { type: "
|
|
311
|
+
? { readonly [P in K]: Col<number> }
|
|
312
|
+
: { readonly [P in K]: NullCol<number> }
|
|
313
|
+
: F extends { type: "money" }
|
|
314
|
+
? F extends { required: true }
|
|
315
|
+
? { readonly [P in K]: Col<number> } & {
|
|
316
|
+
readonly [P in `${K}Currency`]: Col<string>;
|
|
317
|
+
}
|
|
318
|
+
: { readonly [P in K]: NullCol<number> } & {
|
|
319
|
+
readonly [P in `${K}Currency`]: Col<string>;
|
|
320
|
+
}
|
|
321
|
+
: F extends { type: "reference"; multiple: true }
|
|
322
|
+
? { readonly [P in K]: Col<readonly string[]> }
|
|
323
|
+
: F extends { type: "reference" }
|
|
324
|
+
? F extends { required: true }
|
|
325
|
+
? { readonly [P in K]: Col<string> }
|
|
326
|
+
: { readonly [P in K]: NullCol<string> }
|
|
327
|
+
: F extends { type: "embedded" }
|
|
328
|
+
? // jsonb default `{}`, immer notNull
|
|
329
|
+
{ readonly [P in K]: Col<Readonly<Record<string, unknown>>> }
|
|
330
|
+
: F extends { type: "date" | "timestamp" }
|
|
318
331
|
? F extends { required: true }
|
|
319
|
-
? { readonly [P in
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
: { readonly [P in `${K}Utc`]: NullCol<Temporal.Instant> } & {
|
|
323
|
-
readonly [P in `${K}Tz`]: NullCol<string>;
|
|
324
|
-
}
|
|
325
|
-
: F extends { type: "file" | "image" }
|
|
332
|
+
? { readonly [P in K]: Col<Temporal.Instant> }
|
|
333
|
+
: { readonly [P in K]: NullCol<Temporal.Instant> }
|
|
334
|
+
: F extends { type: "locatedTimestamp" }
|
|
326
335
|
? F extends { required: true }
|
|
327
|
-
? { readonly [P in K]: Col<
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
336
|
+
? { readonly [P in `${K}Utc`]: Col<Temporal.Instant> } & {
|
|
337
|
+
readonly [P in `${K}Tz`]: Col<string>;
|
|
338
|
+
}
|
|
339
|
+
: { readonly [P in `${K}Utc`]: NullCol<Temporal.Instant> } & {
|
|
340
|
+
readonly [P in `${K}Tz`]: NullCol<string>;
|
|
341
|
+
}
|
|
342
|
+
: F extends { type: "file" | "image" }
|
|
343
|
+
? F extends { required: true }
|
|
344
|
+
? { readonly [P in K]: Col<string> }
|
|
345
|
+
: { readonly [P in K]: NullCol<string> }
|
|
346
|
+
: F extends { type: "files" | "images" }
|
|
347
|
+
? Record<never, never>
|
|
348
|
+
: never;
|
|
332
349
|
|
|
333
350
|
type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (
|
|
334
351
|
k: infer I,
|
|
@@ -366,25 +383,7 @@ type SoftDeleteColumnsType = {
|
|
|
366
383
|
};
|
|
367
384
|
|
|
368
385
|
// ── ES-write brand ──────────────────────────────────────────────────────
|
|
369
|
-
|
|
370
|
-
// rebuild-safe). To make a direct write a *compile* error rather than a
|
|
371
|
-
// convention, EntityTable carries a phantom `unique symbol` prop; the public
|
|
372
|
-
// write helpers reject anything that has it (see NotExecutorOnly + query.ts).
|
|
373
|
-
// The symbol key dodges SchemaTable's `[field: string]: unknown` index
|
|
374
|
-
// signature (string index sigs don't cover symbol keys), so the brand
|
|
375
|
-
// survives the type-erasure that would swallow a plain marker prop — and the
|
|
376
|
-
// executor seam (applyEntityEvent) erases `table` to TableColumns<any>, which
|
|
377
|
-
// carries no such prop, so the one legitimate writer stays green.
|
|
378
|
-
declare const EXECUTOR_ONLY: unique symbol;
|
|
379
|
-
export interface ExecutorOnly {
|
|
380
|
-
readonly [EXECUTOR_ONLY]: true;
|
|
381
|
-
}
|
|
382
|
-
// Negative brand for write-helper params: a branded EntityTable is NOT
|
|
383
|
-
// assignable (its `true` clashes with `never`), while unmanaged EntityTableMeta
|
|
384
|
-
// and erased SchemaTable pass (they lack the prop, so `?: never` is satisfied).
|
|
385
|
-
export type NotExecutorOnly = {
|
|
386
|
-
readonly [EXECUTOR_ONLY]?: never;
|
|
387
|
-
};
|
|
386
|
+
export type { ExecutorOnly, NotExecutorOnly } from "@cosmicdrift/kumiko-types/executor-brand";
|
|
388
387
|
|
|
389
388
|
export type EntityTable<E extends EntityDefinition = EntityDefinition> =
|
|
390
389
|
TableColumns<// biome-ignore lint/suspicious/noExplicitAny: drizzle's internal table-config stays generic; we layer typed columns on top via the intersection below.
|
|
@@ -526,6 +525,18 @@ export function buildEntityTable<E extends EntityDefinition>(
|
|
|
526
525
|
).on(column);
|
|
527
526
|
}
|
|
528
527
|
}
|
|
528
|
+
// lookupable-Felder: Index auf der bidx-Spalte (lock-step mit
|
|
529
|
+
// buildEntityTableMeta).
|
|
530
|
+
const bidxFieldByField = new Map<string, string>();
|
|
531
|
+
for (const [name, field] of Object.entries(entity.fields)) {
|
|
532
|
+
if (field.type !== "text" || field.lookupable !== true) continue;
|
|
533
|
+
bidxFieldByField.set(name, `${name}Bidx`);
|
|
534
|
+
const column = tHandle[`${name}Bidx`];
|
|
535
|
+
if (column) {
|
|
536
|
+
const idxName = `${tableName}_${toSnakeCase(name)}_bidx_idx`;
|
|
537
|
+
indexes[idxName] = index(idxName).on(column);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
529
540
|
// entity.indexes = composite/unique-Indices die der Author explizit
|
|
530
541
|
// deklariert hat. Spalten werden via field-name (camelCase) angesprochen,
|
|
531
542
|
// der Index-Name folgt der Convention <table>_<col1>_<col2>_<unique|idx>
|
|
@@ -546,6 +557,26 @@ export function buildEntityTable<E extends EntityDefinition>(
|
|
|
546
557
|
chain = chain.where(def.where as SqlExpression);
|
|
547
558
|
}
|
|
548
559
|
indexes[indexName] = chain;
|
|
560
|
+
// Partielles bidx-Pendant für unique-Indices über lookupable-Spalten
|
|
561
|
+
// (lock-step mit buildEntityTableMeta).
|
|
562
|
+
if (def.unique === true && def.where === undefined) {
|
|
563
|
+
const bidxFieldNames = def.columns.map((c) => bidxFieldByField.get(c) ?? c);
|
|
564
|
+
if (bidxFieldNames.some((c, i) => c !== def.columns[i])) {
|
|
565
|
+
const bidxCols = bidxFieldNames
|
|
566
|
+
.map((c) => tHandle[c])
|
|
567
|
+
.filter((col): col is ColumnHandle => col !== undefined);
|
|
568
|
+
if (bidxCols.length === bidxFieldNames.length) {
|
|
569
|
+
const whereText = bidxFieldNames
|
|
570
|
+
.filter((c, i) => c !== def.columns[i])
|
|
571
|
+
.map((c) => `"${toSnakeCase(c)}" IS NOT NULL`)
|
|
572
|
+
.join(" AND ");
|
|
573
|
+
const partialWhere: SqlExpression = { kind: "sql-expr", text: whereText, params: [] };
|
|
574
|
+
indexes[`${indexName}_bidx`] = uniqueIndex(`${indexName}_bidx`)
|
|
575
|
+
.on(...bidxCols)
|
|
576
|
+
.where(partialWhere);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
549
580
|
}
|
|
550
581
|
return indexes;
|
|
551
582
|
},
|
package/src/db/tenant-db.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { KUMIKO_NAME_SYMBOL, type SchemaTable } from "@cosmicdrift/kumiko-types/schema-table-types";
|
|
2
|
+
import type { TenantDb, TenantDbMode } from "@cosmicdrift/kumiko-types/tenant-db-types";
|
|
1
3
|
import {
|
|
4
|
+
asEntityTableMeta,
|
|
2
5
|
asRawClient,
|
|
3
6
|
deleteMany as bunDeleteMany,
|
|
4
7
|
fetchOne as bunFetchOne,
|
|
@@ -11,70 +14,29 @@ import {
|
|
|
11
14
|
import { SYSTEM_TENANT_ID, type TenantId } from "../engine/types/identifiers";
|
|
12
15
|
import { emitDbQuery, type Meter, registerStandardMetrics, type Tracer } from "../observability";
|
|
13
16
|
import type { DbRunner } from "./connection";
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
type
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* TenantDb scope modes:
|
|
21
|
-
*
|
|
22
|
-
* - "tenant" (default): SELECT/UPDATE/DELETE filtered by tenantId + reference data (tenantId=SYSTEM_TENANT_ID).
|
|
23
|
-
* INSERT forces tenantId — handler cannot override.
|
|
24
|
-
*
|
|
25
|
-
* - "system" (r.systemScope()): No tenant filter on reads/updates/deletes.
|
|
26
|
-
* INSERT uses tenantId as default but handler can override.
|
|
27
|
-
*
|
|
28
|
-
* Tables without a tenantId column are always unfiltered regardless of mode.
|
|
29
|
-
*/
|
|
30
|
-
export type TenantDbMode = "tenant" | "system";
|
|
31
|
-
|
|
32
|
-
export type TenantDb = {
|
|
33
|
-
readonly tenantId: TenantId;
|
|
34
|
-
readonly mode: TenantDbMode;
|
|
35
|
-
/**
|
|
36
|
-
* Underlying DbRunner. Framework-internal use (event-store, migrations) —
|
|
37
|
-
* bypasses tenant-filter. Feature code uses the typed helpers above so the
|
|
38
|
-
* automatic scoping stays intact.
|
|
39
|
-
*/
|
|
40
|
-
readonly raw: DbRunner;
|
|
41
|
-
selectMany<T = Record<string, unknown>>(
|
|
42
|
-
table: Table,
|
|
43
|
-
where?: WhereObject,
|
|
44
|
-
options?: SelectOptions,
|
|
45
|
-
): Promise<readonly T[]>;
|
|
46
|
-
fetchOne<T = Record<string, unknown>>(table: Table, where: WhereObject): Promise<T | undefined>;
|
|
47
|
-
insertOne<T = Record<string, unknown>>(
|
|
48
|
-
table: Table,
|
|
49
|
-
values: Record<string, unknown>,
|
|
50
|
-
): Promise<T | undefined>;
|
|
51
|
-
updateMany<T = Record<string, unknown>>(
|
|
52
|
-
table: Table,
|
|
53
|
-
set: Record<string, unknown>,
|
|
54
|
-
where: WhereObject,
|
|
55
|
-
): Promise<readonly T[]>;
|
|
56
|
-
// Method-form writes (ctx.db.insertOne/updateMany/deleteMany) keep the erased
|
|
57
|
-
// Table param: the executor-only brand is enforced on the free-function
|
|
58
|
-
// helpers (the reflexive path all production writes take), while method-form
|
|
59
|
-
// on a branded table is covered by the guard-direct-entity-writes AST guard
|
|
60
|
-
// (P5) — branding it here would force every push+write test into casts for
|
|
61
|
-
// zero production benefit (no prod code writes a projection via method-form).
|
|
62
|
-
deleteMany(table: Table, where: WhereObject): Promise<void>;
|
|
63
|
-
};
|
|
17
|
+
|
|
18
|
+
type Table = SchemaTable;
|
|
19
|
+
|
|
20
|
+
export type { TenantDb, TenantDbMode } from "@cosmicdrift/kumiko-types/tenant-db-types";
|
|
64
21
|
|
|
65
22
|
// @cast-boundary tenant-db-row
|
|
66
23
|
export function castTenantRows<T>(rows: readonly Record<string, unknown>[]): readonly T[] {
|
|
67
24
|
return rows as unknown as readonly T[];
|
|
68
25
|
}
|
|
69
26
|
|
|
70
|
-
const KUMIKO_NAME_SYMBOL = Symbol.for("kumiko:schema:Name");
|
|
71
|
-
|
|
72
27
|
function tableNameOf(table: Table): string {
|
|
73
|
-
const sym =
|
|
28
|
+
const sym = table[KUMIKO_NAME_SYMBOL];
|
|
74
29
|
return typeof sym === "string" ? sym : "<unknown>";
|
|
75
30
|
}
|
|
76
31
|
|
|
32
|
+
// Checks the canonical EntityTableMeta (branded EntityTable's KUMIKO_META_SYMBOL
|
|
33
|
+
// or a plain buildEntityTableMeta/defineUnmanagedTable result), not a direct
|
|
34
|
+
// `table.tenantId` property read — the latter only exists on branded EntityTables
|
|
35
|
+
// and silently returned false (no tenant filter!) for plain EntityTableMeta
|
|
36
|
+
// tables like unmanaged direct-write stores, e.g. userSessionTable.
|
|
77
37
|
function hasTenantColumn(table: Table): boolean {
|
|
38
|
+
const meta = asEntityTableMeta(table);
|
|
39
|
+
if (meta) return meta.columns.some((c) => c.name === "tenant_id");
|
|
78
40
|
return (table as Record<string, unknown>)["tenantId"] !== undefined;
|
|
79
41
|
}
|
|
80
42
|
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { validateBoot } from "../boot-validator";
|
|
4
|
+
import { defineFeature } from "../define-feature";
|
|
5
|
+
import { createEntity, createTextField } from "../factories";
|
|
6
|
+
|
|
7
|
+
describe("validateBoot — action wiring (no function values)", () => {
|
|
8
|
+
test("rowAction writeHandler payload as function → Throw", () => {
|
|
9
|
+
const feature = defineFeature("shop", (r) => {
|
|
10
|
+
r.entity("product", createEntity({ fields: { name: createTextField() } }));
|
|
11
|
+
r.screen({
|
|
12
|
+
id: "product-list",
|
|
13
|
+
type: "entityList",
|
|
14
|
+
entity: "product",
|
|
15
|
+
columns: ["name"],
|
|
16
|
+
rowActions: [
|
|
17
|
+
{
|
|
18
|
+
kind: "writeHandler",
|
|
19
|
+
id: "sync",
|
|
20
|
+
label: "actions.sync",
|
|
21
|
+
handler: "shop:write:sync",
|
|
22
|
+
// biome-ignore lint/suspicious/noExplicitAny: intentional type violation under test
|
|
23
|
+
payload: ((row: unknown) => ({ id: row })) as any,
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
});
|
|
27
|
+
r.writeHandler("sync", z.object({}), async () => ({ isSuccess: true as const, data: null }), {
|
|
28
|
+
access: { roles: ["Admin"] },
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
expect(() => validateBoot([feature])).toThrow(/rowAction "sync" payload is a function/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("rowAction writeHandler payload as declarative pick → kein Throw", () => {
|
|
35
|
+
const feature = defineFeature("shop", (r) => {
|
|
36
|
+
r.entity("product", createEntity({ fields: { name: createTextField({ sortable: true }) } }));
|
|
37
|
+
r.screen({
|
|
38
|
+
id: "product-list",
|
|
39
|
+
type: "entityList",
|
|
40
|
+
entity: "product",
|
|
41
|
+
columns: ["name"],
|
|
42
|
+
defaultSort: { field: "name", dir: "asc" },
|
|
43
|
+
rowActions: [
|
|
44
|
+
{
|
|
45
|
+
kind: "writeHandler",
|
|
46
|
+
id: "sync",
|
|
47
|
+
label: "actions.sync",
|
|
48
|
+
handler: "shop:write:sync",
|
|
49
|
+
payload: { pick: ["name"] },
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
r.writeHandler("sync", z.object({}), async () => ({ isSuccess: true as const, data: null }), {
|
|
54
|
+
access: { roles: ["Admin"] },
|
|
55
|
+
});
|
|
56
|
+
r.translations({
|
|
57
|
+
keys: {
|
|
58
|
+
"screen:product-list.title": { de: "Liste", en: "List" },
|
|
59
|
+
"shop:entity:product:field:name": { de: "Name", en: "Name" },
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
expect(() => validateBoot([feature])).not.toThrow();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("rowAction navigate visible as function → Throw", () => {
|
|
67
|
+
const feature = defineFeature("shop", (r) => {
|
|
68
|
+
r.entity("product", createEntity({ fields: { name: createTextField() } }));
|
|
69
|
+
r.screen({
|
|
70
|
+
id: "product-list",
|
|
71
|
+
type: "entityList",
|
|
72
|
+
entity: "product",
|
|
73
|
+
columns: ["name"],
|
|
74
|
+
rowActions: [
|
|
75
|
+
{
|
|
76
|
+
kind: "navigate",
|
|
77
|
+
id: "edit",
|
|
78
|
+
label: "actions.edit",
|
|
79
|
+
screen: "product-list",
|
|
80
|
+
// biome-ignore lint/suspicious/noExplicitAny: intentional type violation under test
|
|
81
|
+
visible: (() => true) as any,
|
|
82
|
+
},
|
|
83
|
+
],
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
expect(() => validateBoot([feature])).toThrow(/rowAction "edit" visible is a function/);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("toolbarAction writeHandler payload as function → Throw", () => {
|
|
90
|
+
const feature = defineFeature("shop", (r) => {
|
|
91
|
+
r.entity("product", createEntity({ fields: { name: createTextField() } }));
|
|
92
|
+
r.screen({
|
|
93
|
+
id: "product-list",
|
|
94
|
+
type: "entityList",
|
|
95
|
+
entity: "product",
|
|
96
|
+
columns: ["name"],
|
|
97
|
+
toolbarActions: [
|
|
98
|
+
{
|
|
99
|
+
kind: "writeHandler",
|
|
100
|
+
id: "sync",
|
|
101
|
+
label: "actions.sync",
|
|
102
|
+
handler: "shop:write:sync",
|
|
103
|
+
// biome-ignore lint/suspicious/noExplicitAny: intentional type violation under test
|
|
104
|
+
payload: (() => ({})) as any,
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
});
|
|
108
|
+
r.writeHandler("sync", z.object({}), async () => ({ isSuccess: true as const, data: null }), {
|
|
109
|
+
access: { roles: ["Admin"] },
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
expect(() => validateBoot([feature])).toThrow(/toolbarAction "sync" payload is a function/);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("entityList column renderer as function → Throw", () => {
|
|
116
|
+
const feature = defineFeature("shop", (r) => {
|
|
117
|
+
r.entity("product", createEntity({ fields: { name: createTextField() } }));
|
|
118
|
+
r.screen({
|
|
119
|
+
id: "product-list",
|
|
120
|
+
type: "entityList",
|
|
121
|
+
entity: "product",
|
|
122
|
+
columns: [
|
|
123
|
+
// biome-ignore lint/suspicious/noExplicitAny: intentional type violation under test
|
|
124
|
+
{ field: "name", renderer: ((v: unknown) => String(v)) as any },
|
|
125
|
+
],
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
expect(() => validateBoot([feature])).toThrow(/column "name" renderer is a function/);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("entityEdit field visible as function → Throw", () => {
|
|
132
|
+
const feature = defineFeature("shop", (r) => {
|
|
133
|
+
r.entity("product", createEntity({ fields: { name: createTextField() } }));
|
|
134
|
+
r.screen({
|
|
135
|
+
id: "product-edit",
|
|
136
|
+
type: "entityEdit",
|
|
137
|
+
entity: "product",
|
|
138
|
+
layout: {
|
|
139
|
+
sections: [
|
|
140
|
+
{
|
|
141
|
+
columns: 1,
|
|
142
|
+
// biome-ignore lint/suspicious/noExplicitAny: intentional type violation under test
|
|
143
|
+
fields: [{ field: "name", visible: (() => true) as any }],
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
expect(() => validateBoot([feature])).toThrow(/field "name" visible is a function/);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("entityEdit field renderer as function → Throw", () => {
|
|
153
|
+
const feature = defineFeature("shop", (r) => {
|
|
154
|
+
r.entity("product", createEntity({ fields: { name: createTextField() } }));
|
|
155
|
+
r.screen({
|
|
156
|
+
id: "product-edit",
|
|
157
|
+
type: "entityEdit",
|
|
158
|
+
entity: "product",
|
|
159
|
+
layout: {
|
|
160
|
+
sections: [
|
|
161
|
+
{
|
|
162
|
+
columns: 1,
|
|
163
|
+
// biome-ignore lint/suspicious/noExplicitAny: intentional type violation under test
|
|
164
|
+
fields: [{ field: "name", renderer: ((v: unknown) => String(v)) as any }],
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
expect(() => validateBoot([feature])).toThrow(/field "name" renderer is a function/);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("entityEdit field with declarative visible/readOnly/required → kein Throw", () => {
|
|
174
|
+
const feature = defineFeature("shop", (r) => {
|
|
175
|
+
r.entity("product", createEntity({ fields: { name: createTextField() } }));
|
|
176
|
+
r.screen({
|
|
177
|
+
id: "product-edit",
|
|
178
|
+
type: "entityEdit",
|
|
179
|
+
entity: "product",
|
|
180
|
+
layout: {
|
|
181
|
+
sections: [
|
|
182
|
+
{
|
|
183
|
+
columns: 1,
|
|
184
|
+
fields: [
|
|
185
|
+
{
|
|
186
|
+
field: "name",
|
|
187
|
+
visible: { field: "name", ne: "" },
|
|
188
|
+
readOnly: false,
|
|
189
|
+
required: true,
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
r.translations({
|
|
197
|
+
keys: {
|
|
198
|
+
"screen:product-edit.title": { de: "Bearbeiten", en: "Edit" },
|
|
199
|
+
"shop:entity:product:field:name": { de: "Name", en: "Name" },
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
expect(() => validateBoot([feature])).not.toThrow();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("projectionList column renderer as function → Throw", () => {
|
|
207
|
+
const feature = defineFeature("shop", (r) => {
|
|
208
|
+
r.screen({
|
|
209
|
+
id: "sales-list",
|
|
210
|
+
type: "projectionList",
|
|
211
|
+
query: "shop:query:sales",
|
|
212
|
+
columns: [
|
|
213
|
+
// biome-ignore lint/suspicious/noExplicitAny: intentional type violation under test
|
|
214
|
+
{ field: "amount", renderer: ((v: unknown) => String(v)) as any },
|
|
215
|
+
],
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
expect(() => validateBoot([feature])).toThrow(/column "amount" renderer is a function/);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("projectionList rowAction payload as function → Throw", () => {
|
|
222
|
+
const feature = defineFeature("shop", (r) => {
|
|
223
|
+
r.screen({
|
|
224
|
+
id: "sales-list",
|
|
225
|
+
type: "projectionList",
|
|
226
|
+
query: "shop:query:sales",
|
|
227
|
+
columns: ["amount"],
|
|
228
|
+
rowActions: [
|
|
229
|
+
{
|
|
230
|
+
kind: "writeHandler",
|
|
231
|
+
id: "sync",
|
|
232
|
+
label: "actions.sync",
|
|
233
|
+
handler: "shop:write:sync",
|
|
234
|
+
// biome-ignore lint/suspicious/noExplicitAny: intentional type violation under test
|
|
235
|
+
payload: (() => ({})) as any,
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
expect(() => validateBoot([feature])).toThrow(/rowAction "sync" payload is a function/);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// r.bootCheck(fn) — feature-declared mount invariants, checked at boot.
|
|
2
|
+
// Mirrors the prompt-store trap (kumiko-enterprise#229): a feature with
|
|
3
|
+
// PII-annotated fields was mounted without its required companion feature,
|
|
4
|
+
// and nothing caught it at boot. The conditional-invariant tests below
|
|
5
|
+
// reproduce that shape: the check only fails when the feature actually has
|
|
6
|
+
// PII fields AND the companion is missing — a bare "is X mounted" check
|
|
7
|
+
// would already be covered by r.requires and wouldn't justify this API.
|
|
8
|
+
|
|
9
|
+
import { describe, expect, test } from "bun:test";
|
|
10
|
+
import { validateFeatureBootChecks } from "../boot-validator/boot-check";
|
|
11
|
+
import { defineFeature } from "../define-feature";
|
|
12
|
+
import { createEntity, createTextField } from "../factories";
|
|
13
|
+
|
|
14
|
+
function catchMessage(fn: () => void): string {
|
|
15
|
+
try {
|
|
16
|
+
fn();
|
|
17
|
+
} catch (e) {
|
|
18
|
+
return e instanceof Error ? e.message : String(e);
|
|
19
|
+
}
|
|
20
|
+
throw new Error("expected function to throw, but it did not");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const requiresUserDataHook = (features: readonly { readonly name: string }[]) =>
|
|
24
|
+
features.some((f) => f.name === "user-data-hook");
|
|
25
|
+
|
|
26
|
+
const promptStore = () =>
|
|
27
|
+
defineFeature("prompt-store", (r) => {
|
|
28
|
+
const promptFields = { text: createTextField({ pii: true }) };
|
|
29
|
+
r.entity("prompt", createEntity({ fields: promptFields }));
|
|
30
|
+
r.bootCheck(({ features }) => {
|
|
31
|
+
// Conditional on this feature's own shape (has a pii field), closed
|
|
32
|
+
// over from setup — r.requires("user-data-hook") can't express that.
|
|
33
|
+
const hasPiiField = Object.values(promptFields).some((field) => field.pii);
|
|
34
|
+
if (hasPiiField && !requiresUserDataHook(features)) {
|
|
35
|
+
throw new Error("prompt-store has PII fields but no user-data-hook feature is mounted");
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("r.bootCheck / validateFeatureBootChecks", () => {
|
|
41
|
+
test("no bootChecks registered → no-op", () => {
|
|
42
|
+
const noop = defineFeature("noop", () => {});
|
|
43
|
+
expect(() => validateFeatureBootChecks([noop])).not.toThrow();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("conditional invariant satisfied (companion mounted) → boot succeeds", () => {
|
|
47
|
+
const userDataHook = defineFeature("user-data-hook", () => {});
|
|
48
|
+
expect(() => validateFeatureBootChecks([userDataHook, promptStore()])).not.toThrow();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("conditional invariant violated (PII field, no companion) → boot fails with feature-prefixed message", () => {
|
|
52
|
+
const message = catchMessage(() => validateFeatureBootChecks([promptStore()]));
|
|
53
|
+
expect(message).toContain("[Feature prompt-store]");
|
|
54
|
+
expect(message).toContain(
|
|
55
|
+
"prompt-store has PII fields but no user-data-hook feature is mounted",
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("no PII field → boot succeeds even without the companion", () => {
|
|
60
|
+
const noPiiFields = { text: createTextField() };
|
|
61
|
+
const noPii = defineFeature("prompt-store-no-pii", (r) => {
|
|
62
|
+
r.entity("note", createEntity({ fields: noPiiFields }));
|
|
63
|
+
r.bootCheck(({ features }) => {
|
|
64
|
+
const hasPiiField = Object.values(noPiiFields).some((field) => field.pii);
|
|
65
|
+
if (hasPiiField && !requiresUserDataHook(features)) {
|
|
66
|
+
throw new Error("unreachable in this test");
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
expect(() => validateFeatureBootChecks([noPii])).not.toThrow();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("multiple bootChecks on one feature all run in order until one throws", () => {
|
|
74
|
+
const calls: string[] = [];
|
|
75
|
+
const feature = defineFeature("multi-check", (r) => {
|
|
76
|
+
r.bootCheck(() => {
|
|
77
|
+
calls.push("first");
|
|
78
|
+
});
|
|
79
|
+
r.bootCheck(() => {
|
|
80
|
+
calls.push("second");
|
|
81
|
+
throw new Error("second check failed");
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
expect(() => validateFeatureBootChecks([feature])).toThrow("second check failed");
|
|
85
|
+
expect(calls).toEqual(["first", "second"]);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("ctx.features exposes every mounted feature, not just the declaring one", () => {
|
|
89
|
+
const a = defineFeature("feature-a", () => {});
|
|
90
|
+
let seenNames: string[] = [];
|
|
91
|
+
const b = defineFeature("feature-b", (r) => {
|
|
92
|
+
r.bootCheck(({ features }) => {
|
|
93
|
+
seenNames = features.map((f) => f.name);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
validateFeatureBootChecks([a, b]);
|
|
97
|
+
expect(seenNames).toEqual(["feature-a", "feature-b"]);
|
|
98
|
+
});
|
|
99
|
+
});
|