@cosmicdrift/kumiko-framework 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -38
- package/package.json +21 -2
- package/src/__tests__/anonymous-access.integration.test.ts +185 -0
- package/src/__tests__/consumer-cli.integration.test.ts +172 -0
- package/src/__tests__/entity-permalink-open.integration.test.ts +94 -0
- package/src/__tests__/full-stack.integration.test.ts +1 -1
- package/src/__tests__/schema-cli-temporal-polyfill.test.ts +48 -0
- package/src/__tests__/schema-cli.integration.test.ts +50 -0
- package/src/__tests__/store-table.integration.test.ts +93 -0
- package/src/api/__tests__/api.test.ts +373 -0
- package/src/api/__tests__/auth-middleware-anonymous-access-boot.test.ts +40 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +17 -1
- package/src/api/__tests__/auth-routes-invalid-body-invite.test.ts +253 -0
- package/src/api/__tests__/auth-routes-mfa-preauth-confirm.test.ts +222 -0
- package/src/api/__tests__/auth-routes-mfa-preauth-enable-start.test.ts +249 -0
- package/src/api/__tests__/auth-routes-mfa-verify.test.ts +202 -0
- package/src/api/__tests__/auth-routes-trusted-proxy.test.ts +135 -0
- package/src/api/__tests__/batch.integration.test.ts +101 -11
- package/src/api/__tests__/csrf-constants-sync.test.ts +20 -0
- package/src/api/__tests__/dispatcher-live.integration.test.ts +74 -0
- package/src/api/__tests__/jwt.test.ts +200 -1
- package/src/api/__tests__/login-rate-limiter-sweep.test.ts +50 -0
- package/src/api/__tests__/pat-scope.test.ts +36 -0
- package/src/api/__tests__/pii-leak-guard.integration.test.ts +103 -0
- package/src/api/__tests__/redis-login-rate-limiter.integration.test.ts +138 -0
- package/src/api/__tests__/request-id-middleware.test.ts +51 -0
- package/src/api/__tests__/server-boot-guards.test.ts +71 -0
- package/src/api/__tests__/server-jwt-ttl.test.ts +58 -0
- package/src/api/__tests__/sse-broker.test.ts +57 -0
- package/src/api/__tests__/sse-route.test.ts +4 -0
- package/src/api/api-constants.ts +11 -0
- package/src/api/auth-middleware.ts +248 -40
- package/src/api/auth-routes.ts +576 -95
- package/src/api/index.ts +13 -4
- package/src/api/jwt.ts +170 -11
- package/src/api/pat-scope.ts +14 -0
- package/src/api/pii-leak-guard.ts +47 -0
- package/src/api/request-context.ts +3 -0
- package/src/api/request-id-middleware.ts +2 -0
- package/src/api/routes.ts +169 -2
- package/src/api/server.ts +112 -16
- package/src/api/sse-broker.ts +39 -0
- package/src/bun-db/__tests__/PATTERN.md +0 -1
- package/src/bun-db/__tests__/coerce-row-temporal.test.ts +41 -0
- package/src/bun-db/__tests__/select-many-retry.test.ts +79 -0
- package/src/bun-db/__tests__/write-brand.test.ts +21 -0
- package/src/bun-db/connection.ts +3 -3
- package/src/bun-db/index.ts +2 -0
- package/src/bun-db/query.ts +105 -32
- package/src/consumer-cli.ts +134 -0
- package/src/crypto/__tests__/blind-index.test.ts +130 -0
- package/src/crypto/__tests__/event-pii.test.ts +161 -0
- package/src/crypto/__tests__/kek-rotation.integration.test.ts +180 -0
- package/src/crypto/__tests__/kms-adapter-contract.ts +134 -0
- package/src/crypto/__tests__/kms-adapter.contract.test.ts +4 -0
- package/src/crypto/__tests__/pg-kms-adapter.integration.test.ts +117 -0
- package/src/crypto/__tests__/pii-field-encryption.test.ts +376 -0
- package/src/crypto/__tests__/request-kms-cache.test.ts +74 -0
- package/src/crypto/__tests__/subject-resolver.test.ts +108 -0
- package/src/crypto/blind-index.ts +118 -0
- package/src/crypto/event-pii.ts +70 -0
- package/src/crypto/in-memory-kms-adapter.ts +50 -0
- package/src/crypto/index.ts +67 -0
- package/src/crypto/kms-adapter.ts +2 -0
- package/src/crypto/pg-kms-adapter.ts +295 -0
- package/src/crypto/pii-field-encryption.ts +248 -0
- package/src/crypto/request-kms-cache.ts +38 -0
- package/src/crypto/subject-resolver.ts +91 -0
- package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +191 -0
- package/src/db/__tests__/blind-index.integration.test.ts +248 -0
- package/src/db/__tests__/build-filter-where.test.ts +34 -0
- package/src/db/__tests__/collect-table-metas.test.ts +10 -10
- package/src/db/__tests__/config-seed.integration.test.ts +13 -5
- package/src/db/__tests__/dialect-instant.test.ts +1 -4
- package/src/db/__tests__/entity-field-encryption.test.ts +7 -7
- package/src/db/__tests__/entity-table-meta-source.test.ts +50 -0
- package/src/db/__tests__/event-store-executor-context.pii-roundtrip.test.ts +67 -0
- package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +402 -0
- package/src/db/__tests__/event-store-executor.integration.test.ts +299 -29
- package/src/db/__tests__/feature-table-sources.test.ts +34 -0
- package/src/db/__tests__/implicit-projection-equivalence.integration.test.ts +118 -39
- package/src/db/__tests__/instant-to-driver-temporal.test.ts +21 -0
- package/src/db/__tests__/located-timestamp.test.ts +19 -0
- package/src/db/__tests__/migrate-generator.test.ts +21 -0
- package/src/db/__tests__/migrate-runner.test.ts +61 -0
- package/src/db/__tests__/number-field-fractional.integration.test.ts +58 -0
- package/src/db/__tests__/rebuild-marker.test.ts +13 -3
- package/src/db/__tests__/replay-migration-sql.test.ts +384 -0
- package/src/db/__tests__/schema-inspection.test.ts +7 -0
- package/src/db/__tests__/schema-migration.integration.test.ts +1 -1
- package/src/db/__tests__/table-builder-meta-lockstep.test.ts +39 -0
- package/src/db/__tests__/tenant-db-where-merge.test.ts +49 -3
- package/src/db/__tests__/tenant-db.integration.test.ts +6 -2
- package/src/db/api.ts +2 -2
- package/src/db/apply-entity-event.ts +18 -14
- package/src/db/blind-index-cleanup.ts +55 -0
- package/src/db/bun-provider.ts +2 -2
- package/src/db/collect-table-metas.ts +6 -7
- package/src/db/config-seed.ts +9 -9
- package/src/db/connection.ts +7 -12
- package/src/db/cursor.ts +1 -18
- package/src/db/dialect.ts +20 -26
- package/src/db/entity-field-encryption.ts +81 -24
- package/src/db/entity-table-meta-types.ts +2 -0
- package/src/db/entity-table-meta.ts +63 -90
- package/src/db/event-store-executor-context.ts +404 -0
- package/src/db/event-store-executor-read.ts +275 -0
- package/src/db/event-store-executor-write.ts +644 -0
- package/src/db/event-store-executor.ts +28 -1155
- package/src/db/feature-table-sources.ts +6 -5
- package/src/db/index.ts +20 -3
- package/src/db/located-timestamp.ts +4 -0
- package/src/db/migrate-generator.ts +39 -6
- package/src/db/migrate-runner.ts +107 -11
- package/src/db/pg-error.ts +9 -1
- package/src/db/postgres-provider.ts +2 -2
- package/src/db/queries/__tests__/event-store-idempotency-index.integration.test.ts +80 -0
- package/src/db/queries/backfill-pii.ts +277 -0
- package/src/db/queries/ddl.ts +45 -0
- package/src/db/queries/event-consumer.ts +35 -2
- package/src/db/queries/event-store.ts +126 -19
- package/src/db/queries/projection-rebuild.ts +22 -8
- package/src/db/queries/shadow-swap.ts +183 -0
- package/src/db/queries/test-stack.ts +4 -30
- package/src/db/query-api.ts +1 -0
- package/src/db/query.ts +1 -0
- package/src/db/rebuild-marker.ts +18 -2
- package/src/db/reference-data.ts +2 -3
- package/src/db/replay-migration-sql.ts +257 -0
- package/src/db/schema-inspection.ts +1 -1
- package/src/db/table-builder.ts +102 -71
- package/src/db/tenant-db.ts +15 -53
- package/src/engine/__tests__/boot-validator-action-wiring.test.ts +242 -0
- package/src/engine/__tests__/boot-validator-boot-check.test.ts +99 -0
- package/src/engine/__tests__/boot-validator-dashboard.test.ts +237 -0
- package/src/engine/__tests__/boot-validator-entity-list.test.ts +104 -0
- package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +7 -69
- package/src/engine/__tests__/boot-validator-i18n-keys.test.ts +26 -5
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +3 -19
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +246 -4
- package/src/engine/__tests__/boot-validator.test.ts +219 -15
- package/src/engine/__tests__/build-app-schema.test.ts +82 -0
- package/src/engine/__tests__/build-config-feature-schema.test.ts +125 -0
- package/src/engine/__tests__/build-target.test.ts +3 -11
- package/src/engine/__tests__/codemod-pipeline.test.ts +152 -21
- package/src/engine/__tests__/config-helpers.test.ts +16 -0
- package/src/engine/__tests__/define-feature-entity-mapping.test.ts +6 -0
- package/src/engine/__tests__/define-roles.test.ts +21 -0
- package/src/engine/__tests__/engine.test.ts +163 -1
- package/src/engine/__tests__/entity-handlers.test.ts +80 -0
- package/src/engine/__tests__/event-migration-declarative.test.ts +65 -0
- package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
- package/src/engine/__tests__/extend-entity-projection.test.ts +123 -0
- package/src/engine/__tests__/factories-time.test.ts +2 -66
- package/src/engine/__tests__/feature-crud-shorthand.test.ts +28 -0
- package/src/engine/__tests__/feature-manifest.test.ts +31 -1
- package/src/engine/__tests__/field-access.test.ts +23 -1
- package/src/engine/__tests__/hook-phases.test.ts +5 -5
- package/src/engine/__tests__/membership-roles.test.ts +4 -10
- package/src/engine/__tests__/nav.test.ts +86 -1
- package/src/engine/__tests__/pipeline-engine.test.ts +9 -9
- package/src/engine/__tests__/pipeline-handler.integration.test.ts +18 -18
- package/src/engine/__tests__/pipeline-observability.integration.test.ts +2 -2
- package/src/engine/__tests__/pipeline-performance.integration.test.ts +3 -3
- package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +9 -9
- package/src/engine/__tests__/post-query-hook.test.ts +7 -7
- package/src/engine/__tests__/registrar-object-form.test.ts +141 -0
- package/src/engine/__tests__/registry.test.ts +144 -0
- package/src/engine/__tests__/schema-builder.test.ts +18 -0
- package/src/engine/__tests__/screen.test.ts +147 -1
- package/src/engine/__tests__/soft-delete-cleanup.test.ts +3 -0
- package/src/engine/__tests__/store-table.test.ts +229 -0
- package/src/engine/__tests__/tier-resolver-extension.test.ts +19 -1
- package/src/engine/__tests__/{visual-tree-patterns.test.ts → tree-actions-patterns.test.ts} +7 -95
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +15 -15
- package/src/engine/boot-validator/__tests__/config-deps.test.ts +92 -0
- package/src/engine/boot-validator/action-wiring.ts +149 -0
- package/src/engine/boot-validator/boot-check.ts +21 -0
- package/src/engine/boot-validator/config-deps.ts +40 -4
- package/src/engine/boot-validator/entity-handler.ts +20 -21
- package/src/engine/boot-validator/entity-list-screens.ts +88 -0
- package/src/engine/boot-validator/gdpr-storage.ts +0 -20
- package/src/engine/boot-validator/i18n-keys.ts +58 -4
- package/src/engine/boot-validator/index.ts +33 -12
- package/src/engine/boot-validator/nav.ts +125 -0
- package/src/engine/boot-validator/pii-retention.ts +95 -10
- package/src/engine/boot-validator/{screens-nav.ts → screens.ts} +214 -191
- package/src/engine/boot-validator/workspaces.ts +68 -0
- package/src/engine/build-app-schema.ts +16 -0
- package/src/engine/build-config-feature-schema.ts +44 -7
- package/src/engine/codemod/pipeline-codemod.ts +5 -5
- package/src/engine/config-helpers.ts +15 -0
- package/src/engine/constants.ts +32 -6
- package/src/engine/create-app.ts +11 -0
- package/src/engine/define-feature.ts +95 -947
- package/src/engine/define-handler.ts +28 -94
- package/src/engine/define-workflow.ts +1 -1
- package/src/engine/effective-features.ts +12 -2
- package/src/engine/entity-handlers.ts +76 -11
- package/src/engine/extensions/tenant-data.ts +19 -0
- package/src/engine/extensions/user-data.ts +29 -2
- package/src/engine/factories.ts +8 -49
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +26 -29
- package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/feature.ts +9 -0
- package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/screens.ts +4 -0
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -2
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +18 -8
- package/src/engine/feature-ast/__tests__/parse.test.ts +1640 -160
- package/src/engine/feature-ast/__tests__/patch.test.ts +206 -12
- package/src/engine/feature-ast/__tests__/patcher.test.ts +20 -23
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +380 -5
- package/src/engine/feature-ast/extractors/events.ts +322 -0
- package/src/engine/feature-ast/extractors/handlers.ts +234 -0
- package/src/engine/feature-ast/extractors/hooks.ts +243 -0
- package/src/engine/feature-ast/extractors/index.ts +34 -31
- package/src/engine/feature-ast/extractors/jobs-routes.ts +221 -0
- package/src/engine/feature-ast/extractors/projections-screens.ts +269 -0
- package/src/engine/feature-ast/extractors/round1.ts +3 -3
- package/src/engine/feature-ast/extractors/round5.ts +3 -3
- package/src/engine/feature-ast/extractors/round6.ts +2 -36
- package/src/engine/feature-ast/extractors/shared.ts +64 -6
- package/src/engine/feature-ast/index.ts +2 -4
- package/src/engine/feature-ast/parse.ts +130 -23
- package/src/engine/feature-ast/patch.ts +39 -50
- package/src/engine/feature-ast/patcher.ts +37 -38
- package/src/engine/feature-ast/patterns.ts +50 -62
- package/src/engine/feature-ast/render.ts +67 -44
- package/src/engine/feature-builder-state.ts +168 -0
- package/src/engine/feature-config-events-jobs.ts +403 -0
- package/src/engine/feature-entity-handlers.ts +208 -0
- package/src/engine/feature-manifest.ts +2 -1
- package/src/engine/feature-ui-extensions.ts +500 -0
- package/src/engine/field-access.ts +13 -2
- package/src/engine/field-helpers.ts +31 -0
- package/src/engine/handler-helpers.ts +26 -0
- package/src/engine/hook-helpers.ts +16 -0
- package/src/engine/index.ts +23 -7
- package/src/engine/membership-roles.ts +13 -0
- package/src/engine/object-form.ts +27 -0
- package/src/engine/ownership.ts +26 -79
- package/src/engine/pattern-library/__tests__/library.test.ts +7 -20
- package/src/engine/pattern-library/library.ts +44 -1152
- package/src/engine/pattern-library/mixed-schemas.ts +450 -0
- package/src/engine/pattern-library/opaque-schemas.ts +124 -0
- package/src/engine/pattern-library/shared-fields.ts +75 -0
- package/src/engine/pattern-library/static-schemas.ts +456 -0
- package/src/engine/pipeline.ts +6 -11
- package/src/engine/registry-facade.ts +362 -0
- package/src/engine/registry-ingest.ts +478 -0
- package/src/engine/registry-state.ts +388 -0
- package/src/engine/registry-validate.ts +642 -0
- package/src/engine/registry.ts +78 -1658
- package/src/engine/run-pipeline.ts +1 -1
- package/src/engine/schema-builder.ts +1 -0
- package/src/engine/screen-helpers.ts +54 -0
- package/src/engine/soft-delete-cleanup.ts +6 -2
- package/src/engine/steps/__tests__/duration-utils.test.ts +20 -0
- package/src/engine/steps/_duration-utils.ts +2 -0
- package/src/engine/steps/unsafe-projection-upsert.ts +1 -4
- package/src/engine/tier-resolver-extension.ts +3 -2
- package/src/engine/types/config.ts +2 -482
- package/src/engine/types/define-handler.ts +2 -0
- package/src/engine/types/entity-handlers.ts +2 -0
- package/src/engine/types/event-type-map.ts +1 -37
- package/src/engine/types/feature.ts +2 -972
- package/src/engine/types/fields.ts +2 -675
- package/src/engine/types/handlers.ts +2 -774
- package/src/engine/types/hooks.ts +2 -184
- package/src/engine/types/http-route.ts +1 -54
- package/src/engine/types/identifiers.ts +1 -47
- package/src/engine/types/index.ts +86 -40
- package/src/engine/types/nav.ts +2 -63
- package/src/engine/types/ownership.ts +2 -0
- package/src/engine/types/projection.ts +2 -138
- package/src/engine/types/relations.ts +1 -51
- package/src/engine/types/screen.ts +2 -574
- package/src/engine/types/step.ts +2 -334
- package/src/engine/types/target-ref.ts +1 -21
- package/src/engine/types/tree-node.ts +1 -132
- package/src/engine/types/workspace.ts +2 -49
- package/src/engine/validate-projection-allowlist.ts +6 -6
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.test.ts +120 -1
- package/src/entrypoint/index.ts +49 -6
- package/src/errors/classes.ts +22 -1
- package/src/errors/field-issue.ts +0 -3
- package/src/errors/index.ts +1 -1
- package/src/errors/write-error-info.ts +10 -22
- package/src/es-ops/README.md +1 -1
- package/src/es-ops/__tests__/runner.integration.test.ts +74 -0
- package/src/es-ops/context.ts +4 -2
- package/src/es-ops/types.ts +8 -1
- package/src/event-store/__tests__/admin-api.integration.test.ts +27 -1
- package/src/event-store/__tests__/backfill-pii.integration.test.ts +279 -0
- package/src/event-store/__tests__/event-store.integration.test.ts +201 -0
- package/src/event-store/__tests__/row-to-stored-event.test.ts +2 -2
- package/src/event-store/__tests__/snapshot.integration.test.ts +86 -0
- package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +58 -0
- package/src/event-store/__tests__/upcaster.integration.test.ts +77 -45
- package/src/event-store/admin-api.ts +11 -4
- package/src/event-store/errors.ts +2 -35
- package/src/event-store/event-store.ts +64 -78
- package/src/event-store/events-schema.ts +11 -13
- package/src/event-store/index.ts +18 -3
- package/src/event-store/rebuild-dead-letter.ts +111 -0
- package/src/event-store/snapshot.ts +63 -40
- package/src/event-store/types.ts +2 -0
- package/src/files/__tests__/build-storage-key.test.ts +28 -0
- package/src/files/__tests__/file-ref-entity.test.ts +8 -0
- package/src/files/__tests__/files.integration.test.ts +24 -0
- package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
- package/src/files/__tests__/local-provider.test.ts +31 -0
- package/src/files/__tests__/provider-resolver.test.ts +70 -0
- package/src/files/__tests__/write-stream.test.ts +13 -0
- package/src/files/file-handle.ts +2 -19
- package/src/files/file-ref-entity.ts +2 -2
- package/src/files/file-routes.ts +13 -0
- package/src/files/index.ts +1 -1
- package/src/files/local-provider.ts +27 -8
- package/src/files/provider-resolver.ts +31 -20
- package/src/files/types.ts +13 -55
- package/src/i18n/__tests__/required-surface-keys.test.ts +17 -0
- package/src/i18n/required-surface-keys.ts +120 -1
- package/src/jobs/__tests__/job-queue-depth.integration.test.ts +82 -0
- package/src/jobs/__tests__/jobs.integration.test.ts +329 -3
- package/src/jobs/job-runner.ts +82 -12
- package/src/logging/types.ts +1 -7
- package/src/migrations/pending-rebuilds.ts +1 -1
- package/src/observability/__tests__/observability.integration.test.ts +4 -1
- package/src/observability/__tests__/recording-tracer.test.ts +6 -4
- package/src/observability/index.ts +2 -0
- package/src/observability/noop-provider.ts +0 -9
- package/src/observability/recording-tracer.ts +0 -12
- package/src/observability/standard-metrics.ts +64 -2
- package/src/observability/types/index.ts +1 -29
- package/src/observability/types/metric.ts +1 -56
- package/src/observability/types/provider.ts +1 -32
- package/src/observability/types/span.ts +1 -64
- package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +2 -2
- package/src/pipeline/__tests__/dispatcher.test.ts +366 -1
- package/src/pipeline/__tests__/event-consumer-state.integration.test.ts +31 -0
- package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +126 -0
- package/src/pipeline/__tests__/event-dispatcher-rearm.integration.test.ts +263 -0
- package/src/pipeline/__tests__/event-dispatcher.integration.test.ts +10 -0
- package/src/pipeline/__tests__/job-trigger-consumer.integration.test.ts +106 -0
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +308 -77
- package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +16 -8
- package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
- package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +80 -2
- package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +274 -0
- package/src/pipeline/__tests__/try-append-event.integration.test.ts +166 -0
- package/src/pipeline/dispatch-batch.ts +187 -0
- package/src/pipeline/dispatch-query.ts +165 -0
- package/src/pipeline/dispatch-shared.ts +826 -0
- package/src/pipeline/dispatch-stream.ts +90 -0
- package/src/pipeline/dispatch-write.ts +451 -0
- package/src/pipeline/dispatcher-utils.ts +1 -1
- package/src/pipeline/dispatcher.ts +44 -1372
- package/src/pipeline/entity-cache.ts +2 -33
- package/src/pipeline/event-consumer-state.ts +30 -2
- package/src/pipeline/event-dispatcher-admin.ts +293 -0
- package/src/pipeline/event-dispatcher-delivery.ts +305 -0
- package/src/pipeline/event-dispatcher.ts +85 -542
- package/src/pipeline/index.ts +2 -0
- package/src/pipeline/msp-rebuild.ts +42 -3
- package/src/pipeline/multi-stream-apply-context.ts +4 -42
- package/src/pipeline/projection-rebuild.ts +105 -3
- package/src/pipeline/system-hooks.ts +144 -1
- package/src/rate-limit/__tests__/resolver.integration.test.ts +18 -0
- package/src/rate-limit/resolver.ts +16 -32
- package/src/schema-cli.ts +76 -16
- package/src/search/__tests__/meilisearch-adapter.integration.test.ts +207 -185
- package/src/search/__tests__/meilisearch-ids.test.ts +30 -0
- package/src/search/__tests__/reindex-entity.integration.test.ts +144 -0
- package/src/search/index.ts +6 -0
- package/src/search/meilisearch-adapter.ts +13 -12
- package/src/search/reindex-entity.ts +177 -0
- package/src/search/types.ts +1 -39
- package/src/secrets/__tests__/contains-secret.test.ts +34 -0
- package/src/secrets/__tests__/envelope-cipher.test.ts +59 -0
- package/src/secrets/__tests__/envelope.test.ts +1 -1
- package/src/secrets/dek-cache.ts +26 -5
- package/src/secrets/envelope-cipher.ts +53 -0
- package/src/secrets/envelope.ts +5 -3
- package/src/secrets/index.ts +13 -1
- package/src/secrets/stored-envelope.ts +46 -0
- package/src/secrets/types.ts +2 -162
- package/src/stack/__tests__/event-collector.test.ts +42 -0
- package/src/stack/__tests__/setup-test-stack-jobs.integration.test.ts +125 -0
- package/src/stack/db.ts +2 -1
- package/src/stack/push-entity-projection-tables.ts +4 -3
- package/src/stack/redis.ts +8 -0
- package/src/stack/request-helper.ts +38 -2
- package/src/stack/table-helpers.ts +6 -4
- package/src/stack/test-stack.ts +249 -131
- package/src/testing/__tests__/late-bound.test.ts +32 -0
- package/src/testing/__tests__/wait-for.test.ts +59 -0
- package/src/testing/boot-validator-fixture.ts +121 -0
- package/src/testing/e2e-generator.ts +8 -0
- package/src/testing/file-provider-contract.ts +104 -0
- package/src/testing/handler-context.ts +3 -1
- package/src/testing/index.ts +5 -0
- package/src/testing/late-bound.ts +5 -3
- package/src/testing/mutable-master-key-provider.ts +29 -3
- package/src/testing/wait-for.ts +3 -0
- package/src/testing/without-ambient-temporal.ts +14 -0
- package/src/time/__tests__/tz-dateline.test.ts +8 -10
- package/src/time/geo-tz.ts +1 -32
- package/src/time/index.ts +1 -0
- package/src/time/legacy-date.ts +18 -0
- package/src/time/polyfill.ts +21 -38
- package/src/time/tz-context.ts +44 -85
- package/src/ui-types/app-schema.ts +12 -0
- package/src/ui-types/index.ts +20 -7
- package/src/utils/__tests__/safe-json-temporal.test.ts +18 -0
- package/src/utils/safe-json.ts +13 -1
- package/src/__tests__/raw-table.integration.test.ts +0 -116
- package/src/bun-db/__tests__/bun-test-stack.ts +0 -6
- package/src/db/__tests__/encryption.test.ts +0 -39
- package/src/db/encryption.ts +0 -39
- package/src/db/row-helpers.ts +0 -4
- package/src/engine/__tests__/raw-table.test.ts +0 -150
- package/src/engine/__tests__/unmanaged-table.test.ts +0 -127
- package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +0 -184
- package/src/engine/feature-ast/extractors/round4.ts +0 -1366
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// Unit tests for r.storeTable() — declaration-time validation + registry
|
|
2
|
+
// aggregation. Post-drizzle-cut merge of the former r.storeTable() (legacy
|
|
3
|
+
// Drizzle PgTable) and r.unmanagedTable() (EntityTableMeta) — this file
|
|
4
|
+
// absorbs the former unmanaged-table.test.ts coverage under the new name.
|
|
5
|
+
// Full DB roundtrip (setupTestStack pushes the table → INSERT / SELECT)
|
|
6
|
+
// lives in src/__tests__/store-table.integration.test.ts.
|
|
7
|
+
|
|
8
|
+
import { describe, expect, test } from "bun:test";
|
|
9
|
+
import {
|
|
10
|
+
buildEntityTableMeta,
|
|
11
|
+
defineUnmanagedTable,
|
|
12
|
+
resolveTableName,
|
|
13
|
+
} from "../../db/entity-table-meta";
|
|
14
|
+
import { defineFeature } from "../define-feature";
|
|
15
|
+
import { createEntity, createTextField } from "../index";
|
|
16
|
+
import { createRegistry } from "../registry";
|
|
17
|
+
|
|
18
|
+
const probeMeta = defineUnmanagedTable({
|
|
19
|
+
tableName: "rt_probe",
|
|
20
|
+
columns: [{ name: "id", pgType: "text", notNull: true, primaryKey: true }],
|
|
21
|
+
});
|
|
22
|
+
const probeMetaTwo = defineUnmanagedTable({
|
|
23
|
+
tableName: "rt_probe_two",
|
|
24
|
+
columns: [{ name: "id", pgType: "text", notNull: true, primaryKey: true }],
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("r.storeTable — declaration", () => {
|
|
28
|
+
test("rejects an invalid table name", () => {
|
|
29
|
+
const badMeta = defineUnmanagedTable({
|
|
30
|
+
tableName: "BadName",
|
|
31
|
+
columns: [{ name: "id", pgType: "text", notNull: true, primaryKey: true }],
|
|
32
|
+
});
|
|
33
|
+
expect(() =>
|
|
34
|
+
defineFeature("probe", (r) => {
|
|
35
|
+
r.storeTable(badMeta, { reason: "test" });
|
|
36
|
+
}),
|
|
37
|
+
).toThrow(/must be a valid identifier/);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("rejects duplicate registrations within one feature", () => {
|
|
41
|
+
expect(() =>
|
|
42
|
+
defineFeature("probe", (r) => {
|
|
43
|
+
r.storeTable(probeMeta, { reason: "test" });
|
|
44
|
+
r.storeTable(probeMeta, { reason: "test" });
|
|
45
|
+
}),
|
|
46
|
+
).toThrow(/already registered/);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("rejects empty reason", () => {
|
|
50
|
+
expect(() =>
|
|
51
|
+
defineFeature("probe", (r) => {
|
|
52
|
+
r.storeTable(probeMeta, { reason: "" });
|
|
53
|
+
}),
|
|
54
|
+
).toThrow(/options\.reason must be a non-empty string/);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("rejects whitespace-only reason", () => {
|
|
58
|
+
expect(() =>
|
|
59
|
+
defineFeature("probe", (r) => {
|
|
60
|
+
r.storeTable(probeMeta, { reason: " " });
|
|
61
|
+
}),
|
|
62
|
+
).toThrow(/options\.reason must be a non-empty string/);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("rejects an EntityTableMeta whose source is not 'unmanaged' (#1209)", () => {
|
|
66
|
+
const managedEntity = createEntity({
|
|
67
|
+
table: "rt_probe_managed",
|
|
68
|
+
fields: { name: createTextField() },
|
|
69
|
+
});
|
|
70
|
+
const managedMeta = buildEntityTableMeta("rt-probe-managed", managedEntity);
|
|
71
|
+
expect(() =>
|
|
72
|
+
defineFeature("probe", (r) => {
|
|
73
|
+
r.storeTable(managedMeta, { reason: "test" });
|
|
74
|
+
}),
|
|
75
|
+
).toThrow(/requires source: "unmanaged"/);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("rejects a table name with the reserved read_ prefix (#1220)", () => {
|
|
79
|
+
const readPrefixed = defineUnmanagedTable({
|
|
80
|
+
tableName: "read_rt_probe",
|
|
81
|
+
columns: [{ name: "id", pgType: "text", notNull: true, primaryKey: true }],
|
|
82
|
+
});
|
|
83
|
+
expect(() =>
|
|
84
|
+
defineFeature("probe", (r) => {
|
|
85
|
+
r.storeTable(readPrefixed, { reason: "test" });
|
|
86
|
+
}),
|
|
87
|
+
).toThrow(/the "read_" prefix is reserved/);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("accepts valid registration and stores meta + reason", () => {
|
|
91
|
+
const feature = defineFeature("probe", (r) => {
|
|
92
|
+
r.storeTable(probeMeta, {
|
|
93
|
+
reason: "read-side projection of an event-stream",
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
expect(feature.storeTables).toHaveProperty("rt_probe");
|
|
97
|
+
expect(feature.storeTables["rt_probe"]?.reason).toBe("read-side projection of an event-stream");
|
|
98
|
+
expect(feature.storeTables["rt_probe"]?.meta).toBe(probeMeta);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("two store tables on one feature register under their tableName", () => {
|
|
102
|
+
const feature = defineFeature("dual", (r) => {
|
|
103
|
+
r.storeTable(probeMeta, { reason: "one" });
|
|
104
|
+
r.storeTable(probeMetaTwo, { reason: "two" });
|
|
105
|
+
});
|
|
106
|
+
expect(Object.keys(feature.storeTables).sort()).toEqual(["rt_probe", "rt_probe_two"]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("absent storeTables on a feature is ok", () => {
|
|
110
|
+
const feat = defineFeature("plain", () => {
|
|
111
|
+
// no r.storeTable calls
|
|
112
|
+
});
|
|
113
|
+
expect(feat.storeTables).toEqual({});
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe("createRegistry — storeTable aggregation", () => {
|
|
118
|
+
test("aggregates store tables across features and tags featureName", () => {
|
|
119
|
+
const featA = defineFeature("billing", (r) => {
|
|
120
|
+
r.storeTable(probeMeta, { reason: "external API cache" });
|
|
121
|
+
});
|
|
122
|
+
const featB = defineFeature("inventory", (r) => {
|
|
123
|
+
r.storeTable(probeMetaTwo, { reason: "imported pre-ES" });
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const registry = createRegistry([featA, featB]);
|
|
127
|
+
const all = registry.getAllStoreTables();
|
|
128
|
+
|
|
129
|
+
expect(all.size).toBe(2);
|
|
130
|
+
expect(all.get("rt_probe")?.featureName).toBe("billing");
|
|
131
|
+
expect(all.get("rt_probe")?.reason).toBe("external API cache");
|
|
132
|
+
expect(all.get("rt_probe_two")?.featureName).toBe("inventory");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("rejects cross-feature tableName collisions at boot", () => {
|
|
136
|
+
// Two features can't share the same physical tableName — migrate-runner
|
|
137
|
+
// would race two CREATE TABLE statements. Boot-validator catches it.
|
|
138
|
+
const featA = defineFeature("a", (r) => {
|
|
139
|
+
r.storeTable(probeMeta, { reason: "first" });
|
|
140
|
+
});
|
|
141
|
+
const featB = defineFeature("b", (r) => {
|
|
142
|
+
r.storeTable(probeMeta, { reason: "second" });
|
|
143
|
+
});
|
|
144
|
+
expect(() => createRegistry([featA, featB])).toThrow(
|
|
145
|
+
/Store-table "rt_probe" registered by both feature "a" and "b"/,
|
|
146
|
+
);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("two features with distinct tableNames register cleanly", () => {
|
|
150
|
+
const featA = defineFeature("a", (r) => {
|
|
151
|
+
r.storeTable(probeMeta, { reason: "first" });
|
|
152
|
+
});
|
|
153
|
+
const featB = defineFeature("b", (r) => {
|
|
154
|
+
r.storeTable(probeMetaTwo, { reason: "second" });
|
|
155
|
+
});
|
|
156
|
+
expect(() => createRegistry([featA, featB])).not.toThrow();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("rejects a store-table that collides with an entity's physical name", () => {
|
|
160
|
+
// Custom `table` override without the read_ prefix — resolveTableName's
|
|
161
|
+
// default would carry read_, which r.storeTable now rejects outright
|
|
162
|
+
// (#1220), so the collision case needs a table name storeTable can
|
|
163
|
+
// actually register.
|
|
164
|
+
const widget = createEntity({ table: "shop_widgets", fields: { name: createTextField() } });
|
|
165
|
+
// resolveTableName mirrors the migrate-runner — pin the exact physical name.
|
|
166
|
+
const physical = resolveTableName("widget", widget, "shop");
|
|
167
|
+
const clashing = defineUnmanagedTable({
|
|
168
|
+
tableName: physical,
|
|
169
|
+
columns: [{ name: "id", pgType: "text", notNull: true, primaryKey: true }],
|
|
170
|
+
});
|
|
171
|
+
const entityFeature = defineFeature("shop", (r) => {
|
|
172
|
+
r.entity("widget", widget);
|
|
173
|
+
});
|
|
174
|
+
const tableFeature = defineFeature("other", (r) => {
|
|
175
|
+
r.storeTable(clashing, { reason: "clash" });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Entity registered first, then the colliding store table.
|
|
179
|
+
expect(() => createRegistry([entityFeature, tableFeature])).toThrow(
|
|
180
|
+
new RegExp(`Store-table "${physical}".*collides with the physical table of entity "widget"`),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Order-independent: store table registered first, then the entity.
|
|
184
|
+
expect(() => createRegistry([tableFeature, entityFeature])).toThrow(
|
|
185
|
+
new RegExp(`Entity "widget".*collides with r.storeTable\\("${physical}"\\)`),
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("createRegistry — store tables with PII-annotated fields (#820)", () => {
|
|
191
|
+
const piiEntity = createEntity({
|
|
192
|
+
table: "rt_pii_probe",
|
|
193
|
+
fields: {
|
|
194
|
+
userId: createTextField({ required: true }),
|
|
195
|
+
ip: createTextField({ userOwned: { ownerField: "userId" } }),
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
const piiMeta = buildEntityTableMeta("rt-pii-probe", piiEntity, { source: "unmanaged" });
|
|
199
|
+
|
|
200
|
+
test("buildEntityTableMeta records the subject-annotated field names", () => {
|
|
201
|
+
expect(piiMeta.piiSubjectFields).toEqual(["ip"]);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("rejects a PII-carrying store table without piiEncryptedOnWrite", () => {
|
|
205
|
+
const feat = defineFeature("probe", (r) => {
|
|
206
|
+
r.storeTable(piiMeta, { reason: "direct-write store" });
|
|
207
|
+
});
|
|
208
|
+
expect(() => createRegistry([feat])).toThrow(
|
|
209
|
+
/has PII-annotated fields \(ip\) but direct writes bypass the executor's PII encryption/,
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("accepts it once the feature declares piiEncryptedOnWrite", () => {
|
|
214
|
+
const feat = defineFeature("probe", (r) => {
|
|
215
|
+
r.storeTable(piiMeta, {
|
|
216
|
+
reason: "direct-write store",
|
|
217
|
+
piiEncryptedOnWrite: true,
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
expect(() => createRegistry([feat])).not.toThrow();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("a PII-free store table needs no declaration", () => {
|
|
224
|
+
const feat = defineFeature("probe", (r) => {
|
|
225
|
+
r.storeTable(probeMeta, { reason: "plain store" });
|
|
226
|
+
});
|
|
227
|
+
expect(() => createRegistry([feat])).not.toThrow();
|
|
228
|
+
});
|
|
229
|
+
});
|
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
import { describe, expect, test } from "bun:test";
|
|
7
7
|
import { createEntity, defineFeature } from "../index";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
findTierResolverUsage,
|
|
10
|
+
isTierResolverPlugin,
|
|
11
|
+
TENANT_TIER_RESOLVER_EXT,
|
|
12
|
+
} from "../tier-resolver-extension";
|
|
9
13
|
|
|
10
14
|
function tierResolverFeature(name: string) {
|
|
11
15
|
return defineFeature(name, (r) => {
|
|
@@ -49,3 +53,17 @@ describe("findTierResolverUsage", () => {
|
|
|
49
53
|
expect(findTierResolverUsage([])).toBeUndefined();
|
|
50
54
|
});
|
|
51
55
|
});
|
|
56
|
+
|
|
57
|
+
describe("isTierResolverPlugin", () => {
|
|
58
|
+
test("true when build is a function", () => {
|
|
59
|
+
expect(isTierResolverPlugin({ build: async () => new Set() })).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("false for null, non-objects, and missing/non-fn build", () => {
|
|
63
|
+
expect(isTierResolverPlugin(null)).toBe(false);
|
|
64
|
+
expect(isTierResolverPlugin(undefined)).toBe(false);
|
|
65
|
+
expect(isTierResolverPlugin("x")).toBe(false);
|
|
66
|
+
expect(isTierResolverPlugin({})).toBe(false);
|
|
67
|
+
expect(isTierResolverPlugin({ build: 1 })).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -1,15 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
import { buildTarget, createRegistry, defineFeature } from "../index";
|
|
3
|
-
import type { TreeChildrenSubscribe, TreeNode } from "../types/tree-node";
|
|
4
|
-
|
|
5
|
-
// Stub-Provider für Tests. Form: (ctx) => (emit) => unsubscribe.
|
|
6
|
-
// Emittet einmal initial, kein Cleanup nötig (no-op unsubscribe).
|
|
7
|
-
function makeStubProvider(nodes: readonly TreeNode[]): TreeChildrenSubscribe {
|
|
8
|
-
return () => (emit) => {
|
|
9
|
-
emit(nodes);
|
|
10
|
-
return () => {};
|
|
11
|
-
};
|
|
12
|
-
}
|
|
13
3
|
|
|
14
4
|
describe("r.treeActions — registrar slot", () => {
|
|
15
5
|
test("feature without r.treeActions leaves the slot undefined", () => {
|
|
@@ -63,80 +53,12 @@ describe("r.treeActions — registrar slot", () => {
|
|
|
63
53
|
});
|
|
64
54
|
});
|
|
65
55
|
|
|
66
|
-
describe("
|
|
67
|
-
test("
|
|
68
|
-
const feature = defineFeature("empty", () => {});
|
|
69
|
-
expect(feature.treeProvider).toBeUndefined();
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test("single r.tree call stores the provider function on the FeatureDefinition", () => {
|
|
73
|
-
const provider = makeStubProvider([{ label: "Marketing" }]);
|
|
74
|
-
const feature = defineFeature("text-content", (r) => {
|
|
75
|
-
r.tree(provider);
|
|
76
|
-
});
|
|
77
|
-
expect(feature.treeProvider).toBe(provider);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test("second r.tree call throws — only-once-guard", () => {
|
|
81
|
-
expect(() =>
|
|
82
|
-
defineFeature("dupe", (r) => {
|
|
83
|
-
r.tree(makeStubProvider([]));
|
|
84
|
-
r.tree(makeStubProvider([]));
|
|
85
|
-
}),
|
|
86
|
-
).toThrow(/r\.tree\(\) already called/);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test("treeActions and tree are independent slots — can declare one without the other", () => {
|
|
90
|
-
const onlyActions = defineFeature("a", (r) => {
|
|
91
|
-
r.treeActions({ list: {} });
|
|
92
|
-
});
|
|
93
|
-
expect(onlyActions.treeActions).toBeDefined();
|
|
94
|
-
expect(onlyActions.treeProvider).toBeUndefined();
|
|
95
|
-
|
|
96
|
-
const onlyProvider = defineFeature("b", (r) => {
|
|
97
|
-
r.tree(makeStubProvider([]));
|
|
98
|
-
});
|
|
99
|
-
expect(onlyProvider.treeActions).toBeUndefined();
|
|
100
|
-
expect(onlyProvider.treeProvider).toBeDefined();
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
describe("Registry.getTreeProviders + getTreeActions", () => {
|
|
105
|
-
test("empty registry returns empty providers map and undefined actions", () => {
|
|
56
|
+
describe("Registry.getTreeActions", () => {
|
|
57
|
+
test("empty registry returns undefined actions", () => {
|
|
106
58
|
const reg = createRegistry([]);
|
|
107
|
-
expect(reg.getTreeProviders().size).toBe(0);
|
|
108
59
|
expect(reg.getTreeActions("nonexistent")).toBeUndefined();
|
|
109
60
|
});
|
|
110
61
|
|
|
111
|
-
test("aggregates providers from multiple features keyed by feature name", () => {
|
|
112
|
-
const providerA = makeStubProvider([{ label: "A-root" }]);
|
|
113
|
-
const providerB = makeStubProvider([{ label: "B-root" }]);
|
|
114
|
-
const featureA = defineFeature("text-content", (r) => {
|
|
115
|
-
r.tree(providerA);
|
|
116
|
-
});
|
|
117
|
-
const featureB = defineFeature("legal-pages", (r) => {
|
|
118
|
-
r.tree(providerB);
|
|
119
|
-
});
|
|
120
|
-
const reg = createRegistry([featureA, featureB]);
|
|
121
|
-
|
|
122
|
-
const providers = reg.getTreeProviders();
|
|
123
|
-
expect(providers.size).toBe(2);
|
|
124
|
-
expect(providers.get("text-content")).toBe(providerA);
|
|
125
|
-
expect(providers.get("legal-pages")).toBe(providerB);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
test("features without r.tree are absent from the providers map (Zero-Whitelist-Filter)", () => {
|
|
129
|
-
const featureWithProvider = defineFeature("text-content", (r) => {
|
|
130
|
-
r.tree(makeStubProvider([]));
|
|
131
|
-
});
|
|
132
|
-
const featureWithoutProvider = defineFeature("schema-editor", () => {});
|
|
133
|
-
const reg = createRegistry([featureWithProvider, featureWithoutProvider]);
|
|
134
|
-
|
|
135
|
-
const providers = reg.getTreeProviders();
|
|
136
|
-
expect(providers.has("text-content")).toBe(true);
|
|
137
|
-
expect(providers.has("schema-editor")).toBe(false);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
62
|
test("getTreeActions returns the erased map for a feature that declared r.treeActions", () => {
|
|
141
63
|
const feature = defineFeature("text-content", (r) => {
|
|
142
64
|
r.treeActions({
|
|
@@ -153,9 +75,7 @@ describe("Registry.getTreeProviders + getTreeActions", () => {
|
|
|
153
75
|
});
|
|
154
76
|
|
|
155
77
|
test("getTreeActions returns undefined for a feature without r.treeActions", () => {
|
|
156
|
-
const feature = defineFeature("no-actions", (
|
|
157
|
-
r.tree(makeStubProvider([]));
|
|
158
|
-
});
|
|
78
|
+
const feature = defineFeature("no-actions", () => {});
|
|
159
79
|
const reg = createRegistry([feature]);
|
|
160
80
|
|
|
161
81
|
expect(reg.getTreeActions("no-actions")).toBeUndefined();
|
|
@@ -169,7 +89,7 @@ describe("Registry.getTreeProviders + getTreeActions", () => {
|
|
|
169
89
|
// returnt einen typed Handle, der via setup-export durch FeatureDefinition
|
|
170
90
|
// fließt und compile-time-typisiert vom Schicht-1-buildTarget konsumiert
|
|
171
91
|
// wird. Ohne diese Tests würde ein Type-Drift in einer der zwei Schichten
|
|
172
|
-
// erst
|
|
92
|
+
// erst spät sichtbar — siehe advisor-Verdict + Memory `[EventDef-
|
|
173
93
|
// Exports-Pattern]`.
|
|
174
94
|
// =============================================================================
|
|
175
95
|
|
|
@@ -219,18 +139,10 @@ describe("Schicht-1↔Schicht-2 Bridge — buildTarget against real defineFeatur
|
|
|
219
139
|
buildTarget({ target: textContent.exports.handle, action: "delet", args: { slug: "x" } });
|
|
220
140
|
|
|
221
141
|
// @ts-expect-error — slug muss string sein, nicht number
|
|
222
|
-
buildTarget({
|
|
223
|
-
target: textContent.exports.handle,
|
|
224
|
-
action: "edit",
|
|
225
|
-
args: { slug: 42 },
|
|
226
|
-
});
|
|
142
|
+
buildTarget({ target: textContent.exports.handle, action: "edit", args: { slug: 42 } });
|
|
227
143
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
action: "list",
|
|
231
|
-
// @ts-expect-error — list hat keine args, args-Feld nicht erlaubt
|
|
232
|
-
args: { x: 1 },
|
|
233
|
-
});
|
|
144
|
+
// @ts-expect-error — list hat keine args, args-Feld nicht erlaubt
|
|
145
|
+
buildTarget({ target: textContent.exports.handle, action: "list", args: { x: 1 } });
|
|
234
146
|
|
|
235
147
|
// @ts-expect-error — edit braucht args, fehlt
|
|
236
148
|
buildTarget({ target: textContent.exports.handle, action: "edit" });
|
|
@@ -11,7 +11,7 @@ import { defineFeature } from "../define-feature";
|
|
|
11
11
|
import { defineWriteHandler } from "../define-handler";
|
|
12
12
|
import { defineStep } from "../define-step";
|
|
13
13
|
import { createEntity, createTextField } from "../factories";
|
|
14
|
-
import {
|
|
14
|
+
import { stepsPipeline } from "../pipeline";
|
|
15
15
|
import { validateProjectionAllowlist } from "../validate-projection-allowlist";
|
|
16
16
|
|
|
17
17
|
describe("validateProjectionAllowlist", () => {
|
|
@@ -28,7 +28,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
28
28
|
name: "log",
|
|
29
29
|
schema: z.object({ msg: z.string() }),
|
|
30
30
|
access: { roles: ["User"] },
|
|
31
|
-
perform:
|
|
31
|
+
perform: stepsPipeline<{ msg: string }, { ok: true }>(({ event, r }) => [
|
|
32
32
|
r.step.unsafeProjectionUpsert({
|
|
33
33
|
table: demoLogTable,
|
|
34
34
|
on: ["id"],
|
|
@@ -70,7 +70,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
70
70
|
name: "sneaky",
|
|
71
71
|
schema: z.object({}),
|
|
72
72
|
access: { roles: ["User"] },
|
|
73
|
-
perform:
|
|
73
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
74
74
|
r.step.unsafeProjectionUpsert({
|
|
75
75
|
table: widgetsTable,
|
|
76
76
|
on: ["id"],
|
|
@@ -113,7 +113,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
113
113
|
name: "sneaky-delete",
|
|
114
114
|
schema: z.object({}),
|
|
115
115
|
access: { roles: ["User"] },
|
|
116
|
-
perform:
|
|
116
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
117
117
|
r.step.unsafeProjectionDelete({
|
|
118
118
|
table: widgetsTable,
|
|
119
119
|
where: () => ({ id: "anything" }),
|
|
@@ -136,7 +136,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
136
136
|
name: "purge",
|
|
137
137
|
schema: z.object({}),
|
|
138
138
|
access: { roles: ["User"] },
|
|
139
|
-
perform:
|
|
139
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
140
140
|
r.step.unsafeProjectionDelete({
|
|
141
141
|
table: demoLogTable,
|
|
142
142
|
where: () => ({ id: "anything" }),
|
|
@@ -162,7 +162,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
162
162
|
name: "branchedWrite",
|
|
163
163
|
schema: z.object({}),
|
|
164
164
|
access: { roles: ["User"] },
|
|
165
|
-
perform:
|
|
165
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
166
166
|
r.step.branch({
|
|
167
167
|
if: () => true,
|
|
168
168
|
onTrue: [
|
|
@@ -195,7 +195,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
195
195
|
name: "nestedWrite",
|
|
196
196
|
schema: z.object({}),
|
|
197
197
|
access: { roles: ["User"] },
|
|
198
|
-
perform:
|
|
198
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
199
199
|
r.step.forEach({
|
|
200
200
|
over: () => [],
|
|
201
201
|
as: "item",
|
|
@@ -230,7 +230,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
230
230
|
name: "loopedWrite",
|
|
231
231
|
schema: z.object({}),
|
|
232
232
|
access: { roles: ["User"] },
|
|
233
|
-
perform:
|
|
233
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
234
234
|
r.step.forEach({
|
|
235
235
|
over: () => [],
|
|
236
236
|
as: "x",
|
|
@@ -267,7 +267,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
267
267
|
name: "futureBuilder",
|
|
268
268
|
schema: z.object({}),
|
|
269
269
|
access: { roles: ["User"] },
|
|
270
|
-
perform:
|
|
270
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
271
271
|
// Hand-crafted StepInstance simulating a future builder
|
|
272
272
|
// whose kind isn't registered via defineStep yet.
|
|
273
273
|
{
|
|
@@ -316,7 +316,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
316
316
|
name: "futureBuilder",
|
|
317
317
|
schema: z.object({}),
|
|
318
318
|
access: { roles: ["User"] },
|
|
319
|
-
perform:
|
|
319
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
320
320
|
{
|
|
321
321
|
kind: futureKind,
|
|
322
322
|
args: {
|
|
@@ -380,7 +380,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
380
380
|
name: "sneak",
|
|
381
381
|
schema: z.object({}),
|
|
382
382
|
access: { roles: ["Admin"] },
|
|
383
|
-
perform:
|
|
383
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
384
384
|
r.step.webhook.send({
|
|
385
385
|
url: "https://hooks.example/sneak",
|
|
386
386
|
mode: "deferred",
|
|
@@ -404,7 +404,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
404
404
|
name: "sneak",
|
|
405
405
|
schema: z.object({}),
|
|
406
406
|
access: { roles: ["Admin"] },
|
|
407
|
-
perform:
|
|
407
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
408
408
|
r.step.mail.send({
|
|
409
409
|
to: "x@y.com",
|
|
410
410
|
subject: "hi",
|
|
@@ -428,7 +428,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
428
428
|
name: "sneak",
|
|
429
429
|
schema: z.object({}),
|
|
430
430
|
access: { roles: ["Admin"] },
|
|
431
|
-
perform:
|
|
431
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
432
432
|
r.step.callFeature("subResult", {
|
|
433
433
|
handler: "other:write:do",
|
|
434
434
|
payload: () => ({}),
|
|
@@ -451,7 +451,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
451
451
|
name: "ok",
|
|
452
452
|
schema: z.object({}),
|
|
453
453
|
access: { roles: ["Admin"] },
|
|
454
|
-
perform:
|
|
454
|
+
perform: stepsPipeline<Record<string, never>, { ok: true }>(({ r }) => [
|
|
455
455
|
r.step.webhook.send({
|
|
456
456
|
url: "https://hooks.example/ok",
|
|
457
457
|
mode: "deferred",
|
|
@@ -473,7 +473,7 @@ describe("validateProjectionAllowlist", () => {
|
|
|
473
473
|
name: "log",
|
|
474
474
|
schema: z.object({ msg: z.string() }),
|
|
475
475
|
access: { roles: ["User"] },
|
|
476
|
-
perform:
|
|
476
|
+
perform: stepsPipeline<{ msg: string }, { ok: true }>(({ event, r }) => [
|
|
477
477
|
r.step.unsafeProjectionUpsert({
|
|
478
478
|
table: demoLogTable,
|
|
479
479
|
on: ["id"],
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { createSystemConfig, createTenantConfig, createUserConfig } from "../../config-helpers";
|
|
3
|
+
import type { FeatureDefinition } from "../../types";
|
|
4
|
+
import {
|
|
5
|
+
validateConfigKeyAllowPerRequest,
|
|
6
|
+
validateConfigKeyComputed,
|
|
7
|
+
validateConfigKeyPiiEncrypted,
|
|
8
|
+
} from "../config-deps";
|
|
9
|
+
|
|
10
|
+
function fakeFeature(configKeys: FeatureDefinition["configKeys"]): FeatureDefinition {
|
|
11
|
+
return { name: "test-feature", configKeys } as unknown as FeatureDefinition;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("validateConfigKeyComputed", () => {
|
|
15
|
+
test("rejects computed + backing:secrets (encrypted-at-rest via backing, not the encrypted flag)", () => {
|
|
16
|
+
const feature = fakeFeature({
|
|
17
|
+
apiKey: createSystemConfig("text", { backing: "secrets", computed: async () => "x" }),
|
|
18
|
+
});
|
|
19
|
+
expect(() => validateConfigKeyComputed(feature)).toThrow(/mutually exclusive/);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("rejects computed + encrypted:true", () => {
|
|
23
|
+
const feature = fakeFeature({
|
|
24
|
+
apiKey: createSystemConfig("text", { encrypted: true, computed: async () => "x" }),
|
|
25
|
+
});
|
|
26
|
+
expect(() => validateConfigKeyComputed(feature)).toThrow(/mutually exclusive/);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("allows computed without encryption", () => {
|
|
30
|
+
const feature = fakeFeature({
|
|
31
|
+
apiKey: createSystemConfig("text", { computed: async () => "x" }),
|
|
32
|
+
});
|
|
33
|
+
expect(() => validateConfigKeyComputed(feature)).not.toThrow();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("validateConfigKeyAllowPerRequest", () => {
|
|
38
|
+
test("rejects allowPerRequest + backing:secrets on a number key", () => {
|
|
39
|
+
const feature = fakeFeature({
|
|
40
|
+
rateLimit: createSystemConfig("number", { backing: "secrets", allowPerRequest: true }),
|
|
41
|
+
});
|
|
42
|
+
expect(() => validateConfigKeyAllowPerRequest(feature)).toThrow(
|
|
43
|
+
/may not be set via query-params/,
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("validateConfigKeyPiiEncrypted (kumiko-platform#231/#459)", () => {
|
|
49
|
+
test("allows piiEncrypted on a tenant-scoped text key", () => {
|
|
50
|
+
const feature = fakeFeature({
|
|
51
|
+
billingAddress: createTenantConfig("text", { piiEncrypted: true }),
|
|
52
|
+
});
|
|
53
|
+
expect(() => validateConfigKeyPiiEncrypted(feature)).not.toThrow();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("allows piiEncrypted on a user-scoped text key", () => {
|
|
57
|
+
const feature = fakeFeature({
|
|
58
|
+
phoneNumber: createUserConfig("text", { piiEncrypted: true }),
|
|
59
|
+
});
|
|
60
|
+
expect(() => validateConfigKeyPiiEncrypted(feature)).not.toThrow();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("rejects piiEncrypted on a non-text key", () => {
|
|
64
|
+
const feature = fakeFeature({
|
|
65
|
+
rateLimit: { ...createTenantConfig("number"), piiEncrypted: true },
|
|
66
|
+
});
|
|
67
|
+
expect(() => validateConfigKeyPiiEncrypted(feature)).toThrow(
|
|
68
|
+
/piiEncrypted.*only applies to text keys/,
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("rejects piiEncrypted + scope:system", () => {
|
|
73
|
+
const feature = fakeFeature({
|
|
74
|
+
apiKey: createSystemConfig("text", { piiEncrypted: true }),
|
|
75
|
+
});
|
|
76
|
+
expect(() => validateConfigKeyPiiEncrypted(feature)).toThrow(/piiEncrypted.*scope="system"/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("rejects piiEncrypted + encrypted", () => {
|
|
80
|
+
const feature = fakeFeature({
|
|
81
|
+
iban: createTenantConfig("text", { piiEncrypted: true, encrypted: true }),
|
|
82
|
+
});
|
|
83
|
+
expect(() => validateConfigKeyPiiEncrypted(feature)).toThrow(/piiEncrypted=true and encrypted/);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("rejects piiEncrypted + backing:secrets", () => {
|
|
87
|
+
const feature = fakeFeature({
|
|
88
|
+
iban: { ...createTenantConfig("text", { piiEncrypted: true }), backing: "secrets" as const },
|
|
89
|
+
});
|
|
90
|
+
expect(() => validateConfigKeyPiiEncrypted(feature)).toThrow(/piiEncrypted=true and encrypted/);
|
|
91
|
+
});
|
|
92
|
+
});
|