@cosmicdrift/kumiko-framework 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -38
- package/package.json +21 -2
- package/src/__tests__/anonymous-access.integration.test.ts +185 -0
- package/src/__tests__/consumer-cli.integration.test.ts +172 -0
- package/src/__tests__/entity-permalink-open.integration.test.ts +94 -0
- package/src/__tests__/full-stack.integration.test.ts +1 -1
- package/src/__tests__/schema-cli-temporal-polyfill.test.ts +48 -0
- package/src/__tests__/schema-cli.integration.test.ts +50 -0
- package/src/__tests__/store-table.integration.test.ts +93 -0
- package/src/api/__tests__/api.test.ts +373 -0
- package/src/api/__tests__/auth-middleware-anonymous-access-boot.test.ts +40 -0
- package/src/api/__tests__/auth-routes-cookie.test.ts +17 -1
- package/src/api/__tests__/auth-routes-invalid-body-invite.test.ts +253 -0
- package/src/api/__tests__/auth-routes-mfa-preauth-confirm.test.ts +222 -0
- package/src/api/__tests__/auth-routes-mfa-preauth-enable-start.test.ts +249 -0
- package/src/api/__tests__/auth-routes-mfa-verify.test.ts +202 -0
- package/src/api/__tests__/auth-routes-trusted-proxy.test.ts +135 -0
- package/src/api/__tests__/batch.integration.test.ts +101 -11
- package/src/api/__tests__/csrf-constants-sync.test.ts +20 -0
- package/src/api/__tests__/dispatcher-live.integration.test.ts +74 -0
- package/src/api/__tests__/jwt.test.ts +200 -1
- package/src/api/__tests__/login-rate-limiter-sweep.test.ts +50 -0
- package/src/api/__tests__/pat-scope.test.ts +36 -0
- package/src/api/__tests__/pii-leak-guard.integration.test.ts +103 -0
- package/src/api/__tests__/redis-login-rate-limiter.integration.test.ts +138 -0
- package/src/api/__tests__/request-id-middleware.test.ts +51 -0
- package/src/api/__tests__/server-boot-guards.test.ts +71 -0
- package/src/api/__tests__/server-jwt-ttl.test.ts +58 -0
- package/src/api/__tests__/sse-broker.test.ts +57 -0
- package/src/api/__tests__/sse-route.test.ts +4 -0
- package/src/api/api-constants.ts +11 -0
- package/src/api/auth-middleware.ts +248 -40
- package/src/api/auth-routes.ts +576 -95
- package/src/api/index.ts +13 -4
- package/src/api/jwt.ts +170 -11
- package/src/api/pat-scope.ts +14 -0
- package/src/api/pii-leak-guard.ts +47 -0
- package/src/api/request-context.ts +3 -0
- package/src/api/request-id-middleware.ts +2 -0
- package/src/api/routes.ts +169 -2
- package/src/api/server.ts +112 -16
- package/src/api/sse-broker.ts +39 -0
- package/src/bun-db/__tests__/PATTERN.md +0 -1
- package/src/bun-db/__tests__/coerce-row-temporal.test.ts +41 -0
- package/src/bun-db/__tests__/select-many-retry.test.ts +79 -0
- package/src/bun-db/__tests__/write-brand.test.ts +21 -0
- package/src/bun-db/connection.ts +3 -3
- package/src/bun-db/index.ts +2 -0
- package/src/bun-db/query.ts +105 -32
- package/src/consumer-cli.ts +134 -0
- package/src/crypto/__tests__/blind-index.test.ts +130 -0
- package/src/crypto/__tests__/event-pii.test.ts +161 -0
- package/src/crypto/__tests__/kek-rotation.integration.test.ts +180 -0
- package/src/crypto/__tests__/kms-adapter-contract.ts +134 -0
- package/src/crypto/__tests__/kms-adapter.contract.test.ts +4 -0
- package/src/crypto/__tests__/pg-kms-adapter.integration.test.ts +117 -0
- package/src/crypto/__tests__/pii-field-encryption.test.ts +376 -0
- package/src/crypto/__tests__/request-kms-cache.test.ts +74 -0
- package/src/crypto/__tests__/subject-resolver.test.ts +108 -0
- package/src/crypto/blind-index.ts +118 -0
- package/src/crypto/event-pii.ts +70 -0
- package/src/crypto/in-memory-kms-adapter.ts +50 -0
- package/src/crypto/index.ts +67 -0
- package/src/crypto/kms-adapter.ts +2 -0
- package/src/crypto/pg-kms-adapter.ts +295 -0
- package/src/crypto/pii-field-encryption.ts +248 -0
- package/src/crypto/request-kms-cache.ts +38 -0
- package/src/crypto/subject-resolver.ts +91 -0
- package/src/db/__tests__/assert-no-unreachable-live-rows.integration.test.ts +191 -0
- package/src/db/__tests__/blind-index.integration.test.ts +248 -0
- package/src/db/__tests__/build-filter-where.test.ts +34 -0
- package/src/db/__tests__/collect-table-metas.test.ts +10 -10
- package/src/db/__tests__/config-seed.integration.test.ts +13 -5
- package/src/db/__tests__/dialect-instant.test.ts +1 -4
- package/src/db/__tests__/entity-field-encryption.test.ts +7 -7
- package/src/db/__tests__/entity-table-meta-source.test.ts +50 -0
- package/src/db/__tests__/event-store-executor-context.pii-roundtrip.test.ts +67 -0
- package/src/db/__tests__/event-store-executor-write-verbs.integration.test.ts +402 -0
- package/src/db/__tests__/event-store-executor.integration.test.ts +299 -29
- package/src/db/__tests__/feature-table-sources.test.ts +34 -0
- package/src/db/__tests__/implicit-projection-equivalence.integration.test.ts +118 -39
- package/src/db/__tests__/instant-to-driver-temporal.test.ts +21 -0
- package/src/db/__tests__/located-timestamp.test.ts +19 -0
- package/src/db/__tests__/migrate-generator.test.ts +21 -0
- package/src/db/__tests__/migrate-runner.test.ts +61 -0
- package/src/db/__tests__/number-field-fractional.integration.test.ts +58 -0
- package/src/db/__tests__/rebuild-marker.test.ts +13 -3
- package/src/db/__tests__/replay-migration-sql.test.ts +384 -0
- package/src/db/__tests__/schema-inspection.test.ts +7 -0
- package/src/db/__tests__/schema-migration.integration.test.ts +1 -1
- package/src/db/__tests__/table-builder-meta-lockstep.test.ts +39 -0
- package/src/db/__tests__/tenant-db-where-merge.test.ts +49 -3
- package/src/db/__tests__/tenant-db.integration.test.ts +6 -2
- package/src/db/api.ts +2 -2
- package/src/db/apply-entity-event.ts +18 -14
- package/src/db/blind-index-cleanup.ts +55 -0
- package/src/db/bun-provider.ts +2 -2
- package/src/db/collect-table-metas.ts +6 -7
- package/src/db/config-seed.ts +9 -9
- package/src/db/connection.ts +7 -12
- package/src/db/cursor.ts +1 -18
- package/src/db/dialect.ts +20 -26
- package/src/db/entity-field-encryption.ts +81 -24
- package/src/db/entity-table-meta-types.ts +2 -0
- package/src/db/entity-table-meta.ts +63 -90
- package/src/db/event-store-executor-context.ts +404 -0
- package/src/db/event-store-executor-read.ts +275 -0
- package/src/db/event-store-executor-write.ts +644 -0
- package/src/db/event-store-executor.ts +28 -1155
- package/src/db/feature-table-sources.ts +6 -5
- package/src/db/index.ts +20 -3
- package/src/db/located-timestamp.ts +4 -0
- package/src/db/migrate-generator.ts +39 -6
- package/src/db/migrate-runner.ts +107 -11
- package/src/db/pg-error.ts +9 -1
- package/src/db/postgres-provider.ts +2 -2
- package/src/db/queries/__tests__/event-store-idempotency-index.integration.test.ts +80 -0
- package/src/db/queries/backfill-pii.ts +277 -0
- package/src/db/queries/ddl.ts +45 -0
- package/src/db/queries/event-consumer.ts +35 -2
- package/src/db/queries/event-store.ts +126 -19
- package/src/db/queries/projection-rebuild.ts +22 -8
- package/src/db/queries/shadow-swap.ts +183 -0
- package/src/db/queries/test-stack.ts +4 -30
- package/src/db/query-api.ts +1 -0
- package/src/db/query.ts +1 -0
- package/src/db/rebuild-marker.ts +18 -2
- package/src/db/reference-data.ts +2 -3
- package/src/db/replay-migration-sql.ts +257 -0
- package/src/db/schema-inspection.ts +1 -1
- package/src/db/table-builder.ts +102 -71
- package/src/db/tenant-db.ts +15 -53
- package/src/engine/__tests__/boot-validator-action-wiring.test.ts +242 -0
- package/src/engine/__tests__/boot-validator-boot-check.test.ts +99 -0
- package/src/engine/__tests__/boot-validator-dashboard.test.ts +237 -0
- package/src/engine/__tests__/boot-validator-entity-list.test.ts +104 -0
- package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +7 -69
- package/src/engine/__tests__/boot-validator-i18n-keys.test.ts +26 -5
- package/src/engine/__tests__/boot-validator-located-timestamps.test.ts +3 -19
- package/src/engine/__tests__/boot-validator-pii-retention.test.ts +246 -4
- package/src/engine/__tests__/boot-validator.test.ts +219 -15
- package/src/engine/__tests__/build-app-schema.test.ts +82 -0
- package/src/engine/__tests__/build-config-feature-schema.test.ts +125 -0
- package/src/engine/__tests__/build-target.test.ts +3 -11
- package/src/engine/__tests__/codemod-pipeline.test.ts +152 -21
- package/src/engine/__tests__/config-helpers.test.ts +16 -0
- package/src/engine/__tests__/define-feature-entity-mapping.test.ts +6 -0
- package/src/engine/__tests__/define-roles.test.ts +21 -0
- package/src/engine/__tests__/engine.test.ts +163 -1
- package/src/engine/__tests__/entity-handlers.test.ts +80 -0
- package/src/engine/__tests__/event-migration-declarative.test.ts +65 -0
- package/src/engine/__tests__/event-type-map-augmentation.test.ts +24 -0
- package/src/engine/__tests__/extend-entity-projection.test.ts +123 -0
- package/src/engine/__tests__/factories-time.test.ts +2 -66
- package/src/engine/__tests__/feature-crud-shorthand.test.ts +28 -0
- package/src/engine/__tests__/feature-manifest.test.ts +31 -1
- package/src/engine/__tests__/field-access.test.ts +23 -1
- package/src/engine/__tests__/hook-phases.test.ts +5 -5
- package/src/engine/__tests__/membership-roles.test.ts +4 -10
- package/src/engine/__tests__/nav.test.ts +86 -1
- package/src/engine/__tests__/pipeline-engine.test.ts +9 -9
- package/src/engine/__tests__/pipeline-handler.integration.test.ts +18 -18
- package/src/engine/__tests__/pipeline-observability.integration.test.ts +2 -2
- package/src/engine/__tests__/pipeline-performance.integration.test.ts +3 -3
- package/src/engine/__tests__/pipeline-sub-pipelines.test.ts +9 -9
- package/src/engine/__tests__/post-query-hook.test.ts +7 -7
- package/src/engine/__tests__/registrar-object-form.test.ts +141 -0
- package/src/engine/__tests__/registry.test.ts +144 -0
- package/src/engine/__tests__/schema-builder.test.ts +18 -0
- package/src/engine/__tests__/screen.test.ts +147 -1
- package/src/engine/__tests__/soft-delete-cleanup.test.ts +3 -0
- package/src/engine/__tests__/store-table.test.ts +229 -0
- package/src/engine/__tests__/tier-resolver-extension.test.ts +19 -1
- package/src/engine/__tests__/{visual-tree-patterns.test.ts → tree-actions-patterns.test.ts} +7 -95
- package/src/engine/__tests__/validate-projection-allowlist.test.ts +15 -15
- package/src/engine/boot-validator/__tests__/config-deps.test.ts +92 -0
- package/src/engine/boot-validator/action-wiring.ts +149 -0
- package/src/engine/boot-validator/boot-check.ts +21 -0
- package/src/engine/boot-validator/config-deps.ts +40 -4
- package/src/engine/boot-validator/entity-handler.ts +20 -21
- package/src/engine/boot-validator/entity-list-screens.ts +88 -0
- package/src/engine/boot-validator/gdpr-storage.ts +0 -20
- package/src/engine/boot-validator/i18n-keys.ts +58 -4
- package/src/engine/boot-validator/index.ts +33 -12
- package/src/engine/boot-validator/nav.ts +125 -0
- package/src/engine/boot-validator/pii-retention.ts +95 -10
- package/src/engine/boot-validator/{screens-nav.ts → screens.ts} +214 -191
- package/src/engine/boot-validator/workspaces.ts +68 -0
- package/src/engine/build-app-schema.ts +16 -0
- package/src/engine/build-config-feature-schema.ts +44 -7
- package/src/engine/codemod/pipeline-codemod.ts +5 -5
- package/src/engine/config-helpers.ts +15 -0
- package/src/engine/constants.ts +32 -6
- package/src/engine/create-app.ts +11 -0
- package/src/engine/define-feature.ts +95 -947
- package/src/engine/define-handler.ts +28 -94
- package/src/engine/define-workflow.ts +1 -1
- package/src/engine/effective-features.ts +12 -2
- package/src/engine/entity-handlers.ts +76 -11
- package/src/engine/extensions/tenant-data.ts +19 -0
- package/src/engine/extensions/user-data.ts +29 -2
- package/src/engine/factories.ts +8 -49
- package/src/engine/feature-ast/__tests__/canonical-form.test.ts +26 -29
- package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/feature.ts +9 -0
- package/src/engine/feature-ast/__tests__/fixtures/cross-file-registrar/screens.ts +4 -0
- package/src/engine/feature-ast/__tests__/parse-happy-path.test.ts +1 -2
- package/src/engine/feature-ast/__tests__/parse-real-features.test.ts +18 -8
- package/src/engine/feature-ast/__tests__/parse.test.ts +1640 -160
- package/src/engine/feature-ast/__tests__/patch.test.ts +206 -12
- package/src/engine/feature-ast/__tests__/patcher.test.ts +20 -23
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +380 -5
- package/src/engine/feature-ast/extractors/events.ts +322 -0
- package/src/engine/feature-ast/extractors/handlers.ts +234 -0
- package/src/engine/feature-ast/extractors/hooks.ts +243 -0
- package/src/engine/feature-ast/extractors/index.ts +34 -31
- package/src/engine/feature-ast/extractors/jobs-routes.ts +221 -0
- package/src/engine/feature-ast/extractors/projections-screens.ts +269 -0
- package/src/engine/feature-ast/extractors/round1.ts +3 -3
- package/src/engine/feature-ast/extractors/round5.ts +3 -3
- package/src/engine/feature-ast/extractors/round6.ts +2 -36
- package/src/engine/feature-ast/extractors/shared.ts +64 -6
- package/src/engine/feature-ast/index.ts +2 -4
- package/src/engine/feature-ast/parse.ts +130 -23
- package/src/engine/feature-ast/patch.ts +39 -50
- package/src/engine/feature-ast/patcher.ts +37 -38
- package/src/engine/feature-ast/patterns.ts +50 -62
- package/src/engine/feature-ast/render.ts +67 -44
- package/src/engine/feature-builder-state.ts +168 -0
- package/src/engine/feature-config-events-jobs.ts +403 -0
- package/src/engine/feature-entity-handlers.ts +208 -0
- package/src/engine/feature-manifest.ts +2 -1
- package/src/engine/feature-ui-extensions.ts +500 -0
- package/src/engine/field-access.ts +13 -2
- package/src/engine/field-helpers.ts +31 -0
- package/src/engine/handler-helpers.ts +26 -0
- package/src/engine/hook-helpers.ts +16 -0
- package/src/engine/index.ts +23 -7
- package/src/engine/membership-roles.ts +13 -0
- package/src/engine/object-form.ts +27 -0
- package/src/engine/ownership.ts +26 -79
- package/src/engine/pattern-library/__tests__/library.test.ts +7 -20
- package/src/engine/pattern-library/library.ts +44 -1152
- package/src/engine/pattern-library/mixed-schemas.ts +450 -0
- package/src/engine/pattern-library/opaque-schemas.ts +124 -0
- package/src/engine/pattern-library/shared-fields.ts +75 -0
- package/src/engine/pattern-library/static-schemas.ts +456 -0
- package/src/engine/pipeline.ts +6 -11
- package/src/engine/registry-facade.ts +362 -0
- package/src/engine/registry-ingest.ts +478 -0
- package/src/engine/registry-state.ts +388 -0
- package/src/engine/registry-validate.ts +642 -0
- package/src/engine/registry.ts +78 -1658
- package/src/engine/run-pipeline.ts +1 -1
- package/src/engine/schema-builder.ts +1 -0
- package/src/engine/screen-helpers.ts +54 -0
- package/src/engine/soft-delete-cleanup.ts +6 -2
- package/src/engine/steps/__tests__/duration-utils.test.ts +20 -0
- package/src/engine/steps/_duration-utils.ts +2 -0
- package/src/engine/steps/unsafe-projection-upsert.ts +1 -4
- package/src/engine/tier-resolver-extension.ts +3 -2
- package/src/engine/types/config.ts +2 -482
- package/src/engine/types/define-handler.ts +2 -0
- package/src/engine/types/entity-handlers.ts +2 -0
- package/src/engine/types/event-type-map.ts +1 -37
- package/src/engine/types/feature.ts +2 -972
- package/src/engine/types/fields.ts +2 -675
- package/src/engine/types/handlers.ts +2 -774
- package/src/engine/types/hooks.ts +2 -184
- package/src/engine/types/http-route.ts +1 -54
- package/src/engine/types/identifiers.ts +1 -47
- package/src/engine/types/index.ts +86 -40
- package/src/engine/types/nav.ts +2 -63
- package/src/engine/types/ownership.ts +2 -0
- package/src/engine/types/projection.ts +2 -138
- package/src/engine/types/relations.ts +1 -51
- package/src/engine/types/screen.ts +2 -574
- package/src/engine/types/step.ts +2 -334
- package/src/engine/types/target-ref.ts +1 -21
- package/src/engine/types/tree-node.ts +1 -132
- package/src/engine/types/workspace.ts +2 -49
- package/src/engine/validate-projection-allowlist.ts +6 -6
- package/src/entrypoint/__tests__/entrypoint-job-wiring.integration.test.ts +120 -1
- package/src/entrypoint/index.ts +49 -6
- package/src/errors/classes.ts +22 -1
- package/src/errors/field-issue.ts +0 -3
- package/src/errors/index.ts +1 -1
- package/src/errors/write-error-info.ts +10 -22
- package/src/es-ops/README.md +1 -1
- package/src/es-ops/__tests__/runner.integration.test.ts +74 -0
- package/src/es-ops/context.ts +4 -2
- package/src/es-ops/types.ts +8 -1
- package/src/event-store/__tests__/admin-api.integration.test.ts +27 -1
- package/src/event-store/__tests__/backfill-pii.integration.test.ts +279 -0
- package/src/event-store/__tests__/event-store.integration.test.ts +201 -0
- package/src/event-store/__tests__/row-to-stored-event.test.ts +2 -2
- package/src/event-store/__tests__/snapshot.integration.test.ts +86 -0
- package/src/event-store/__tests__/unscoped-stream-primitives.guard.test.ts +58 -0
- package/src/event-store/__tests__/upcaster.integration.test.ts +77 -45
- package/src/event-store/admin-api.ts +11 -4
- package/src/event-store/errors.ts +2 -35
- package/src/event-store/event-store.ts +64 -78
- package/src/event-store/events-schema.ts +11 -13
- package/src/event-store/index.ts +18 -3
- package/src/event-store/rebuild-dead-letter.ts +111 -0
- package/src/event-store/snapshot.ts +63 -40
- package/src/event-store/types.ts +2 -0
- package/src/files/__tests__/build-storage-key.test.ts +28 -0
- package/src/files/__tests__/file-ref-entity.test.ts +8 -0
- package/src/files/__tests__/files.integration.test.ts +24 -0
- package/src/files/__tests__/in-memory-provider.contract.test.ts +4 -0
- package/src/files/__tests__/local-provider.test.ts +31 -0
- package/src/files/__tests__/provider-resolver.test.ts +70 -0
- package/src/files/__tests__/write-stream.test.ts +13 -0
- package/src/files/file-handle.ts +2 -19
- package/src/files/file-ref-entity.ts +2 -2
- package/src/files/file-routes.ts +13 -0
- package/src/files/index.ts +1 -1
- package/src/files/local-provider.ts +27 -8
- package/src/files/provider-resolver.ts +31 -20
- package/src/files/types.ts +13 -55
- package/src/i18n/__tests__/required-surface-keys.test.ts +17 -0
- package/src/i18n/required-surface-keys.ts +120 -1
- package/src/jobs/__tests__/job-queue-depth.integration.test.ts +82 -0
- package/src/jobs/__tests__/jobs.integration.test.ts +329 -3
- package/src/jobs/job-runner.ts +82 -12
- package/src/logging/types.ts +1 -7
- package/src/migrations/pending-rebuilds.ts +1 -1
- package/src/observability/__tests__/observability.integration.test.ts +4 -1
- package/src/observability/__tests__/recording-tracer.test.ts +6 -4
- package/src/observability/index.ts +2 -0
- package/src/observability/noop-provider.ts +0 -9
- package/src/observability/recording-tracer.ts +0 -12
- package/src/observability/standard-metrics.ts +64 -2
- package/src/observability/types/index.ts +1 -29
- package/src/observability/types/metric.ts +1 -56
- package/src/observability/types/provider.ts +1 -32
- package/src/observability/types/span.ts +1 -64
- package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +2 -2
- package/src/pipeline/__tests__/dispatcher.test.ts +366 -1
- package/src/pipeline/__tests__/event-consumer-state.integration.test.ts +31 -0
- package/src/pipeline/__tests__/event-dispatcher-delivery-max-attempts.test.ts +126 -0
- package/src/pipeline/__tests__/event-dispatcher-rearm.integration.test.ts +263 -0
- package/src/pipeline/__tests__/event-dispatcher.integration.test.ts +10 -0
- package/src/pipeline/__tests__/job-trigger-consumer.integration.test.ts +106 -0
- package/src/pipeline/__tests__/lifecycle-pipeline.test.ts +308 -77
- package/src/pipeline/__tests__/load-aggregate-query.integration.test.ts +16 -8
- package/src/pipeline/__tests__/post-query-hook.integration.test.ts +3 -3
- package/src/pipeline/__tests__/projection-rebuild.integration.test.ts +80 -2
- package/src/pipeline/__tests__/rebuild-poison-quarantine.integration.test.ts +274 -0
- package/src/pipeline/__tests__/try-append-event.integration.test.ts +166 -0
- package/src/pipeline/dispatch-batch.ts +187 -0
- package/src/pipeline/dispatch-query.ts +165 -0
- package/src/pipeline/dispatch-shared.ts +826 -0
- package/src/pipeline/dispatch-stream.ts +90 -0
- package/src/pipeline/dispatch-write.ts +451 -0
- package/src/pipeline/dispatcher-utils.ts +1 -1
- package/src/pipeline/dispatcher.ts +44 -1372
- package/src/pipeline/entity-cache.ts +2 -33
- package/src/pipeline/event-consumer-state.ts +30 -2
- package/src/pipeline/event-dispatcher-admin.ts +293 -0
- package/src/pipeline/event-dispatcher-delivery.ts +305 -0
- package/src/pipeline/event-dispatcher.ts +85 -542
- package/src/pipeline/index.ts +2 -0
- package/src/pipeline/msp-rebuild.ts +42 -3
- package/src/pipeline/multi-stream-apply-context.ts +4 -42
- package/src/pipeline/projection-rebuild.ts +105 -3
- package/src/pipeline/system-hooks.ts +144 -1
- package/src/rate-limit/__tests__/resolver.integration.test.ts +18 -0
- package/src/rate-limit/resolver.ts +16 -32
- package/src/schema-cli.ts +76 -16
- package/src/search/__tests__/meilisearch-adapter.integration.test.ts +207 -185
- package/src/search/__tests__/meilisearch-ids.test.ts +30 -0
- package/src/search/__tests__/reindex-entity.integration.test.ts +144 -0
- package/src/search/index.ts +6 -0
- package/src/search/meilisearch-adapter.ts +13 -12
- package/src/search/reindex-entity.ts +177 -0
- package/src/search/types.ts +1 -39
- package/src/secrets/__tests__/contains-secret.test.ts +34 -0
- package/src/secrets/__tests__/envelope-cipher.test.ts +59 -0
- package/src/secrets/__tests__/envelope.test.ts +1 -1
- package/src/secrets/dek-cache.ts +26 -5
- package/src/secrets/envelope-cipher.ts +53 -0
- package/src/secrets/envelope.ts +5 -3
- package/src/secrets/index.ts +13 -1
- package/src/secrets/stored-envelope.ts +46 -0
- package/src/secrets/types.ts +2 -162
- package/src/stack/__tests__/event-collector.test.ts +42 -0
- package/src/stack/__tests__/setup-test-stack-jobs.integration.test.ts +125 -0
- package/src/stack/db.ts +2 -1
- package/src/stack/push-entity-projection-tables.ts +4 -3
- package/src/stack/redis.ts +8 -0
- package/src/stack/request-helper.ts +38 -2
- package/src/stack/table-helpers.ts +6 -4
- package/src/stack/test-stack.ts +249 -131
- package/src/testing/__tests__/late-bound.test.ts +32 -0
- package/src/testing/__tests__/wait-for.test.ts +59 -0
- package/src/testing/boot-validator-fixture.ts +121 -0
- package/src/testing/e2e-generator.ts +8 -0
- package/src/testing/file-provider-contract.ts +104 -0
- package/src/testing/handler-context.ts +3 -1
- package/src/testing/index.ts +5 -0
- package/src/testing/late-bound.ts +5 -3
- package/src/testing/mutable-master-key-provider.ts +29 -3
- package/src/testing/wait-for.ts +3 -0
- package/src/testing/without-ambient-temporal.ts +14 -0
- package/src/time/__tests__/tz-dateline.test.ts +8 -10
- package/src/time/geo-tz.ts +1 -32
- package/src/time/index.ts +1 -0
- package/src/time/legacy-date.ts +18 -0
- package/src/time/polyfill.ts +21 -38
- package/src/time/tz-context.ts +44 -85
- package/src/ui-types/app-schema.ts +12 -0
- package/src/ui-types/index.ts +20 -7
- package/src/utils/__tests__/safe-json-temporal.test.ts +18 -0
- package/src/utils/safe-json.ts +13 -1
- package/src/__tests__/raw-table.integration.test.ts +0 -116
- package/src/bun-db/__tests__/bun-test-stack.ts +0 -6
- package/src/db/__tests__/encryption.test.ts +0 -39
- package/src/db/encryption.ts +0 -39
- package/src/db/row-helpers.ts +0 -4
- package/src/engine/__tests__/raw-table.test.ts +0 -150
- package/src/engine/__tests__/unmanaged-table.test.ts +0 -127
- package/src/engine/feature-ast/__tests__/visual-tree-parse.test.ts +0 -184
- package/src/engine/feature-ast/extractors/round4.ts +0 -1366
|
@@ -1,84 +1,10 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import type {
|
|
8
|
-
AccessRule,
|
|
9
|
-
AuthClaimsFn,
|
|
10
|
-
ClaimKeyDefinition,
|
|
11
|
-
ClaimKeyHandle,
|
|
12
|
-
ClaimKeyType,
|
|
13
|
-
ConfigKeyDefinition,
|
|
14
|
-
ConfigKeyHandle,
|
|
15
|
-
ConfigKeyType,
|
|
16
|
-
ConfigSeedDef,
|
|
17
|
-
EntityDefinition,
|
|
18
|
-
EntityRef,
|
|
19
|
-
EventDef,
|
|
20
|
-
EventMigrationDef,
|
|
21
|
-
EventUpcastFn,
|
|
22
|
-
ExtensionSelectorDef,
|
|
23
|
-
FeatureDefinition,
|
|
24
|
-
FeatureMetricDef,
|
|
25
|
-
FeatureRegistrar,
|
|
26
|
-
HandlerRef,
|
|
27
|
-
HookMap,
|
|
28
|
-
HookPhase,
|
|
29
|
-
JobDefinition,
|
|
30
|
-
JobHandlerFn,
|
|
31
|
-
LifecycleHookFn,
|
|
32
|
-
LifecycleHookType,
|
|
33
|
-
MetricOptions,
|
|
34
|
-
MultiStreamProjectionDefinition,
|
|
35
|
-
NameOrRef,
|
|
36
|
-
NotificationDataFn,
|
|
37
|
-
NotificationDefinition,
|
|
38
|
-
NotificationRecipientFn,
|
|
39
|
-
NotificationTemplateFn,
|
|
40
|
-
OwnedFn,
|
|
41
|
-
PhasedHook,
|
|
42
|
-
PostDeleteHookFn,
|
|
43
|
-
PostQueryHookFn,
|
|
44
|
-
PostSaveHookFn,
|
|
45
|
-
PreDeleteHookFn,
|
|
46
|
-
ProjectionDefinition,
|
|
47
|
-
QualifiedEventName,
|
|
48
|
-
QueryHandlerDef,
|
|
49
|
-
QueryHandlerFn,
|
|
50
|
-
RateLimitOption,
|
|
51
|
-
RawTableEntry,
|
|
52
|
-
RawTableOptions,
|
|
53
|
-
ReferenceDataDef,
|
|
54
|
-
RegistrarExtensionDef,
|
|
55
|
-
RegistrarExtensionRegistration,
|
|
56
|
-
RelationDefinition,
|
|
57
|
-
SearchPayloadContributorFn,
|
|
58
|
-
SecretKeyDefinition,
|
|
59
|
-
SecretKeyHandle,
|
|
60
|
-
SecretOptions,
|
|
61
|
-
TranslationKeys,
|
|
62
|
-
TranslationsDef,
|
|
63
|
-
TreeActionDef,
|
|
64
|
-
TreeActionsHandle,
|
|
65
|
-
TreeChildrenSubscribe,
|
|
66
|
-
UiHints,
|
|
67
|
-
UnmanagedTableEntry,
|
|
68
|
-
ValidationHookFn,
|
|
69
|
-
WriteHandlerDef,
|
|
70
|
-
WriteHandlerFn,
|
|
71
|
-
} from "./types";
|
|
72
|
-
import { HookPhases } from "./types";
|
|
1
|
+
import { createInitialFeatureBuilderState } from "./feature-builder-state";
|
|
2
|
+
import { buildConfigEventsJobsMethods } from "./feature-config-events-jobs";
|
|
3
|
+
import { buildEntityHandlerMethods } from "./feature-entity-handlers";
|
|
4
|
+
import { buildUiExtensionsMethods } from "./feature-ui-extensions";
|
|
5
|
+
import { unwrapArrayForm } from "./object-form";
|
|
6
|
+
import type { FeatureDefinition, FeatureRegistrar, HookMap, UiHints } from "./types";
|
|
73
7
|
import type { RequiresApi } from "./types/feature";
|
|
74
|
-
import { resolveName } from "./types/handlers";
|
|
75
|
-
import type { HttpRouteDefinition } from "./types/http-route";
|
|
76
|
-
import type { NavDefinition } from "./types/nav";
|
|
77
|
-
import type { ScreenDefinition } from "./types/screen";
|
|
78
|
-
import type { PipelineDef } from "./types/step";
|
|
79
|
-
import type { WorkspaceDefinition } from "./types/workspace";
|
|
80
|
-
|
|
81
|
-
const LIFECYCLE_TYPES = Object.values(LifecycleHookTypes);
|
|
82
8
|
|
|
83
9
|
// `TExports` lets the setup callback hand back a typed object that
|
|
84
10
|
// downstream features can import (e.g. `tenantFeature.exports.config`). The
|
|
@@ -92,120 +18,29 @@ const LIFECYCLE_TYPES = Object.values(LifecycleHookTypes);
|
|
|
92
18
|
// for `ctx.appendEvent({ type: eventDef.name, ... })` lights up. Apps
|
|
93
19
|
// that don't care can keep the default-string and use the wrapper-based
|
|
94
20
|
// strict-mode (string-literal types per call-site) like before.
|
|
21
|
+
|
|
22
|
+
// requires/optionalRequires accept either variadic strings (hand-written
|
|
23
|
+
// call sites) or a single `{ features }` object (the feature-ast renderer's
|
|
24
|
+
// canonical Object-Form for Designer/AI-generated code) — see the matching
|
|
25
|
+
// overloads on RequiresApi / FeatureRegistrar['optionalRequires'].
|
|
26
|
+
function resolveFeatureNamesArgs(
|
|
27
|
+
args: readonly [{ readonly features: readonly string[] }] | readonly string[],
|
|
28
|
+
): readonly string[] {
|
|
29
|
+
return unwrapArrayForm(args, "features");
|
|
30
|
+
}
|
|
31
|
+
|
|
95
32
|
export function defineFeature<const TName extends string, TExports = undefined>(
|
|
96
33
|
name: TName,
|
|
97
34
|
setup: (r: FeatureRegistrar<TName>) => TExports,
|
|
98
35
|
): FeatureDefinition & { readonly exports: TExports } {
|
|
99
|
-
const
|
|
100
|
-
const optionalRequires: string[] = [];
|
|
101
|
-
// Read-side projection-tables declared via r.requires.projection("table").
|
|
102
|
-
// Boot-validator checks unsafeProjection-* step calls against this set.
|
|
103
|
-
const requiredProjections = new Set<string>();
|
|
104
|
-
// Tier-2 step kinds declared via r.requires.step("webhook.send"). Q9.
|
|
105
|
-
const requiredSteps = new Set<string>();
|
|
106
|
-
const entities: Record<string, EntityDefinition> = {};
|
|
107
|
-
const entityTables: Record<string, unknown> = {};
|
|
108
|
-
const relations: Record<string, Record<string, RelationDefinition>> = {};
|
|
109
|
-
const writeHandlers: Record<string, WriteHandlerDef> = {};
|
|
110
|
-
const queryHandlers: Record<string, QueryHandlerDef> = {};
|
|
111
|
-
const validationHooks: Record<string, ValidationHookFn> = {};
|
|
112
|
-
// preSave/preQuery stay unphased (owned-fn); postSave/preDelete/postDelete
|
|
113
|
-
// are phased (owned-fn + phase). Each hook carries its owning feature so
|
|
114
|
-
// the lifecycle pipeline can filter by effectiveFeatures without a parallel
|
|
115
|
-
// bookkeeping structure.
|
|
116
|
-
const lifecycleHooks: Record<string, Record<string, OwnedFn<LifecycleHookFn>[]>> = {};
|
|
117
|
-
const phasedLifecycleHooks: Record<
|
|
118
|
-
"postSave" | "preDelete" | "postDelete",
|
|
119
|
-
Record<string, PhasedHook<LifecycleHookFn>[]>
|
|
120
|
-
> = { postSave: {}, preDelete: {}, postDelete: {} };
|
|
121
|
-
const configKeys: Record<string, ConfigKeyDefinition> = {};
|
|
122
|
-
const configSeeds: ConfigSeedDef[] = [];
|
|
123
|
-
const jobs: Record<string, JobDefinition> = {};
|
|
124
|
-
const events: Record<string, { name: string; schema: ZodType; version: number }> = {};
|
|
125
|
-
const eventMigrations: Record<string, EventMigrationDef[]> = {};
|
|
126
|
-
const configReads: string[] = [];
|
|
127
|
-
const entityPostSave: Record<string, PhasedHook<PostSaveHookFn>[]> = {};
|
|
128
|
-
const entityPreDelete: Record<string, PhasedHook<PreDeleteHookFn>[]> = {};
|
|
129
|
-
const entityPostDelete: Record<string, PhasedHook<PostDeleteHookFn>[]> = {};
|
|
130
|
-
const entityPostQuery: Record<string, OwnedFn<PostQueryHookFn>[]> = {};
|
|
131
|
-
const searchPayloadExtensions: Record<string, OwnedFn<SearchPayloadContributorFn>[]> = {};
|
|
132
|
-
const notifications: Record<string, NotificationDefinition> = {};
|
|
133
|
-
const registrarExtensions: Record<string, RegistrarExtensionDef> = {};
|
|
134
|
-
const extensionUsages: RegistrarExtensionRegistration[] = [];
|
|
135
|
-
const extensionSelectors: ExtensionSelectorDef[] = [];
|
|
136
|
-
const exposedApis: Set<string> = new Set();
|
|
137
|
-
const usedApis: Set<string> = new Set();
|
|
138
|
-
const referenceData: ReferenceDataDef[] = [];
|
|
139
|
-
const handlerEntityMappings: Record<string, string> = {};
|
|
140
|
-
const metrics: Record<string, FeatureMetricDef> = {};
|
|
141
|
-
const secretKeys: Record<string, SecretKeyDefinition> = {};
|
|
142
|
-
const projections: Record<string, ProjectionDefinition> = {};
|
|
143
|
-
const multiStreamProjections: Record<string, MultiStreamProjectionDefinition> = {};
|
|
144
|
-
const rawTables: Record<string, RawTableEntry> = {};
|
|
145
|
-
const unmanagedTables: Record<string, UnmanagedTableEntry> = {};
|
|
146
|
-
const authClaimsHooks: AuthClaimsFn[] = [];
|
|
147
|
-
const claimKeys: Record<string, ClaimKeyDefinition> = {};
|
|
148
|
-
const screens: Record<string, ScreenDefinition> = {};
|
|
149
|
-
const navs: Record<string, NavDefinition> = {};
|
|
150
|
-
const workspaces: Record<string, WorkspaceDefinition> = {};
|
|
151
|
-
const httpRoutes: Record<string, HttpRouteDefinition> = {};
|
|
152
|
-
let translations: TranslationKeys = {};
|
|
153
|
-
|
|
154
|
-
for (const t of LIFECYCLE_TYPES) {
|
|
155
|
-
lifecycleHooks[t] = {};
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
let isSystemScoped = false;
|
|
159
|
-
let toggleableDefault: boolean | undefined;
|
|
160
|
-
let description: string | undefined;
|
|
161
|
-
let uiHints: UiHints | undefined;
|
|
162
|
-
// Visual-Tree-Slots — at-most-one per feature, only-once-guard im
|
|
163
|
-
// registrar (siehe r.treeActions / r.tree). Undefined wenn das Feature
|
|
164
|
-
// keinen Visual-Tree-Beitrag liefert (Zero-Whitelist-Filter).
|
|
165
|
-
// Name-Collision-Sicherheit: object-literal-method-Names im registrar
|
|
166
|
-
// sind keine bindings im closure-scope, daher kollidiert die `treeActions`
|
|
167
|
-
// closure-let-var nicht mit der `treeActions(...)` registrar-Methode.
|
|
168
|
-
let treeActions: Readonly<Record<string, TreeActionDef>> | undefined;
|
|
169
|
-
let treeProvider: TreeChildrenSubscribe | undefined;
|
|
170
|
-
// Optional Zod-schema for env-vars this feature reads at runtime,
|
|
171
|
-
// declared via r.envSchema(). At-most-one per feature.
|
|
172
|
-
let envSchema: z.ZodObject<z.ZodRawShape> | undefined;
|
|
173
|
-
|
|
174
|
-
// Map handler name to entity via colon convention.
|
|
175
|
-
// "task:create" → entity "task". Bare CRUD verbs (create/update/delete) map
|
|
176
|
-
// when feature name matches an entity or the feature owns exactly one entity.
|
|
177
|
-
const CRUD_VERBS = new Set(["create", "update", "delete"]);
|
|
178
|
-
|
|
179
|
-
function tryMapEntity(handlerName: string): void {
|
|
180
|
-
const colonIdx = handlerName.indexOf(":");
|
|
181
|
-
if (colonIdx >= 0) {
|
|
182
|
-
const candidate = handlerName.slice(0, colonIdx);
|
|
183
|
-
if (entities[candidate]) {
|
|
184
|
-
handlerEntityMappings[handlerName] = candidate;
|
|
185
|
-
}
|
|
186
|
-
// skip: colon-prefixed handler processed (mapped or not), bare CRUD path not applicable
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
if (CRUD_VERBS.has(handlerName)) {
|
|
190
|
-
if (entities[name]) {
|
|
191
|
-
handlerEntityMappings[handlerName] = name;
|
|
192
|
-
// skip: feature-name entity match is the preferred mapping
|
|
193
|
-
return;
|
|
194
|
-
}
|
|
195
|
-
const entityKeys = Object.keys(entities);
|
|
196
|
-
if (entityKeys.length === 1) {
|
|
197
|
-
handlerEntityMappings[handlerName] = entityKeys[0] as string;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
36
|
+
const state = createInitialFeatureBuilderState();
|
|
201
37
|
|
|
202
38
|
const registrar: FeatureRegistrar<TName> = {
|
|
203
39
|
systemScope(): void {
|
|
204
|
-
isSystemScoped = true;
|
|
40
|
+
state.isSystemScoped = true;
|
|
205
41
|
},
|
|
206
|
-
|
|
207
42
|
describe(text: string): void {
|
|
208
|
-
if (description !== undefined) {
|
|
43
|
+
if (state.description !== undefined) {
|
|
209
44
|
throw new Error(
|
|
210
45
|
`[Feature ${name}] r.describe() called twice — a feature's description is declared once`,
|
|
211
46
|
);
|
|
@@ -213,801 +48,114 @@ export function defineFeature<const TName extends string, TExports = undefined>(
|
|
|
213
48
|
if (typeof text !== "string" || text.trim().length === 0) {
|
|
214
49
|
throw new Error(`[Feature ${name}] r.describe(): text must be a non-empty string`);
|
|
215
50
|
}
|
|
216
|
-
description = text.trim();
|
|
51
|
+
state.description = text.trim();
|
|
217
52
|
},
|
|
218
|
-
|
|
219
53
|
requires: (() => {
|
|
220
|
-
const fn = (
|
|
221
|
-
|
|
54
|
+
const fn = (
|
|
55
|
+
...args: readonly [{ readonly features: readonly string[] }] | readonly string[]
|
|
56
|
+
) => {
|
|
57
|
+
state.requires.push(...resolveFeatureNamesArgs(args));
|
|
222
58
|
};
|
|
223
59
|
fn.projection = (tableName: string) => {
|
|
224
|
-
requiredProjections.add(tableName);
|
|
60
|
+
state.requiredProjections.add(tableName);
|
|
225
61
|
};
|
|
226
62
|
fn.step = (stepKind: string) => {
|
|
227
|
-
requiredSteps.add(stepKind);
|
|
63
|
+
state.requiredSteps.add(stepKind);
|
|
228
64
|
};
|
|
229
65
|
return fn as RequiresApi;
|
|
230
66
|
})(),
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
67
|
+
optionalRequires(
|
|
68
|
+
...args: readonly [{ readonly features: readonly string[] }] | readonly string[]
|
|
69
|
+
): void {
|
|
70
|
+
state.optionalRequires.push(...resolveFeatureNamesArgs(args));
|
|
234
71
|
},
|
|
235
|
-
|
|
236
72
|
toggleable(options: { default: boolean }): void {
|
|
237
|
-
if (toggleableDefault !== undefined) {
|
|
73
|
+
if (state.toggleableDefault !== undefined) {
|
|
238
74
|
throw new Error(
|
|
239
75
|
`[Feature ${name}] r.toggleable() called twice — a feature's toggleable status is declared once`,
|
|
240
76
|
);
|
|
241
77
|
}
|
|
242
|
-
toggleableDefault = options.default;
|
|
78
|
+
state.toggleableDefault = options.default;
|
|
243
79
|
},
|
|
244
|
-
|
|
245
80
|
uiHints(hints: UiHints): void {
|
|
246
|
-
if (uiHints !== undefined) {
|
|
81
|
+
if (state.uiHints !== undefined) {
|
|
247
82
|
throw new Error(`[Feature ${name}] r.uiHints() called twice — UI hints are declared once`);
|
|
248
83
|
}
|
|
249
|
-
uiHints = hints;
|
|
250
|
-
},
|
|
251
|
-
|
|
252
|
-
entity(
|
|
253
|
-
entityName: string,
|
|
254
|
-
definition: EntityDefinition,
|
|
255
|
-
options?: { readonly table?: unknown },
|
|
256
|
-
): EntityRef {
|
|
257
|
-
entities[entityName] = definition;
|
|
258
|
-
if (options?.table !== undefined) entityTables[entityName] = options.table;
|
|
259
|
-
return { name: entityName, table: definition.table ?? toTableName(entityName) };
|
|
260
|
-
},
|
|
261
|
-
|
|
262
|
-
writeHandler<TName extends string, TSchema extends ZodType>(
|
|
263
|
-
nameOrDef: string | WriteHandlerDefinition<TName, TSchema>,
|
|
264
|
-
schema?: TSchema,
|
|
265
|
-
handler?: WriteHandlerFn<z.infer<TSchema>>,
|
|
266
|
-
options?: { access?: AccessRule; rateLimit?: RateLimitOption },
|
|
267
|
-
): HandlerRef {
|
|
268
|
-
if (typeof nameOrDef === "object") {
|
|
269
|
-
const def = nameOrDef;
|
|
270
|
-
writeHandlers[def.name] = {
|
|
271
|
-
name: def.name,
|
|
272
|
-
schema: def.schema,
|
|
273
|
-
// @cast-boundary engine-bridge — typed Dev-API's handler is
|
|
274
|
-
// generic over the schema's parsed payload (`WriteEvent<output<TSchema>>`),
|
|
275
|
-
// the storage form WriteHandlerFn carries `WriteEvent<unknown>`.
|
|
276
|
-
// Function-arg variance: TS sees the typed handler as stricter
|
|
277
|
-
// than the loose storage shape and rejects direct assignment.
|
|
278
|
-
// The runtime value is identical — the cast crosses that boundary.
|
|
279
|
-
// `satisfies` does not work here (it asserts assignability, which
|
|
280
|
-
// is what fails). Explicit cast is the right tool.
|
|
281
|
-
handler: def.handler as WriteHandlerFn,
|
|
282
|
-
...(def.access && { access: def.access }),
|
|
283
|
-
...(def.unsafeSkipTransitionGuard && { unsafeSkipTransitionGuard: true }),
|
|
284
|
-
...(def.rateLimit && { rateLimit: def.rateLimit }),
|
|
285
|
-
// Forward the pipeline-build closure so boot-validators and
|
|
286
|
-
// Designer/AI tooling can inspect the step list. Absent on
|
|
287
|
-
// free-form handlers — defineWriteHandler only sets `perform`
|
|
288
|
-
// when the author used the pipeline form. Variance cast
|
|
289
|
-
// mirrors the handler-cast above: PipelineDef<output<TSchema>>
|
|
290
|
-
// is stricter than PipelineDef<unknown> for the same reason.
|
|
291
|
-
...(def.perform !== undefined && {
|
|
292
|
-
perform: def.perform as PipelineDef,
|
|
293
|
-
}),
|
|
294
|
-
};
|
|
295
|
-
tryMapEntity(def.name);
|
|
296
|
-
return { name: def.name };
|
|
297
|
-
}
|
|
298
|
-
if (!schema || !handler)
|
|
299
|
-
throw new Error("writeHandler inline form requires schema + handler");
|
|
300
|
-
writeHandlers[nameOrDef] = {
|
|
301
|
-
name: nameOrDef,
|
|
302
|
-
schema,
|
|
303
|
-
handler: handler as WriteHandlerFn, // @cast-boundary engine-bridge
|
|
304
|
-
...(options?.access && { access: options.access }),
|
|
305
|
-
...(options?.rateLimit && { rateLimit: options.rateLimit }),
|
|
306
|
-
};
|
|
307
|
-
tryMapEntity(nameOrDef);
|
|
308
|
-
return { name: nameOrDef };
|
|
309
|
-
},
|
|
310
|
-
|
|
311
|
-
queryHandler<TName extends string, TSchema extends ZodType>(
|
|
312
|
-
nameOrDef: string | QueryHandlerDefinition<TName, TSchema>,
|
|
313
|
-
schema?: TSchema,
|
|
314
|
-
handler?: QueryHandlerFn<z.infer<TSchema>>,
|
|
315
|
-
options?: { access?: AccessRule; rateLimit?: RateLimitOption },
|
|
316
|
-
): HandlerRef {
|
|
317
|
-
if (typeof nameOrDef === "object") {
|
|
318
|
-
const def = nameOrDef;
|
|
319
|
-
queryHandlers[def.name] = {
|
|
320
|
-
name: def.name,
|
|
321
|
-
schema: def.schema,
|
|
322
|
-
// @cast-boundary engine-bridge — typed Dev-API → erased internal storage
|
|
323
|
-
handler: def.handler as QueryHandlerFn, // @cast-boundary engine-bridge
|
|
324
|
-
...(def.access && { access: def.access }),
|
|
325
|
-
...(def.rateLimit && { rateLimit: def.rateLimit }),
|
|
326
|
-
};
|
|
327
|
-
tryMapEntity(def.name);
|
|
328
|
-
return { name: def.name };
|
|
329
|
-
}
|
|
330
|
-
if (!schema || !handler)
|
|
331
|
-
throw new Error("queryHandler inline form requires schema + handler");
|
|
332
|
-
queryHandlers[nameOrDef] = {
|
|
333
|
-
name: nameOrDef,
|
|
334
|
-
schema,
|
|
335
|
-
handler: handler as QueryHandlerFn, // @cast-boundary engine-bridge
|
|
336
|
-
...(options?.access && { access: options.access }),
|
|
337
|
-
...(options?.rateLimit && { rateLimit: options.rateLimit }),
|
|
338
|
-
};
|
|
339
|
-
tryMapEntity(nameOrDef);
|
|
340
|
-
return { name: nameOrDef };
|
|
341
|
-
},
|
|
342
|
-
|
|
343
|
-
relation(entityRef: NameOrRef, relationName: string, definition: RelationDefinition): void {
|
|
344
|
-
const entityName = resolveName(entityRef);
|
|
345
|
-
if (!relations[entityName]) relations[entityName] = {};
|
|
346
|
-
relations[entityName][relationName] = definition;
|
|
347
|
-
},
|
|
348
|
-
|
|
349
|
-
hook(
|
|
350
|
-
type: LifecycleHookType | "validation",
|
|
351
|
-
target: NameOrRef | readonly NameOrRef[],
|
|
352
|
-
fn: LifecycleHookFn | ValidationHookFn,
|
|
353
|
-
options?: { phase?: HookPhase },
|
|
354
|
-
): void {
|
|
355
|
-
const targets = Array.isArray(target) ? target : [target];
|
|
356
|
-
const names = targets.map(resolveName);
|
|
357
|
-
|
|
358
|
-
// Hook-fn casts unten alle: @cast-boundary engine-bridge
|
|
359
|
-
// — typed Dev-API (LifecycleHookFn|ValidationHookFn) → erased Map<name, fn>.
|
|
360
|
-
if (type === "validation") {
|
|
361
|
-
for (const n of names) {
|
|
362
|
-
validationHooks[n] = fn as ValidationHookFn; // @cast-boundary engine-bridge
|
|
363
|
-
}
|
|
364
|
-
// skip: validation hooks have no phase, stored and done
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (
|
|
369
|
-
type === LifecycleHookTypes.preSave ||
|
|
370
|
-
type === LifecycleHookTypes.preQuery ||
|
|
371
|
-
type === LifecycleHookTypes.postQuery
|
|
372
|
-
) {
|
|
373
|
-
if (!lifecycleHooks[type]) lifecycleHooks[type] = {};
|
|
374
|
-
for (const n of names) {
|
|
375
|
-
if (!lifecycleHooks[type][n]) lifecycleHooks[type][n] = [];
|
|
376
|
-
lifecycleHooks[type][n].push({ fn: fn as LifecycleHookFn, featureName: name }); // @cast-boundary engine-bridge
|
|
377
|
-
}
|
|
378
|
-
// skip: pre/post-hooks without phase semantics, stored and done
|
|
379
|
-
return;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Phased storage. preDelete has no phase option (always inTransaction);
|
|
383
|
-
// postSave/postDelete default to afterCommit.
|
|
384
|
-
const phase =
|
|
385
|
-
type === LifecycleHookTypes.preDelete
|
|
386
|
-
? HookPhases.inTransaction
|
|
387
|
-
: (options?.phase ?? HookPhases.afterCommit);
|
|
388
|
-
const bucket = phasedLifecycleHooks[type];
|
|
389
|
-
for (const n of names) {
|
|
390
|
-
if (!bucket[n]) bucket[n] = [];
|
|
391
|
-
bucket[n].push({ fn: fn as LifecycleHookFn, phase, featureName: name }); // @cast-boundary engine-bridge
|
|
392
|
-
}
|
|
393
|
-
},
|
|
394
|
-
|
|
395
|
-
entityHook(
|
|
396
|
-
type: "postSave" | "preDelete" | "postDelete" | "postQuery",
|
|
397
|
-
entityRef: NameOrRef,
|
|
398
|
-
fn: LifecycleHookFn,
|
|
399
|
-
options?: { phase?: HookPhase },
|
|
400
|
-
): void {
|
|
401
|
-
const entityName = resolveName(entityRef);
|
|
402
|
-
if (type === LifecycleHookTypes.postSave) {
|
|
403
|
-
const phase = options?.phase ?? HookPhases.afterCommit;
|
|
404
|
-
if (!entityPostSave[entityName]) entityPostSave[entityName] = [];
|
|
405
|
-
entityPostSave[entityName].push({ fn: fn as PostSaveHookFn, phase, featureName: name }); // @cast-boundary engine-bridge
|
|
406
|
-
} else if (type === LifecycleHookTypes.preDelete) {
|
|
407
|
-
if (!entityPreDelete[entityName]) entityPreDelete[entityName] = [];
|
|
408
|
-
entityPreDelete[entityName].push({
|
|
409
|
-
fn: fn as PreDeleteHookFn, // @cast-boundary engine-bridge
|
|
410
|
-
phase: HookPhases.inTransaction,
|
|
411
|
-
featureName: name,
|
|
412
|
-
});
|
|
413
|
-
} else if (type === LifecycleHookTypes.postDelete) {
|
|
414
|
-
const phase = options?.phase ?? HookPhases.afterCommit;
|
|
415
|
-
if (!entityPostDelete[entityName]) entityPostDelete[entityName] = [];
|
|
416
|
-
entityPostDelete[entityName].push({ fn: fn as PostDeleteHookFn, phase, featureName: name }); // @cast-boundary engine-bridge
|
|
417
|
-
} else if (type === LifecycleHookTypes.postQuery) {
|
|
418
|
-
// postQuery is unphased (no inTransaction/afterCommit semantics — fires
|
|
419
|
-
// synchronously after query-handler-execute, before field-access-filter)
|
|
420
|
-
if (!entityPostQuery[entityName]) entityPostQuery[entityName] = [];
|
|
421
|
-
entityPostQuery[entityName].push({ fn: fn as PostQueryHookFn, featureName: name }); // @cast-boundary engine-bridge
|
|
422
|
-
}
|
|
423
|
-
},
|
|
424
|
-
|
|
425
|
-
searchPayloadExtension(entityRef: NameOrRef, fn: SearchPayloadContributorFn): void {
|
|
426
|
-
const entityName = resolveName(entityRef);
|
|
427
|
-
if (!searchPayloadExtensions[entityName]) searchPayloadExtensions[entityName] = [];
|
|
428
|
-
searchPayloadExtensions[entityName].push({ fn, featureName: name });
|
|
429
|
-
},
|
|
430
|
-
|
|
431
|
-
config<TKeys extends Readonly<Record<string, ConfigKeyDefinition<ConfigKeyType>>>>(definition: {
|
|
432
|
-
readonly keys: TKeys;
|
|
433
|
-
readonly seeds?: Readonly<Record<string, ConfigSeedDef>>;
|
|
434
|
-
}): { readonly [K in keyof TKeys]: ConfigKeyHandle<TKeys[K]["type"]> } {
|
|
435
|
-
// Qualify eagerly (same as defineEvent) so the handle name matches what
|
|
436
|
-
// the registry stores — lazy qualification would break compile-time
|
|
437
|
-
// autocomplete and hand-built test registries.
|
|
438
|
-
const handles: Record<string, ConfigKeyHandle<ConfigKeyType>> = {};
|
|
439
|
-
for (const [key, keyDef] of Object.entries(definition.keys)) {
|
|
440
|
-
configKeys[key] = keyDef;
|
|
441
|
-
handles[key] = {
|
|
442
|
-
name: qn(toKebab(name), "config", toKebab(key)),
|
|
443
|
-
type: keyDef.type,
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
// Parse seeds: resolve qualified key names and validate scope
|
|
447
|
-
if (definition.seeds) {
|
|
448
|
-
for (const [shortKey, seedDef] of Object.entries(definition.seeds)) {
|
|
449
|
-
const keyDef = definition.keys[shortKey];
|
|
450
|
-
if (!keyDef) continue; // skip — boot-validator reports unknown keys
|
|
451
|
-
const qualifiedKey = qn(toKebab(name), "config", toKebab(shortKey));
|
|
452
|
-
const scope = seedDef.scope ?? keyDef.scope;
|
|
453
|
-
configSeeds.push({
|
|
454
|
-
key: qualifiedKey,
|
|
455
|
-
value: seedDef.value,
|
|
456
|
-
scope,
|
|
457
|
-
tenantId: seedDef.tenantId,
|
|
458
|
-
userId: seedDef.userId,
|
|
459
|
-
});
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
return handles as {
|
|
463
|
-
readonly [K in keyof TKeys]: ConfigKeyHandle<TKeys[K]["type"]>;
|
|
464
|
-
}; // @cast-boundary engine-bridge — Mapped-Type-Inference at config()-callsite
|
|
465
|
-
},
|
|
466
|
-
|
|
467
|
-
job(
|
|
468
|
-
jobName: string,
|
|
469
|
-
options: Omit<JobDefinition, "name" | "handler">,
|
|
470
|
-
handler: JobHandlerFn,
|
|
471
|
-
): void {
|
|
472
|
-
// Resolve NameOrRef(s) in trigger.on. Multi-Trigger-Form: Array
|
|
473
|
-
// wird zu Array von resolved strings, Single bleibt single string —
|
|
474
|
-
// job-runner unterscheidet anhand Array.isArray.
|
|
475
|
-
const trigger =
|
|
476
|
-
"on" in options.trigger
|
|
477
|
-
? {
|
|
478
|
-
on: Array.isArray(options.trigger.on)
|
|
479
|
-
? options.trigger.on.map(resolveName)
|
|
480
|
-
: resolveName(options.trigger.on as NameOrRef), // @cast-boundary engine-bridge
|
|
481
|
-
}
|
|
482
|
-
: options.trigger;
|
|
483
|
-
jobs[jobName] = { ...options, trigger, name: jobName, handler };
|
|
484
|
-
},
|
|
485
|
-
|
|
486
|
-
notification(
|
|
487
|
-
notificationName: string,
|
|
488
|
-
definition: {
|
|
489
|
-
readonly trigger: { readonly on: NameOrRef };
|
|
490
|
-
readonly recipient: NotificationRecipientFn;
|
|
491
|
-
readonly data: NotificationDataFn;
|
|
492
|
-
readonly templates?: Readonly<Record<string, NotificationTemplateFn>>;
|
|
493
|
-
},
|
|
494
|
-
): void {
|
|
495
|
-
notifications[notificationName] = {
|
|
496
|
-
name: notificationName,
|
|
497
|
-
trigger: { on: resolveName(definition.trigger.on) },
|
|
498
|
-
recipient: definition.recipient,
|
|
499
|
-
data: definition.data,
|
|
500
|
-
templates: definition.templates,
|
|
501
|
-
};
|
|
502
|
-
},
|
|
503
|
-
|
|
504
|
-
translations(def: TranslationsDef): void {
|
|
505
|
-
translations = { ...translations, ...def.keys };
|
|
506
|
-
},
|
|
507
|
-
|
|
508
|
-
defineEvent: <const TInner extends string, TPayload>(
|
|
509
|
-
eventName: TInner,
|
|
510
|
-
schema: ZodType<TPayload>,
|
|
511
|
-
options?: { readonly version?: number },
|
|
512
|
-
): EventDef<TPayload, QualifiedEventName<TName, TInner>> => {
|
|
513
|
-
// Return the fully-qualified event name so callers can pass it
|
|
514
|
-
// straight to ctx.appendEvent without hand-building the
|
|
515
|
-
// "<feature>:event:<name>" shape. Registry keeps events keyed by
|
|
516
|
-
// short name — qualification is the framework's job, not the feature
|
|
517
|
-
// author's.
|
|
518
|
-
//
|
|
519
|
-
// The runtime kebab-step (`qn(toKebab(feature), …)`) is mirrored at
|
|
520
|
-
// the type-level by `QualifiedEventName<TName, TInner>` so the
|
|
521
|
-
// returned `name` carries the literal qualified shape that the
|
|
522
|
-
// augmented `KumikoEventTypeMap` keys against.
|
|
523
|
-
const qualified = qn(toKebab(name), "event", toKebab(eventName));
|
|
524
|
-
const version = options?.version ?? 1;
|
|
525
|
-
if (!Number.isInteger(version) || version < 1) {
|
|
526
|
-
throw new Error(
|
|
527
|
-
`[Feature ${name}] defineEvent("${eventName}"): version must be a positive integer, got ${String(version)}`,
|
|
528
|
-
);
|
|
529
|
-
}
|
|
530
|
-
// @cast-boundary engine-bridge — runtime-string mirrors the
|
|
531
|
-
// template-literal-type via QualifiedEventName + toKebab. Both
|
|
532
|
-
// sides are tested (CamelToKebab type-tests + scan-events kebab
|
|
533
|
-
// tests), so the cast is a contract, not a typing-loss.
|
|
534
|
-
const def: EventDef<TPayload, QualifiedEventName<TName, TInner>> = {
|
|
535
|
-
name: qualified as QualifiedEventName<TName, TInner>,
|
|
536
|
-
schema,
|
|
537
|
-
version,
|
|
538
|
-
};
|
|
539
|
-
events[eventName] = def;
|
|
540
|
-
return def;
|
|
541
|
-
},
|
|
542
|
-
|
|
543
|
-
eventMigration(
|
|
544
|
-
eventName: string,
|
|
545
|
-
fromVersion: number,
|
|
546
|
-
toVersion: number,
|
|
547
|
-
transform: EventUpcastFn,
|
|
548
|
-
): void {
|
|
549
|
-
if (toVersion !== fromVersion + 1) {
|
|
550
|
-
throw new Error(
|
|
551
|
-
`[Feature ${name}] eventMigration("${eventName}", ${fromVersion}, ${toVersion}): ` +
|
|
552
|
-
`only single-step migrations are allowed — toVersion must be fromVersion + 1. ` +
|
|
553
|
-
`Chain larger jumps by registering each step separately.`,
|
|
554
|
-
);
|
|
555
|
-
}
|
|
556
|
-
if (!Number.isInteger(fromVersion) || fromVersion < 1) {
|
|
557
|
-
throw new Error(
|
|
558
|
-
`[Feature ${name}] eventMigration("${eventName}", ...): fromVersion must be >= 1, got ${String(fromVersion)}`,
|
|
559
|
-
);
|
|
560
|
-
}
|
|
561
|
-
const qualified = qn(toKebab(name), "event", toKebab(eventName));
|
|
562
|
-
const list = eventMigrations[eventName] ?? [];
|
|
563
|
-
if (list.some((m) => m.fromVersion === fromVersion)) {
|
|
564
|
-
throw new Error(
|
|
565
|
-
`[Feature ${name}] eventMigration("${eventName}", ${fromVersion}, ${toVersion}): ` +
|
|
566
|
-
`a migration from v${fromVersion} is already registered. Each step may only be declared once.`,
|
|
567
|
-
);
|
|
568
|
-
}
|
|
569
|
-
list.push({ eventName: qualified, fromVersion, toVersion, transform });
|
|
570
|
-
eventMigrations[eventName] = list;
|
|
571
|
-
},
|
|
572
|
-
|
|
573
|
-
readsConfig(...qualifiedKeys: string[]): void {
|
|
574
|
-
configReads.push(...qualifiedKeys);
|
|
575
|
-
},
|
|
576
|
-
|
|
577
|
-
referenceData(
|
|
578
|
-
entityRef: NameOrRef,
|
|
579
|
-
data: readonly Record<string, unknown>[],
|
|
580
|
-
options?: { upsertKey?: string },
|
|
581
|
-
): void {
|
|
582
|
-
referenceData.push({
|
|
583
|
-
entityName: resolveName(entityRef),
|
|
584
|
-
data,
|
|
585
|
-
upsertKey: options?.upsertKey,
|
|
586
|
-
});
|
|
587
|
-
},
|
|
588
|
-
|
|
589
|
-
extendsRegistrar(extensionName: string, def: RegistrarExtensionDef): void {
|
|
590
|
-
registrarExtensions[extensionName] = def;
|
|
591
|
-
},
|
|
592
|
-
|
|
593
|
-
useExtension(
|
|
594
|
-
extensionName: string,
|
|
595
|
-
entityRef: NameOrRef,
|
|
596
|
-
options?: Record<string, unknown>,
|
|
597
|
-
): void {
|
|
598
|
-
extensionUsages.push({ extensionName, entityName: resolveName(entityRef), options });
|
|
599
|
-
},
|
|
600
|
-
|
|
601
|
-
extensionSelector(extensionName: string, key: { readonly name: string } | string): void {
|
|
602
|
-
if (extensionSelectors.some((s) => s.extensionName === extensionName)) {
|
|
603
|
-
throw new Error(
|
|
604
|
-
`[Feature ${name}] extensionSelector("${extensionName}") declared twice — ` +
|
|
605
|
-
`one selector key per extension point.`,
|
|
606
|
-
);
|
|
607
|
-
}
|
|
608
|
-
const qualifiedKey = typeof key === "string" ? key : key.name;
|
|
609
|
-
extensionSelectors.push({ extensionName, qualifiedKey });
|
|
610
|
-
},
|
|
611
|
-
|
|
612
|
-
/**
|
|
613
|
-
* Marker-Deklaration: dieses Feature stellt eine Cross-Feature-API
|
|
614
|
-
* unter dem genannten Namen bereit. Die eigentliche Implementation
|
|
615
|
-
* wird separat als Query- oder Write-Handler unter dem QN-Pattern
|
|
616
|
-
* registriert; r.exposesApi ist reine Boot-Check-Surface.
|
|
617
|
-
*
|
|
618
|
-
* Beispiel:
|
|
619
|
-
* defineFeature("compliance-profiles", (r) => {
|
|
620
|
-
* r.exposesApi("compliance.forTenant");
|
|
621
|
-
* r.queryHandler({ name: "compliance:query:for-tenant", ... });
|
|
622
|
-
* });
|
|
623
|
-
* defineFeature("user-data-rights", (r) => {
|
|
624
|
-
* r.requires("compliance-profiles");
|
|
625
|
-
* r.usesApi("compliance.forTenant");
|
|
626
|
-
* // ruft im Handler: ctx.callQuery("compliance:query:for-tenant", ...)
|
|
627
|
-
* });
|
|
628
|
-
*/
|
|
629
|
-
exposesApi(apiName: string): void {
|
|
630
|
-
if (exposedApis.has(apiName)) {
|
|
631
|
-
throw new Error(
|
|
632
|
-
`[Feature ${name}] r.exposesApi("${apiName}") called twice — API names must be unique within a feature.`,
|
|
633
|
-
);
|
|
634
|
-
}
|
|
635
|
-
exposedApis.add(apiName);
|
|
636
|
-
},
|
|
637
|
-
|
|
638
|
-
/**
|
|
639
|
-
* Declares that this feature calls a cross-feature API. Boot-Validator
|
|
640
|
-
* checkt dass irgendein anderes Feature `r.exposesApi(name)` macht und
|
|
641
|
-
* dass dieses Feature `r.requires` darauf hat.
|
|
642
|
-
*/
|
|
643
|
-
usesApi(apiName: string): void {
|
|
644
|
-
usedApis.add(apiName);
|
|
645
|
-
},
|
|
646
|
-
|
|
647
|
-
metric(shortName: string, options: MetricOptions): void {
|
|
648
|
-
if (metrics[shortName]) {
|
|
649
|
-
throw new Error(
|
|
650
|
-
`[Feature ${name}] Metric "${shortName}" already registered. ` +
|
|
651
|
-
`Metric names must be unique per feature.`,
|
|
652
|
-
);
|
|
653
|
-
}
|
|
654
|
-
metrics[shortName] = { shortName, ...options };
|
|
655
|
-
},
|
|
656
|
-
|
|
657
|
-
envSchema(schema: z.ZodObject<z.ZodRawShape>): void {
|
|
658
|
-
if (envSchema !== undefined) {
|
|
659
|
-
throw new Error(
|
|
660
|
-
`[Feature ${name}] r.envSchema() called twice — declare one composed Zod-object per feature.`,
|
|
661
|
-
);
|
|
662
|
-
}
|
|
663
|
-
envSchema = schema;
|
|
664
|
-
},
|
|
665
|
-
|
|
666
|
-
secret(shortName: string, options: SecretOptions): SecretKeyHandle {
|
|
667
|
-
if (secretKeys[shortName]) {
|
|
668
|
-
throw new Error(
|
|
669
|
-
`[Feature ${name}] Secret "${shortName}" already registered. ` +
|
|
670
|
-
`Secret key names must be unique per feature.`,
|
|
671
|
-
);
|
|
672
|
-
}
|
|
673
|
-
// Qualified name follows the framework's "<feature>:<type>:<name>"
|
|
674
|
-
// QN convention — same pattern config / jobs / events use. toKebab
|
|
675
|
-
// handles the common input shapes ("stripe.apiKey" → "stripe-api-key")
|
|
676
|
-
// so features can declare keys in their natural style without
|
|
677
|
-
// thinking about kebab-case on every call.
|
|
678
|
-
const qualifiedName = qn(toKebab(name), QnTypes.secret, toKebab(shortName));
|
|
679
|
-
secretKeys[shortName] = {
|
|
680
|
-
shortName,
|
|
681
|
-
qualifiedName,
|
|
682
|
-
...options,
|
|
683
|
-
};
|
|
684
|
-
return { name: qualifiedName };
|
|
685
|
-
},
|
|
686
|
-
|
|
687
|
-
projection(definition: ProjectionDefinition): void {
|
|
688
|
-
// Reject names that would blow up at registry-boot when we qualify them.
|
|
689
|
-
// Catch it at the registration site so the stack trace points at the
|
|
690
|
-
// feature file, not at framework internals.
|
|
691
|
-
if (!isKebabSegment(definition.name)) {
|
|
692
|
-
throw new Error(
|
|
693
|
-
`[Feature ${name}] Projection name "${definition.name}" must be kebab-case ` +
|
|
694
|
-
`(lowercase letters, digits, dashes; start with a letter). ` +
|
|
695
|
-
`Got "${definition.name}" — try "${toKebab(definition.name).replace(/_/g, "-")}".`,
|
|
696
|
-
);
|
|
697
|
-
}
|
|
698
|
-
if (projections[definition.name]) {
|
|
699
|
-
throw new Error(
|
|
700
|
-
`[Feature ${name}] Projection "${definition.name}" already registered. ` +
|
|
701
|
-
`Projection names must be unique per feature.`,
|
|
702
|
-
);
|
|
703
|
-
}
|
|
704
|
-
projections[definition.name] = definition;
|
|
705
|
-
},
|
|
706
|
-
|
|
707
|
-
multiStreamProjection(definition: MultiStreamProjectionDefinition): void {
|
|
708
|
-
if (!isKebabSegment(definition.name)) {
|
|
709
|
-
throw new Error(
|
|
710
|
-
`[Feature ${name}] MultiStreamProjection name "${definition.name}" must be kebab-case ` +
|
|
711
|
-
`(lowercase letters, digits, dashes; start with a letter). ` +
|
|
712
|
-
`Got "${definition.name}" — try "${toKebab(definition.name).replace(/_/g, "-")}".`,
|
|
713
|
-
);
|
|
714
|
-
}
|
|
715
|
-
if (multiStreamProjections[definition.name] || projections[definition.name]) {
|
|
716
|
-
throw new Error(
|
|
717
|
-
`[Feature ${name}] Projection name "${definition.name}" already registered. ` +
|
|
718
|
-
`r.projection and r.multiStreamProjection share a namespace — pick a unique short name.`,
|
|
719
|
-
);
|
|
720
|
-
}
|
|
721
|
-
if (Object.keys(definition.apply).length === 0) {
|
|
722
|
-
throw new Error(
|
|
723
|
-
`[Feature ${name}] MultiStreamProjection "${definition.name}" has no apply handlers. ` +
|
|
724
|
-
`Declare at least one event type it reacts to, otherwise the dispatcher has nothing to route.`,
|
|
725
|
-
);
|
|
726
|
-
}
|
|
727
|
-
multiStreamProjections[definition.name] = definition;
|
|
728
|
-
},
|
|
729
|
-
|
|
730
|
-
authClaims(fn: AuthClaimsFn): void {
|
|
731
|
-
authClaimsHooks.push(fn);
|
|
732
|
-
},
|
|
733
|
-
|
|
734
|
-
screen(definition: ScreenDefinition): void {
|
|
735
|
-
// Reject kebab-drift at registration-time so the stack trace points at
|
|
736
|
-
// the feature file, not at registry-boot. Same guard pattern as
|
|
737
|
-
// r.projection / r.multiStreamProjection.
|
|
738
|
-
if (!isKebabSegment(definition.id)) {
|
|
739
|
-
throw new Error(
|
|
740
|
-
`[Feature ${name}] Screen id "${definition.id}" must be kebab-case ` +
|
|
741
|
-
`(lowercase letters, digits, dashes; start with a letter). ` +
|
|
742
|
-
`Got "${definition.id}" — try "${toKebab(definition.id).replace(/_/g, "-")}".`,
|
|
743
|
-
);
|
|
744
|
-
}
|
|
745
|
-
if (screens[definition.id]) {
|
|
746
|
-
throw new Error(
|
|
747
|
-
`[Feature ${name}] Screen "${definition.id}" already registered. ` +
|
|
748
|
-
`Screen ids must be unique per feature.`,
|
|
749
|
-
);
|
|
750
|
-
}
|
|
751
|
-
screens[definition.id] = definition;
|
|
752
|
-
},
|
|
753
|
-
|
|
754
|
-
nav(definition: NavDefinition): void {
|
|
755
|
-
// Reject kebab-drift at registration-time so the stack trace points at
|
|
756
|
-
// the feature file, not at registry-boot. Same guard pattern as
|
|
757
|
-
// r.projection / r.multiStreamProjection / r.screen.
|
|
758
|
-
if (!isKebabSegment(definition.id)) {
|
|
759
|
-
throw new Error(
|
|
760
|
-
`[Feature ${name}] Nav id "${definition.id}" must be kebab-case ` +
|
|
761
|
-
`(lowercase letters, digits, dashes; start with a letter). ` +
|
|
762
|
-
`Got "${definition.id}" — try "${toKebab(definition.id).replace(/_/g, "-")}".`,
|
|
763
|
-
);
|
|
764
|
-
}
|
|
765
|
-
if (navs[definition.id]) {
|
|
766
|
-
throw new Error(
|
|
767
|
-
`[Feature ${name}] Nav entry "${definition.id}" already registered. ` +
|
|
768
|
-
`Nav ids must be unique per feature.`,
|
|
769
|
-
);
|
|
770
|
-
}
|
|
771
|
-
navs[definition.id] = definition;
|
|
772
|
-
},
|
|
773
|
-
|
|
774
|
-
workspace(definition: WorkspaceDefinition): void {
|
|
775
|
-
// Same kebab guard as r.screen / r.nav so authoring-time mistakes
|
|
776
|
-
// surface at the feature file, not deep in registry boot.
|
|
777
|
-
if (!isKebabSegment(definition.id)) {
|
|
778
|
-
throw new Error(
|
|
779
|
-
`[Feature ${name}] Workspace id "${definition.id}" must be kebab-case ` +
|
|
780
|
-
`(lowercase letters, digits, dashes; start with a letter). ` +
|
|
781
|
-
`Got "${definition.id}" — try "${toKebab(definition.id).replace(/_/g, "-")}".`,
|
|
782
|
-
);
|
|
783
|
-
}
|
|
784
|
-
if (workspaces[definition.id]) {
|
|
785
|
-
throw new Error(
|
|
786
|
-
`[Feature ${name}] Workspace "${definition.id}" already registered. ` +
|
|
787
|
-
`Workspace ids must be unique per feature.`,
|
|
788
|
-
);
|
|
789
|
-
}
|
|
790
|
-
workspaces[definition.id] = definition;
|
|
791
|
-
},
|
|
792
|
-
|
|
793
|
-
httpRoute(definition: HttpRouteDefinition): void {
|
|
794
|
-
// Path-Validation: muss mit "/" beginnen, keine /api/-Routes (die
|
|
795
|
-
// sind dem Dispatcher reserviert; eine HTTP-Route die /api/foo
|
|
796
|
-
// belegt, würde die Auth-Middleware umgehen ohne dass der Author
|
|
797
|
-
// das ausgesprochen hat — bewusster Block).
|
|
798
|
-
if (!definition.path.startsWith("/")) {
|
|
799
|
-
throw new Error(
|
|
800
|
-
`[Feature ${name}] httpRoute path "${definition.path}" must start with "/". ` +
|
|
801
|
-
`Got "${definition.path}".`,
|
|
802
|
-
);
|
|
803
|
-
}
|
|
804
|
-
if (definition.path === "/api" || definition.path.startsWith("/api/")) {
|
|
805
|
-
throw new Error(
|
|
806
|
-
`[Feature ${name}] httpRoute path "${definition.path}" is in the /api/* namespace ` +
|
|
807
|
-
`which is reserved for the dispatcher (write/query/batch/auth/sse). ` +
|
|
808
|
-
`Pick a different path or use r.queryHandler / r.writeHandler.`,
|
|
809
|
-
);
|
|
810
|
-
}
|
|
811
|
-
const key = `${definition.method} ${definition.path}`;
|
|
812
|
-
if (httpRoutes[key]) {
|
|
813
|
-
throw new Error(
|
|
814
|
-
`[Feature ${name}] HTTP-Route "${key}" already registered. ` +
|
|
815
|
-
`method + path must be unique per feature.`,
|
|
816
|
-
);
|
|
817
|
-
}
|
|
818
|
-
httpRoutes[key] = definition;
|
|
819
|
-
},
|
|
820
|
-
|
|
821
|
-
rawTable(rawTableName: string, table: unknown, options: RawTableOptions): void {
|
|
822
|
-
// Same kebab guard as r.projection / r.screen / r.nav so authoring-time
|
|
823
|
-
// mistakes surface at the feature file, not deep in registry boot.
|
|
824
|
-
if (!isKebabSegment(rawTableName)) {
|
|
825
|
-
throw new Error(
|
|
826
|
-
`[Feature ${name}] Raw-table name "${rawTableName}" must be kebab-case ` +
|
|
827
|
-
`(lowercase letters, digits, dashes; start with a letter). ` +
|
|
828
|
-
`Got "${rawTableName}" — try "${toKebab(rawTableName).replace(/_/g, "-")}".`,
|
|
829
|
-
);
|
|
830
|
-
}
|
|
831
|
-
if (rawTables[rawTableName]) {
|
|
832
|
-
throw new Error(
|
|
833
|
-
`[Feature ${name}] r.rawTable("${rawTableName}") already registered. ` +
|
|
834
|
-
`Raw-table names must be unique per feature.`,
|
|
835
|
-
);
|
|
836
|
-
}
|
|
837
|
-
// The `reason` is the marker that justifies the bypass — empty
|
|
838
|
-
// strings would defeat the audit trail. Reject early so the
|
|
839
|
-
// failure points at the feature file.
|
|
840
|
-
if (typeof options.reason !== "string" || options.reason.trim().length === 0) {
|
|
841
|
-
throw new Error(
|
|
842
|
-
`[Feature ${name}] r.rawTable("${rawTableName}"): options.reason must be a ` +
|
|
843
|
-
`non-empty string. The reason is the marker that justifies the bypass — ` +
|
|
844
|
-
`if you can't write one, declare data via r.entity() instead.`,
|
|
845
|
-
);
|
|
846
|
-
}
|
|
847
|
-
rawTables[rawTableName] = {
|
|
848
|
-
name: rawTableName,
|
|
849
|
-
table,
|
|
850
|
-
reason: options.reason,
|
|
851
|
-
};
|
|
852
|
-
},
|
|
853
|
-
|
|
854
|
-
unmanagedTable(meta: EntityTableMeta, options: RawTableOptions): void {
|
|
855
|
-
// Name comes from the meta itself — apps already give the table a
|
|
856
|
-
// name when calling defineUnmanagedTable, no need to repeat it.
|
|
857
|
-
const tableName = meta.tableName;
|
|
858
|
-
if (!isKebabSegment(tableName.replace(/_/g, "-"))) {
|
|
859
|
-
// EntityTableMeta uses snake_case for tableName (matches Postgres
|
|
860
|
-
// convention); we just guard against truly broken input.
|
|
861
|
-
throw new Error(
|
|
862
|
-
`[Feature ${name}] Unmanaged-table name "${tableName}" must be a ` +
|
|
863
|
-
`valid identifier (lowercase letters, digits, underscores; start with a letter).`,
|
|
864
|
-
);
|
|
865
|
-
}
|
|
866
|
-
if (unmanagedTables[tableName]) {
|
|
867
|
-
throw new Error(
|
|
868
|
-
`[Feature ${name}] r.unmanagedTable("${tableName}") already registered. ` +
|
|
869
|
-
`Unmanaged-table names must be unique per feature.`,
|
|
870
|
-
);
|
|
871
|
-
}
|
|
872
|
-
if (typeof options.reason !== "string" || options.reason.trim().length === 0) {
|
|
873
|
-
throw new Error(
|
|
874
|
-
`[Feature ${name}] r.unmanagedTable("${tableName}"): options.reason must be a ` +
|
|
875
|
-
`non-empty string. The reason justifies the audit-trail bypass — ` +
|
|
876
|
-
`if you can't write one, declare data via r.entity() instead.`,
|
|
877
|
-
);
|
|
878
|
-
}
|
|
879
|
-
unmanagedTables[tableName] = {
|
|
880
|
-
name: tableName,
|
|
881
|
-
meta,
|
|
882
|
-
reason: options.reason,
|
|
883
|
-
};
|
|
884
|
-
},
|
|
885
|
-
|
|
886
|
-
claimKey<T extends ClaimKeyType>(
|
|
887
|
-
shortName: string,
|
|
888
|
-
options: { readonly type: T },
|
|
889
|
-
): ClaimKeyHandle<T> {
|
|
890
|
-
if (claimKeys[shortName]) {
|
|
891
|
-
throw new Error(
|
|
892
|
-
`[Feature ${name}] Claim key "${shortName}" already declared. ` +
|
|
893
|
-
"Claim short-names must be unique per feature.",
|
|
894
|
-
);
|
|
895
|
-
}
|
|
896
|
-
// Claim keys are NOT full QNs — the JWT shape is 2-segment
|
|
897
|
-
// "<featureName>:<shortName>" (same as Translation keys), not
|
|
898
|
-
// kebab-cased. The authClaims resolver prefixes with the raw
|
|
899
|
-
// feature.name + the raw inner key the hook returns, so the handle's
|
|
900
|
-
// `name` must match that literal string exactly for `readClaim` to
|
|
901
|
-
// find the value. kebab-conversion here would break the round-trip.
|
|
902
|
-
const qualifiedName = `${name}:${shortName}`;
|
|
903
|
-
claimKeys[shortName] = {
|
|
904
|
-
shortName,
|
|
905
|
-
qualifiedName,
|
|
906
|
-
type: options.type,
|
|
907
|
-
};
|
|
908
|
-
return { name: qualifiedName, type: options.type };
|
|
909
|
-
},
|
|
910
|
-
|
|
911
|
-
treeActions<const TActions extends Record<string, TreeActionDef>>(
|
|
912
|
-
actions: TActions,
|
|
913
|
-
): TreeActionsHandle<TName, TActions> {
|
|
914
|
-
// Only-once-guard: zweiter Aufruf ist Author-Bug, soll am
|
|
915
|
-
// Feature-File aufschlagen (gleicher Stil wie r.toggleable).
|
|
916
|
-
if (treeActions !== undefined) {
|
|
917
|
-
throw new Error(
|
|
918
|
-
`[Feature ${name}] r.treeActions() already called. ` +
|
|
919
|
-
`Each feature may declare a single tree-actions schema.`,
|
|
920
|
-
);
|
|
921
|
-
}
|
|
922
|
-
treeActions = actions;
|
|
923
|
-
// Return typed handle für setup-export. Frozen damit Caller die
|
|
924
|
-
// Map nicht nachträglich mutieren (würde Pattern-AST + Runtime-
|
|
925
|
-
// Lookup divergieren lassen).
|
|
926
|
-
return Object.freeze({
|
|
927
|
-
id: name,
|
|
928
|
-
treeActions: actions,
|
|
929
|
-
});
|
|
930
|
-
},
|
|
931
|
-
|
|
932
|
-
tree(provider: TreeChildrenSubscribe): void {
|
|
933
|
-
// Only-once-guard analog zu r.treeActions.
|
|
934
|
-
if (treeProvider !== undefined) {
|
|
935
|
-
throw new Error(
|
|
936
|
-
`[Feature ${name}] r.tree() already called. ` +
|
|
937
|
-
`Each feature may declare a single tree-provider.`,
|
|
938
|
-
);
|
|
939
|
-
}
|
|
940
|
-
treeProvider = provider;
|
|
84
|
+
state.uiHints = hints;
|
|
941
85
|
},
|
|
86
|
+
...buildEntityHandlerMethods(state, name),
|
|
87
|
+
...buildConfigEventsJobsMethods(state, name),
|
|
88
|
+
...buildUiExtensionsMethods(state, name),
|
|
942
89
|
};
|
|
943
90
|
|
|
944
91
|
const exports = setup(registrar) as TExports; // @cast-boundary engine-bridge
|
|
945
92
|
|
|
946
93
|
return {
|
|
947
94
|
name,
|
|
948
|
-
...(description !== undefined && { description }),
|
|
949
|
-
systemScope: isSystemScoped,
|
|
95
|
+
...(state.description !== undefined && { description: state.description }),
|
|
96
|
+
systemScope: state.isSystemScoped,
|
|
950
97
|
exports,
|
|
951
|
-
requires,
|
|
952
|
-
optionalRequires,
|
|
953
|
-
requiredProjections,
|
|
954
|
-
requiredSteps,
|
|
955
|
-
...(toggleableDefault !== undefined && { toggleableDefault }),
|
|
956
|
-
...(uiHints !== undefined && { uiHints }),
|
|
957
|
-
entities,
|
|
958
|
-
entityTables,
|
|
959
|
-
relations,
|
|
960
|
-
writeHandlers,
|
|
961
|
-
queryHandlers,
|
|
962
|
-
|
|
98
|
+
requires: state.requires,
|
|
99
|
+
optionalRequires: state.optionalRequires,
|
|
100
|
+
requiredProjections: state.requiredProjections,
|
|
101
|
+
requiredSteps: state.requiredSteps,
|
|
102
|
+
...(state.toggleableDefault !== undefined && { toggleableDefault: state.toggleableDefault }),
|
|
103
|
+
...(state.uiHints !== undefined && { uiHints: state.uiHints }),
|
|
104
|
+
entities: state.entities,
|
|
105
|
+
entityTables: state.entityTables,
|
|
106
|
+
relations: state.relations,
|
|
107
|
+
writeHandlers: state.writeHandlers,
|
|
108
|
+
queryHandlers: state.queryHandlers,
|
|
109
|
+
streamHandlers: state.streamHandlers,
|
|
110
|
+
translations: state.translations,
|
|
963
111
|
hooks: {
|
|
964
|
-
validation: validationHooks,
|
|
965
|
-
preSave: lifecycleHooks["preSave"] ?? {},
|
|
966
|
-
postSave: phasedLifecycleHooks.postSave,
|
|
967
|
-
preDelete: phasedLifecycleHooks.preDelete,
|
|
968
|
-
postDelete: phasedLifecycleHooks.postDelete,
|
|
969
|
-
preQuery: lifecycleHooks["preQuery"] ?? {},
|
|
970
|
-
postQuery: lifecycleHooks["postQuery"] ?? {},
|
|
112
|
+
validation: state.validationHooks,
|
|
113
|
+
preSave: state.lifecycleHooks["preSave"] ?? {},
|
|
114
|
+
postSave: state.phasedLifecycleHooks.postSave,
|
|
115
|
+
preDelete: state.phasedLifecycleHooks.preDelete,
|
|
116
|
+
postDelete: state.phasedLifecycleHooks.postDelete,
|
|
117
|
+
preQuery: state.lifecycleHooks["preQuery"] ?? {},
|
|
118
|
+
postQuery: state.lifecycleHooks["postQuery"] ?? {},
|
|
971
119
|
// @cast-boundary engine-bridge — die Hook-Registrierung erased die
|
|
972
120
|
// per-Slot-Signaturen zu LifecycleHookFn (Union, s. Cast in
|
|
973
121
|
// addLifecycleHook); die Branches dort sind die einzigen Producer und
|
|
974
122
|
// schreiben pro Slot typrichtig.
|
|
975
123
|
} as HookMap,
|
|
976
124
|
entityHooks: {
|
|
977
|
-
postSave: entityPostSave,
|
|
978
|
-
preDelete: entityPreDelete,
|
|
979
|
-
postDelete: entityPostDelete,
|
|
980
|
-
postQuery: entityPostQuery,
|
|
981
|
-
},
|
|
982
|
-
searchPayloadExtensions,
|
|
983
|
-
configKeys,
|
|
984
|
-
configSeeds,
|
|
985
|
-
jobs,
|
|
986
|
-
notifications,
|
|
987
|
-
registrarExtensions,
|
|
988
|
-
extensionUsages,
|
|
989
|
-
extensionSelectors,
|
|
990
|
-
exposedApis,
|
|
991
|
-
usedApis,
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
...(
|
|
1011
|
-
...(envSchema !== undefined && { envSchema }),
|
|
125
|
+
postSave: state.entityPostSave,
|
|
126
|
+
preDelete: state.entityPreDelete,
|
|
127
|
+
postDelete: state.entityPostDelete,
|
|
128
|
+
postQuery: state.entityPostQuery,
|
|
129
|
+
},
|
|
130
|
+
searchPayloadExtensions: state.searchPayloadExtensions,
|
|
131
|
+
configKeys: state.configKeys,
|
|
132
|
+
configSeeds: state.configSeeds,
|
|
133
|
+
jobs: state.jobs,
|
|
134
|
+
notifications: state.notifications,
|
|
135
|
+
registrarExtensions: state.registrarExtensions,
|
|
136
|
+
extensionUsages: state.extensionUsages,
|
|
137
|
+
extensionSelectors: state.extensionSelectors,
|
|
138
|
+
exposedApis: state.exposedApis,
|
|
139
|
+
usedApis: state.usedApis,
|
|
140
|
+
bootChecks: state.bootChecks,
|
|
141
|
+
referenceData: state.referenceData,
|
|
142
|
+
events: state.events,
|
|
143
|
+
eventMigrations: state.eventMigrations,
|
|
144
|
+
configReads: state.configReads,
|
|
145
|
+
handlerEntityMappings: state.handlerEntityMappings,
|
|
146
|
+
metrics: state.metrics,
|
|
147
|
+
secretKeys: state.secretKeys,
|
|
148
|
+
projections: state.projections,
|
|
149
|
+
entityProjectionExtensions: state.entityProjectionExtensions,
|
|
150
|
+
multiStreamProjections: state.multiStreamProjections,
|
|
151
|
+
authClaimsHooks: state.authClaimsHooks,
|
|
152
|
+
claimKeys: state.claimKeys,
|
|
153
|
+
screens: state.screens,
|
|
154
|
+
navs: state.navs,
|
|
155
|
+
workspaces: state.workspaces,
|
|
156
|
+
httpRoutes: state.httpRoutes,
|
|
157
|
+
storeTables: state.storeTables,
|
|
158
|
+
...(state.treeActions !== undefined && { treeActions: state.treeActions }),
|
|
159
|
+
...(state.envSchema !== undefined && { envSchema: state.envSchema }),
|
|
1012
160
|
};
|
|
1013
161
|
}
|