@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,403 @@
|
|
|
1
|
+
import { ZodObject, type ZodType, type z } from "zod";
|
|
2
|
+
import type { FeatureBuilderState } from "./feature-builder-state";
|
|
3
|
+
import { resolveName } from "./handler-helpers";
|
|
4
|
+
import { splitNamedDefinition, unwrapArrayForm } from "./object-form";
|
|
5
|
+
import { QnTypes, qn, toKebab } from "./qualified-name";
|
|
6
|
+
import type {
|
|
7
|
+
AuthClaimsFn,
|
|
8
|
+
ClaimKeyHandle,
|
|
9
|
+
ClaimKeyType,
|
|
10
|
+
ConfigKeyDefinition,
|
|
11
|
+
ConfigKeyHandle,
|
|
12
|
+
ConfigKeyType,
|
|
13
|
+
ConfigSeedDef,
|
|
14
|
+
DeclarativeEventMigration,
|
|
15
|
+
EventDef,
|
|
16
|
+
EventPiiFields,
|
|
17
|
+
EventUpcastFn,
|
|
18
|
+
JobDefinition,
|
|
19
|
+
JobHandlerFn,
|
|
20
|
+
MetricOptions,
|
|
21
|
+
NameOrRef,
|
|
22
|
+
NotificationDataFn,
|
|
23
|
+
NotificationRecipientFn,
|
|
24
|
+
NotificationTemplateFn,
|
|
25
|
+
QualifiedEventName,
|
|
26
|
+
SecretKeyHandle,
|
|
27
|
+
SecretOptions,
|
|
28
|
+
TranslationsDef,
|
|
29
|
+
} from "./types";
|
|
30
|
+
|
|
31
|
+
// Builds config/secrets/claims/events/jobs/notifications registrar methods.
|
|
32
|
+
export function buildConfigEventsJobsMethods<TName extends string>(
|
|
33
|
+
state: FeatureBuilderState,
|
|
34
|
+
name: TName,
|
|
35
|
+
) {
|
|
36
|
+
// Overloaded: (keyName, def) for the single-key case, ({keys, seeds}) for
|
|
37
|
+
// the multi-key case — both funnel through the same qualify/register loop
|
|
38
|
+
// below, so a single-key call is byte-identical to what
|
|
39
|
+
// `{keys:{name:def}}` would have produced.
|
|
40
|
+
function config<T extends ConfigKeyType>(
|
|
41
|
+
keyName: string,
|
|
42
|
+
def: ConfigKeyDefinition<T>,
|
|
43
|
+
): ConfigKeyHandle<T>;
|
|
44
|
+
function config<
|
|
45
|
+
TKeys extends Readonly<Record<string, ConfigKeyDefinition<ConfigKeyType>>>,
|
|
46
|
+
>(definition: {
|
|
47
|
+
readonly keys: TKeys;
|
|
48
|
+
readonly seeds?: Readonly<Record<string, ConfigSeedDef>>;
|
|
49
|
+
}): { readonly [K in keyof TKeys]: ConfigKeyHandle<TKeys[K]["type"]> };
|
|
50
|
+
function config(
|
|
51
|
+
arg1:
|
|
52
|
+
| string
|
|
53
|
+
| {
|
|
54
|
+
readonly keys: Record<string, ConfigKeyDefinition<ConfigKeyType>>;
|
|
55
|
+
readonly seeds?: Readonly<Record<string, ConfigSeedDef>>;
|
|
56
|
+
},
|
|
57
|
+
arg2?: ConfigKeyDefinition<ConfigKeyType>,
|
|
58
|
+
): unknown {
|
|
59
|
+
// arg2 is always defined here — the two public overloads above guarantee
|
|
60
|
+
// it whenever arg1 is a string; the impl signature just has to widen it
|
|
61
|
+
// to optional to satisfy both call shapes.
|
|
62
|
+
const definition = typeof arg1 === "string" && arg2 ? { keys: { [arg1]: arg2 } } : arg1;
|
|
63
|
+
if (typeof definition === "string") {
|
|
64
|
+
throw new Error("config(): single-key form requires a definition as the second argument");
|
|
65
|
+
}
|
|
66
|
+
// Qualify eagerly (same as defineEvent) so the handle name matches what
|
|
67
|
+
// the registry stores — lazy qualification would break compile-time
|
|
68
|
+
// autocomplete and hand-built test registries.
|
|
69
|
+
const handles: Record<string, ConfigKeyHandle<ConfigKeyType>> = {};
|
|
70
|
+
for (const [key, keyDef] of Object.entries(definition.keys)) {
|
|
71
|
+
state.configKeys[key] = keyDef;
|
|
72
|
+
handles[key] = {
|
|
73
|
+
name: qn(toKebab(name), "config", toKebab(key)),
|
|
74
|
+
type: keyDef.type,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
// Parse seeds: resolve qualified key names and validate scope
|
|
78
|
+
if (definition.seeds) {
|
|
79
|
+
for (const [shortKey, seedDef] of Object.entries(definition.seeds)) {
|
|
80
|
+
const keyDef = definition.keys[shortKey];
|
|
81
|
+
if (!keyDef) continue; // skip — boot-validator reports unknown keys
|
|
82
|
+
const qualifiedKey = qn(toKebab(name), "config", toKebab(shortKey));
|
|
83
|
+
const scope = seedDef.scope ?? keyDef.scope;
|
|
84
|
+
state.configSeeds.push({
|
|
85
|
+
key: qualifiedKey,
|
|
86
|
+
value: seedDef.value,
|
|
87
|
+
scope,
|
|
88
|
+
tenantId: seedDef.tenantId,
|
|
89
|
+
userId: seedDef.userId,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Single-key call unwraps its own handle; multi-key returns the record.
|
|
94
|
+
return typeof arg1 === "string" ? handles[arg1] : handles;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// piiFields misconfiguration is a boot-time error, not a silent
|
|
98
|
+
// plaintext leak: both the pii field and its subjectField must exist
|
|
99
|
+
// on the payload schema (checkable when the schema is a ZodObject).
|
|
100
|
+
function validateEventPiiFields(
|
|
101
|
+
eventName: string,
|
|
102
|
+
schema: ZodType,
|
|
103
|
+
piiFields: EventPiiFields,
|
|
104
|
+
): void {
|
|
105
|
+
const shape = schema instanceof ZodObject ? schema.shape : undefined;
|
|
106
|
+
for (const [field, spec] of Object.entries(piiFields)) {
|
|
107
|
+
if (field === spec.subjectField) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`[Feature ${name}] defineEvent("${eventName}"): piiFields."${field}" cannot use itself as subjectField — the subject id is a plaintext pseudonymous fk, the pii field is the value it owns.`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
for (const required of [field, spec.subjectField]) {
|
|
113
|
+
if (shape && !(required in shape)) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`[Feature ${name}] defineEvent("${eventName}"): piiFields references "${required}" which is not a field of the payload schema.`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Shared by defineEvent's `migrations` option — each entry registers a
|
|
123
|
+
// single upcast step, same validation/dedup as the old standalone
|
|
124
|
+
// r.eventMigration() call (folded into defineEvent, #1082 step 8).
|
|
125
|
+
function registerEventMigration(
|
|
126
|
+
eventName: string,
|
|
127
|
+
fromVersion: number,
|
|
128
|
+
toVersion: number,
|
|
129
|
+
transform: EventUpcastFn | DeclarativeEventMigration,
|
|
130
|
+
): void {
|
|
131
|
+
if (toVersion !== fromVersion + 1) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`[Feature ${name}] defineEvent("${eventName}") migrations: only single-step migrations are allowed — toVersion must be fromVersion + 1 (got ${fromVersion} -> ${toVersion}). ` +
|
|
134
|
+
`Chain larger jumps by declaring each step separately.`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (!Number.isInteger(fromVersion) || fromVersion < 1) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`[Feature ${name}] defineEvent("${eventName}") migrations: fromVersion must be >= 1, got ${String(fromVersion)}`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const qualified = qn(toKebab(name), "event", toKebab(eventName));
|
|
143
|
+
const list = state.eventMigrations[eventName] ?? [];
|
|
144
|
+
if (list.some((m) => m.fromVersion === fromVersion)) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`[Feature ${name}] defineEvent("${eventName}") migrations: a migration from v${fromVersion} is already declared. Each step may only be declared once.`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
const transformFn =
|
|
150
|
+
typeof transform === "function" ? transform : compileEventMigration(transform);
|
|
151
|
+
list.push({ eventName: qualified, fromVersion, toVersion, transform: transformFn });
|
|
152
|
+
state.eventMigrations[eventName] = list;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
config,
|
|
157
|
+
job(
|
|
158
|
+
jobNameOrDefinition: string | JobDefinition,
|
|
159
|
+
options?: Omit<JobDefinition, "name" | "handler">,
|
|
160
|
+
handler?: JobHandlerFn,
|
|
161
|
+
): void {
|
|
162
|
+
const [jobName, jobOptions, jobHandler] =
|
|
163
|
+
typeof jobNameOrDefinition === "string"
|
|
164
|
+
? [
|
|
165
|
+
jobNameOrDefinition,
|
|
166
|
+
options as Omit<JobDefinition, "name" | "handler">,
|
|
167
|
+
handler as JobHandlerFn,
|
|
168
|
+
]
|
|
169
|
+
: (() => {
|
|
170
|
+
const { name, handler: h, ...rest } = jobNameOrDefinition;
|
|
171
|
+
return [name, rest, h] as const;
|
|
172
|
+
})();
|
|
173
|
+
// Resolve NameOrRef(s) in trigger.on. Multi-Trigger-Form: Array
|
|
174
|
+
// wird zu Array von resolved strings, Single bleibt single string —
|
|
175
|
+
// job-runner unterscheidet anhand Array.isArray.
|
|
176
|
+
const trigger =
|
|
177
|
+
"on" in jobOptions.trigger
|
|
178
|
+
? {
|
|
179
|
+
on: Array.isArray(jobOptions.trigger.on)
|
|
180
|
+
? jobOptions.trigger.on.map(resolveName)
|
|
181
|
+
: resolveName(jobOptions.trigger.on as NameOrRef), // @cast-boundary engine-bridge
|
|
182
|
+
}
|
|
183
|
+
: jobOptions.trigger;
|
|
184
|
+
state.jobs[jobName] = { ...jobOptions, trigger, name: jobName, handler: jobHandler };
|
|
185
|
+
},
|
|
186
|
+
notification(
|
|
187
|
+
notificationNameOrDefinition:
|
|
188
|
+
| string
|
|
189
|
+
| ({ readonly name: string } & {
|
|
190
|
+
readonly trigger: { readonly on: NameOrRef };
|
|
191
|
+
readonly recipient: NotificationRecipientFn;
|
|
192
|
+
readonly data: NotificationDataFn;
|
|
193
|
+
readonly templates?: Readonly<Record<string, NotificationTemplateFn>>;
|
|
194
|
+
}),
|
|
195
|
+
definition?: {
|
|
196
|
+
readonly trigger: { readonly on: NameOrRef };
|
|
197
|
+
readonly recipient: NotificationRecipientFn;
|
|
198
|
+
readonly data: NotificationDataFn;
|
|
199
|
+
readonly templates?: Readonly<Record<string, NotificationTemplateFn>>;
|
|
200
|
+
},
|
|
201
|
+
): void {
|
|
202
|
+
const [notificationName, resolvedDefinition] =
|
|
203
|
+
typeof notificationNameOrDefinition === "string"
|
|
204
|
+
? [notificationNameOrDefinition, definition as NonNullable<typeof definition>]
|
|
205
|
+
: splitNamedDefinition(notificationNameOrDefinition);
|
|
206
|
+
state.notifications[notificationName] = {
|
|
207
|
+
name: notificationName,
|
|
208
|
+
trigger: { on: resolveName(resolvedDefinition.trigger.on) },
|
|
209
|
+
recipient: resolvedDefinition.recipient,
|
|
210
|
+
data: resolvedDefinition.data,
|
|
211
|
+
templates: resolvedDefinition.templates,
|
|
212
|
+
};
|
|
213
|
+
},
|
|
214
|
+
translations(def: TranslationsDef): void {
|
|
215
|
+
state.translations = { ...state.translations, ...def.keys };
|
|
216
|
+
},
|
|
217
|
+
defineEvent: <const TInner extends string, TPayload>(
|
|
218
|
+
eventName: TInner,
|
|
219
|
+
schema: ZodType<TPayload>,
|
|
220
|
+
options?: {
|
|
221
|
+
readonly version?: number;
|
|
222
|
+
readonly piiFields?: EventPiiFields;
|
|
223
|
+
// Step-wise upcast chain for this event, folded in from the former
|
|
224
|
+
// standalone r.eventMigration() call (#1082 step 8) — an event and
|
|
225
|
+
// its schema evolution are one lifecycle, not two registrar
|
|
226
|
+
// concepts. Each entry's fromVersion must be unique and the chain
|
|
227
|
+
// from 1 to `version` must be gap-free (same validation as before).
|
|
228
|
+
readonly migrations?: readonly {
|
|
229
|
+
readonly fromVersion: number;
|
|
230
|
+
readonly toVersion: number;
|
|
231
|
+
readonly transform: EventUpcastFn | DeclarativeEventMigration;
|
|
232
|
+
}[];
|
|
233
|
+
},
|
|
234
|
+
): EventDef<TPayload, QualifiedEventName<TName, TInner>> => {
|
|
235
|
+
// Return the fully-qualified event name so callers can pass it
|
|
236
|
+
// straight to ctx.appendEvent without hand-building the
|
|
237
|
+
// "<feature>:event:<name>" shape. Registry keeps events keyed by
|
|
238
|
+
// short name — qualification is the framework's job, not the feature
|
|
239
|
+
// author's.
|
|
240
|
+
//
|
|
241
|
+
// The runtime kebab-step (`qn(toKebab(feature), …)`) is mirrored at
|
|
242
|
+
// the type-level by `QualifiedEventName<TName, TInner>` so the
|
|
243
|
+
// returned `name` carries the literal qualified shape that the
|
|
244
|
+
// augmented `KumikoEventTypeMap` keys against.
|
|
245
|
+
const qualified = qn(toKebab(name), "event", toKebab(eventName));
|
|
246
|
+
const version = options?.version ?? 1;
|
|
247
|
+
if (!Number.isInteger(version) || version < 1) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`[Feature ${name}] defineEvent("${eventName}"): version must be a positive integer, got ${String(version)}`,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
const piiFields = options?.piiFields;
|
|
253
|
+
if (piiFields) {
|
|
254
|
+
validateEventPiiFields(eventName, schema, piiFields);
|
|
255
|
+
}
|
|
256
|
+
// @cast-boundary engine-bridge — runtime-string mirrors the
|
|
257
|
+
// template-literal-type via QualifiedEventName + toKebab. Both
|
|
258
|
+
// sides are tested (CamelToKebab type-tests + scan-events kebab
|
|
259
|
+
// tests), so the cast is a contract, not a typing-loss.
|
|
260
|
+
const def: EventDef<TPayload, QualifiedEventName<TName, TInner>> = {
|
|
261
|
+
name: qualified as QualifiedEventName<TName, TInner>,
|
|
262
|
+
schema,
|
|
263
|
+
version,
|
|
264
|
+
...(piiFields !== undefined && { piiFields }),
|
|
265
|
+
};
|
|
266
|
+
state.events[eventName] = def;
|
|
267
|
+
for (const m of options?.migrations ?? []) {
|
|
268
|
+
registerEventMigration(eventName, m.fromVersion, m.toVersion, m.transform);
|
|
269
|
+
}
|
|
270
|
+
return def;
|
|
271
|
+
},
|
|
272
|
+
readsConfig(
|
|
273
|
+
...args: readonly [{ readonly keys: readonly string[] }] | readonly string[]
|
|
274
|
+
): void {
|
|
275
|
+
state.configReads.push(...unwrapArrayForm(args, "keys"));
|
|
276
|
+
},
|
|
277
|
+
metric(
|
|
278
|
+
shortNameOrDefinition: string | ({ readonly name: string } & MetricOptions),
|
|
279
|
+
options?: MetricOptions,
|
|
280
|
+
): void {
|
|
281
|
+
const [shortName, metricOptions] =
|
|
282
|
+
typeof shortNameOrDefinition === "string"
|
|
283
|
+
? [shortNameOrDefinition, options as MetricOptions]
|
|
284
|
+
: splitNamedDefinition(shortNameOrDefinition);
|
|
285
|
+
if (state.metrics[shortName]) {
|
|
286
|
+
throw new Error(
|
|
287
|
+
`[Feature ${name}] Metric "${shortName}" already registered. ` +
|
|
288
|
+
`Metric names must be unique per feature.`,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
state.metrics[shortName] = { shortName, ...metricOptions };
|
|
292
|
+
},
|
|
293
|
+
envSchema(schema: z.ZodObject<z.ZodRawShape>): void {
|
|
294
|
+
if (state.envSchema !== undefined) {
|
|
295
|
+
throw new Error(
|
|
296
|
+
`[Feature ${name}] r.envSchema() called twice — declare one composed Zod-object per feature.`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
state.envSchema = schema;
|
|
300
|
+
},
|
|
301
|
+
secret(
|
|
302
|
+
shortNameOrDefinition: string | ({ readonly name: string } & SecretOptions),
|
|
303
|
+
options?: SecretOptions,
|
|
304
|
+
): SecretKeyHandle {
|
|
305
|
+
const [shortName, secretOptions] =
|
|
306
|
+
typeof shortNameOrDefinition === "string"
|
|
307
|
+
? [shortNameOrDefinition, options as SecretOptions]
|
|
308
|
+
: splitNamedDefinition(shortNameOrDefinition);
|
|
309
|
+
if (state.secretKeys[shortName]) {
|
|
310
|
+
throw new Error(
|
|
311
|
+
`[Feature ${name}] Secret "${shortName}" already registered. ` +
|
|
312
|
+
`Secret key names must be unique per feature.`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
// Qualified name follows the framework's "<feature>:<type>:<name>"
|
|
316
|
+
// QN convention — same pattern config / jobs / events use. toKebab
|
|
317
|
+
// handles the common input shapes ("stripe.apiKey" → "stripe-api-key")
|
|
318
|
+
// so features can declare keys in their natural style without
|
|
319
|
+
// thinking about kebab-case on every call.
|
|
320
|
+
const qualifiedName = qn(toKebab(name), QnTypes.secret, toKebab(shortName));
|
|
321
|
+
state.secretKeys[shortName] = {
|
|
322
|
+
shortName,
|
|
323
|
+
qualifiedName,
|
|
324
|
+
...secretOptions,
|
|
325
|
+
};
|
|
326
|
+
return { name: qualifiedName };
|
|
327
|
+
},
|
|
328
|
+
claimKey<T extends ClaimKeyType>(
|
|
329
|
+
shortNameOrDefinition: string | { readonly name: string; readonly type: T },
|
|
330
|
+
options?: { readonly type: T },
|
|
331
|
+
): ClaimKeyHandle<T> {
|
|
332
|
+
const shortName =
|
|
333
|
+
typeof shortNameOrDefinition === "string"
|
|
334
|
+
? shortNameOrDefinition
|
|
335
|
+
: shortNameOrDefinition.name;
|
|
336
|
+
const claimType: T =
|
|
337
|
+
typeof shortNameOrDefinition === "string"
|
|
338
|
+
? (options as { readonly type: T }).type
|
|
339
|
+
: shortNameOrDefinition.type;
|
|
340
|
+
if (state.claimKeys[shortName]) {
|
|
341
|
+
throw new Error(
|
|
342
|
+
`[Feature ${name}] Claim key "${shortName}" already declared. ` +
|
|
343
|
+
"Claim short-names must be unique per feature.",
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
// Claim keys are NOT full QNs — the JWT shape is 2-segment
|
|
347
|
+
// "<featureName>:<shortName>" (same as Translation keys), not
|
|
348
|
+
// kebab-cased. The authClaims resolver prefixes with the raw
|
|
349
|
+
// feature.name + the raw inner key the hook returns, so the handle's
|
|
350
|
+
// `name` must match that literal string exactly for `readClaim` to
|
|
351
|
+
// find the value. kebab-conversion here would break the round-trip.
|
|
352
|
+
const qualifiedName = `${name}:${shortName}`;
|
|
353
|
+
state.claimKeys[shortName] = {
|
|
354
|
+
shortName,
|
|
355
|
+
qualifiedName,
|
|
356
|
+
type: claimType,
|
|
357
|
+
};
|
|
358
|
+
return { name: qualifiedName, type: claimType };
|
|
359
|
+
},
|
|
360
|
+
authClaims(fn: AuthClaimsFn): void {
|
|
361
|
+
state.authClaimsHooks.push(fn);
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Compile the declarative {rename, default, map} migration spec into an
|
|
367
|
+
// EventUpcastFn. Fixed order: rename → default → map.
|
|
368
|
+
function compileEventMigration(spec: DeclarativeEventMigration): EventUpcastFn {
|
|
369
|
+
// Registration-time (not replay-time) check: two rename sources mapping to
|
|
370
|
+
// the same target would silently drop one value on every future replay —
|
|
371
|
+
// event migrations run against the full production event history, so a
|
|
372
|
+
// typo here must fail loud at defineEvent() registration time, not at replay.
|
|
373
|
+
const renameTargets = new Map<string, string>();
|
|
374
|
+
for (const [from, to] of Object.entries(spec.rename ?? {})) {
|
|
375
|
+
const existing = renameTargets.get(to);
|
|
376
|
+
if (existing !== undefined) {
|
|
377
|
+
throw new Error(
|
|
378
|
+
`Declarative event migration: rename collision — both "${existing}" and "${from}" rename to "${to}". Only one source may map to a given target.`,
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
renameTargets.set(to, from);
|
|
382
|
+
}
|
|
383
|
+
return (payload) => {
|
|
384
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
|
|
385
|
+
throw new Error("Declarative event migration expects an object payload");
|
|
386
|
+
}
|
|
387
|
+
// @cast-boundary parse — payload is guarded as a plain object above
|
|
388
|
+
const next = { ...(payload as Record<string, unknown>) };
|
|
389
|
+
for (const [from, to] of Object.entries(spec.rename ?? {})) {
|
|
390
|
+
if (from in next) {
|
|
391
|
+
next[to] = next[from];
|
|
392
|
+
delete next[from];
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
for (const [key, value] of Object.entries(spec.default ?? {})) {
|
|
396
|
+
if (!(key in next)) next[key] = value;
|
|
397
|
+
}
|
|
398
|
+
for (const [key, fn] of Object.entries(spec.map ?? {})) {
|
|
399
|
+
if (key in next) next[key] = fn(next[key]);
|
|
400
|
+
}
|
|
401
|
+
return next;
|
|
402
|
+
};
|
|
403
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import type { ZodType, z } from "zod";
|
|
2
|
+
import { toTableName } from "../db/table-builder";
|
|
3
|
+
import type {
|
|
4
|
+
QueryHandlerDefinition,
|
|
5
|
+
StreamHandlerDefinition,
|
|
6
|
+
WriteHandlerDefinition,
|
|
7
|
+
} from "./define-handler";
|
|
8
|
+
import type { RegisterEntityCrudOptions } from "./entity-handlers";
|
|
9
|
+
import { registerEntityCrud } from "./entity-handlers";
|
|
10
|
+
import type { FeatureBuilderState } from "./feature-builder-state";
|
|
11
|
+
import { resolveName } from "./handler-helpers";
|
|
12
|
+
import { splitNamedDefinition } from "./object-form";
|
|
13
|
+
import type {
|
|
14
|
+
AccessRule,
|
|
15
|
+
EntityDefinition,
|
|
16
|
+
EntityRef,
|
|
17
|
+
HandlerRef,
|
|
18
|
+
NameOrRef,
|
|
19
|
+
QueryHandlerFn,
|
|
20
|
+
RateLimitOption,
|
|
21
|
+
RelationDefinition,
|
|
22
|
+
StreamHandlerFn,
|
|
23
|
+
WriteHandlerFn,
|
|
24
|
+
} from "./types";
|
|
25
|
+
import type { PipelineDef } from "./types/step";
|
|
26
|
+
|
|
27
|
+
const CRUD_VERBS = new Set(["create", "update", "delete"]);
|
|
28
|
+
|
|
29
|
+
// Map handler name to entity via colon convention.
|
|
30
|
+
// "task:create" → entity "task". Bare CRUD verbs (create/update/delete) map
|
|
31
|
+
// when feature name matches an entity or the feature owns exactly one entity.
|
|
32
|
+
function tryMapEntity(state: FeatureBuilderState, name: string, handlerName: string): void {
|
|
33
|
+
const colonIdx = handlerName.indexOf(":");
|
|
34
|
+
if (colonIdx >= 0) {
|
|
35
|
+
const candidate = handlerName.slice(0, colonIdx);
|
|
36
|
+
if (state.entities[candidate]) {
|
|
37
|
+
state.handlerEntityMappings[handlerName] = candidate;
|
|
38
|
+
}
|
|
39
|
+
// skip: colon-prefixed handler processed (mapped or not), bare CRUD path not applicable
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (CRUD_VERBS.has(handlerName)) {
|
|
43
|
+
if (state.entities[name]) {
|
|
44
|
+
state.handlerEntityMappings[handlerName] = name;
|
|
45
|
+
// skip: feature-name entity match is the preferred mapping
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const entityKeys = Object.keys(state.entities);
|
|
49
|
+
if (entityKeys.length === 1) {
|
|
50
|
+
state.handlerEntityMappings[handlerName] = entityKeys[0] as string;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Builds entity/relation/writeHandler/queryHandler — the registrar
|
|
56
|
+
// methods that create or reference entities.
|
|
57
|
+
export function buildEntityHandlerMethods<TName extends string>(
|
|
58
|
+
state: FeatureBuilderState,
|
|
59
|
+
name: TName,
|
|
60
|
+
) {
|
|
61
|
+
const methods = {
|
|
62
|
+
entity(
|
|
63
|
+
nameOrDefinition: string | ({ readonly name: string } & EntityDefinition),
|
|
64
|
+
definition?: EntityDefinition,
|
|
65
|
+
options?: { readonly table?: unknown },
|
|
66
|
+
): EntityRef {
|
|
67
|
+
const [entityName, entityDefinition] =
|
|
68
|
+
typeof nameOrDefinition === "string"
|
|
69
|
+
? [nameOrDefinition, definition as EntityDefinition]
|
|
70
|
+
: splitNamedDefinition(nameOrDefinition);
|
|
71
|
+
state.entities[entityName] = entityDefinition;
|
|
72
|
+
if (options?.table !== undefined) state.entityTables[entityName] = options.table;
|
|
73
|
+
return { name: entityName, table: entityDefinition.table ?? toTableName(entityName) };
|
|
74
|
+
},
|
|
75
|
+
relation(
|
|
76
|
+
entityRefOrDefinition:
|
|
77
|
+
| NameOrRef
|
|
78
|
+
| ({ readonly entity: NameOrRef; readonly name: string } & RelationDefinition),
|
|
79
|
+
relationName?: string,
|
|
80
|
+
definition?: RelationDefinition,
|
|
81
|
+
): void {
|
|
82
|
+
const [entityRef, resolvedRelationName, resolvedDefinition] =
|
|
83
|
+
typeof entityRefOrDefinition === "object" && "entity" in entityRefOrDefinition
|
|
84
|
+
? (() => {
|
|
85
|
+
const { entity, name: innerName, ...rest } = entityRefOrDefinition;
|
|
86
|
+
return [entity, innerName, rest as RelationDefinition] as const;
|
|
87
|
+
})()
|
|
88
|
+
: [entityRefOrDefinition, relationName as string, definition as RelationDefinition];
|
|
89
|
+
const entityName = resolveName(entityRef);
|
|
90
|
+
if (!state.relations[entityName]) state.relations[entityName] = {};
|
|
91
|
+
state.relations[entityName][resolvedRelationName] = resolvedDefinition;
|
|
92
|
+
},
|
|
93
|
+
writeHandler<TName extends string, TSchema extends ZodType>(
|
|
94
|
+
nameOrDef: string | WriteHandlerDefinition<TName, TSchema>,
|
|
95
|
+
schema?: TSchema,
|
|
96
|
+
handler?: WriteHandlerFn<z.infer<TSchema>>,
|
|
97
|
+
options?: { access?: AccessRule; rateLimit?: RateLimitOption },
|
|
98
|
+
): HandlerRef {
|
|
99
|
+
if (typeof nameOrDef === "object") {
|
|
100
|
+
const def = nameOrDef;
|
|
101
|
+
state.writeHandlers[def.name] = {
|
|
102
|
+
name: def.name,
|
|
103
|
+
schema: def.schema,
|
|
104
|
+
// @cast-boundary engine-bridge — typed Dev-API's handler is
|
|
105
|
+
// generic over the schema's parsed payload (`WriteEvent<output<TSchema>>`),
|
|
106
|
+
// the storage form WriteHandlerFn carries `WriteEvent<unknown>`.
|
|
107
|
+
// Function-arg variance: TS sees the typed handler as stricter
|
|
108
|
+
// than the loose storage shape and rejects direct assignment.
|
|
109
|
+
// The runtime value is identical — the cast crosses that boundary.
|
|
110
|
+
// `satisfies` does not work here (it asserts assignability, which
|
|
111
|
+
// is what fails). Explicit cast is the right tool.
|
|
112
|
+
handler: def.handler as WriteHandlerFn,
|
|
113
|
+
...(def.access && { access: def.access }),
|
|
114
|
+
...(def.unsafeSkipTransitionGuard && { unsafeSkipTransitionGuard: true }),
|
|
115
|
+
...(def.rateLimit && { rateLimit: def.rateLimit }),
|
|
116
|
+
// Forward the pipeline-build closure so boot-validators and
|
|
117
|
+
// Designer/AI tooling can inspect the step list. Absent on
|
|
118
|
+
// free-form handlers — defineWriteHandler only sets `perform`
|
|
119
|
+
// when the author used the pipeline form. Variance cast
|
|
120
|
+
// mirrors the handler-cast above: PipelineDef<output<TSchema>>
|
|
121
|
+
// is stricter than PipelineDef<unknown> for the same reason.
|
|
122
|
+
...(def.perform !== undefined && {
|
|
123
|
+
perform: def.perform as PipelineDef,
|
|
124
|
+
}),
|
|
125
|
+
};
|
|
126
|
+
tryMapEntity(state, name, def.name);
|
|
127
|
+
return { name: def.name };
|
|
128
|
+
}
|
|
129
|
+
if (!schema || !handler)
|
|
130
|
+
throw new Error("writeHandler inline form requires schema + handler");
|
|
131
|
+
state.writeHandlers[nameOrDef] = {
|
|
132
|
+
name: nameOrDef,
|
|
133
|
+
schema,
|
|
134
|
+
handler: handler as WriteHandlerFn, // @cast-boundary engine-bridge
|
|
135
|
+
...(options?.access && { access: options.access }),
|
|
136
|
+
...(options?.rateLimit && { rateLimit: options.rateLimit }),
|
|
137
|
+
};
|
|
138
|
+
tryMapEntity(state, name, nameOrDef);
|
|
139
|
+
return { name: nameOrDef };
|
|
140
|
+
},
|
|
141
|
+
queryHandler<TName extends string, TSchema extends ZodType>(
|
|
142
|
+
nameOrDef: string | QueryHandlerDefinition<TName, TSchema>,
|
|
143
|
+
schema?: TSchema,
|
|
144
|
+
handler?: QueryHandlerFn<z.infer<TSchema>>,
|
|
145
|
+
options?: { access?: AccessRule; rateLimit?: RateLimitOption },
|
|
146
|
+
): HandlerRef {
|
|
147
|
+
if (typeof nameOrDef === "object") {
|
|
148
|
+
const def = nameOrDef;
|
|
149
|
+
state.queryHandlers[def.name] = {
|
|
150
|
+
name: def.name,
|
|
151
|
+
schema: def.schema,
|
|
152
|
+
// @cast-boundary engine-bridge — typed Dev-API → erased internal storage
|
|
153
|
+
handler: def.handler as QueryHandlerFn, // @cast-boundary engine-bridge
|
|
154
|
+
...(def.access && { access: def.access }),
|
|
155
|
+
...(def.rateLimit && { rateLimit: def.rateLimit }),
|
|
156
|
+
};
|
|
157
|
+
tryMapEntity(state, name, def.name);
|
|
158
|
+
return { name: def.name };
|
|
159
|
+
}
|
|
160
|
+
if (!schema || !handler)
|
|
161
|
+
throw new Error("queryHandler inline form requires schema + handler");
|
|
162
|
+
state.queryHandlers[nameOrDef] = {
|
|
163
|
+
name: nameOrDef,
|
|
164
|
+
schema,
|
|
165
|
+
handler: handler as QueryHandlerFn, // @cast-boundary engine-bridge
|
|
166
|
+
...(options?.access && { access: options.access }),
|
|
167
|
+
...(options?.rateLimit && { rateLimit: options.rateLimit }),
|
|
168
|
+
};
|
|
169
|
+
tryMapEntity(state, name, nameOrDef);
|
|
170
|
+
return { name: nameOrDef };
|
|
171
|
+
},
|
|
172
|
+
streamHandler<TName extends string, TSchema extends ZodType>(
|
|
173
|
+
nameOrDef: string | StreamHandlerDefinition<TName, TSchema>,
|
|
174
|
+
schema?: TSchema,
|
|
175
|
+
handler?: StreamHandlerFn<z.infer<TSchema>>,
|
|
176
|
+
options?: { access?: AccessRule; rateLimit?: RateLimitOption },
|
|
177
|
+
): HandlerRef {
|
|
178
|
+
if (typeof nameOrDef === "object") {
|
|
179
|
+
const def = nameOrDef;
|
|
180
|
+
state.streamHandlers[def.name] = {
|
|
181
|
+
name: def.name,
|
|
182
|
+
schema: def.schema,
|
|
183
|
+
// @cast-boundary engine-bridge — typed Dev-API → erased internal storage
|
|
184
|
+
handler: def.handler as StreamHandlerFn, // @cast-boundary engine-bridge
|
|
185
|
+
...(def.access && { access: def.access }),
|
|
186
|
+
...(def.rateLimit && { rateLimit: def.rateLimit }),
|
|
187
|
+
};
|
|
188
|
+
return { name: def.name };
|
|
189
|
+
}
|
|
190
|
+
if (!schema || !handler)
|
|
191
|
+
throw new Error("streamHandler inline form requires schema + handler");
|
|
192
|
+
state.streamHandlers[nameOrDef] = {
|
|
193
|
+
name: nameOrDef,
|
|
194
|
+
schema,
|
|
195
|
+
handler: handler as StreamHandlerFn, // @cast-boundary engine-bridge
|
|
196
|
+
...(options?.access && { access: options.access }),
|
|
197
|
+
...(options?.rateLimit && { rateLimit: options.rateLimit }),
|
|
198
|
+
};
|
|
199
|
+
return { name: nameOrDef };
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
return {
|
|
203
|
+
...methods,
|
|
204
|
+
crud(entityName: string, entity: EntityDefinition, options?: RegisterEntityCrudOptions): void {
|
|
205
|
+
registerEntityCrud(methods, entityName, entity, options);
|
|
206
|
+
},
|
|
207
|
+
};
|
|
208
|
+
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// lesen.
|
|
8
8
|
|
|
9
9
|
import { compareByCodepoint } from "../utils";
|
|
10
|
+
import { isEncryptedAtRest } from "./config-helpers";
|
|
10
11
|
import { qualifyEntityName } from "./qualified-name";
|
|
11
12
|
import type { Registry, UiHints } from "./types/feature";
|
|
12
13
|
|
|
@@ -106,7 +107,7 @@ export function buildManifestFromRegistry(
|
|
|
106
107
|
type: def.type,
|
|
107
108
|
scope: def.scope,
|
|
108
109
|
default: def.default ?? null,
|
|
109
|
-
encrypted: def
|
|
110
|
+
encrypted: isEncryptedAtRest(def),
|
|
110
111
|
computed: def.computed !== undefined,
|
|
111
112
|
options: def.options ?? null,
|
|
112
113
|
bounds: def.bounds ?? null,
|