@cosmicdrift/kumiko-framework 0.386.0 → 0.388.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/AGENTS.md +6 -1
- package/LICENSE +21 -57
- package/README.md +2 -2
- package/agents/examples/anonymous-access.md +1 -1
- package/agents/examples/apex-landing.md +204 -0
- package/agents/examples/api-worker-split.md +64 -0
- package/agents/examples/app-config.md +14 -0
- package/agents/examples/auth-mfa.md +115 -0
- package/agents/examples/env-composition.md +28 -1
- package/agents/examples/legal-pages.md +1 -1
- package/agents/examples/prod-boot-options.md +16 -0
- package/agents/examples/rate-limiting.md +48 -0
- package/agents/examples/record-detail-layout.md +692 -0
- package/agents/examples/seed-migration.md +212 -0
- package/agents/examples/wizard-form.md +941 -0
- package/agents/examples/writeform-section.md +548 -0
- package/agents/features.md +19 -2
- package/agents/widgets.md +40 -39
- package/dist/api/api-constants.js +2 -2
- package/dist/api/auth-middleware.js +7 -7
- package/dist/api/auth-rate-limiters.js +7 -3
- package/dist/api/auth-routes-mfa.js +1 -1
- package/dist/api/auth-routes-session.js +1 -1
- package/dist/api/auth-routes-shared.js +7 -3
- package/dist/api/auth-routes-signup.js +1 -1
- package/dist/api/client-error-route.js +8 -2
- package/dist/api/client-ip.js +3 -1
- package/dist/api/extra-route.js +1 -1
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +3 -1
- package/dist/api/pii-leak-guard.js +4 -2
- package/dist/api/readiness.js +5 -5
- package/dist/api/redis-sse-broker.js +8 -6
- package/dist/api/request-context.d.ts +1 -1
- package/dist/api/request-context.js +1 -1
- package/dist/api/routes.js +2 -2
- package/dist/api/server-boot-checks.d.ts +10 -0
- package/dist/api/server-boot-checks.js +157 -0
- package/dist/api/server-event-dispatcher.d.ts +26 -0
- package/dist/api/server-event-dispatcher.js +208 -0
- package/dist/api/server-extra-routes.d.ts +11 -0
- package/dist/api/server-extra-routes.js +203 -0
- package/dist/api/server-middleware.d.ts +20 -0
- package/dist/api/server-middleware.js +211 -0
- package/dist/api/server-routes.d.ts +19 -0
- package/dist/api/server-routes.js +293 -0
- package/dist/api/server-websocket-routes.d.ts +4 -0
- package/dist/api/server-websocket-routes.js +108 -0
- package/dist/api/server-wiring.d.ts +32 -0
- package/dist/api/server-wiring.js +236 -0
- package/dist/api/server.d.ts +10 -15
- package/dist/api/server.js +48 -1250
- package/dist/api/session-cookies.js +4 -4
- package/dist/api/sse-broker.js +1 -1
- package/dist/api/sse-route.d.ts +13 -13
- package/dist/api/sse-route.js +31 -27
- package/dist/auth/index.js +4 -4
- package/dist/auth/roles.d.ts +4 -4
- package/dist/auth/roles.js +24 -24
- package/dist/compliance/duration-spec.js +18 -20
- package/dist/compliance/index.js +2 -3
- package/dist/compliance/override-schema.d.ts +4 -4
- package/dist/compliance/override-schema.js +19 -22
- package/dist/compliance/profiles.d.ts +26 -29
- package/dist/compliance/profiles.js +62 -69
- package/dist/compliance/sub-processors.d.ts +28 -30
- package/dist/compliance/sub-processors.js +17 -21
- package/dist/consumer-cli.js +4 -4
- package/dist/crypto/blind-index.js +18 -17
- package/dist/crypto/ciphertext-pattern.js +1 -1
- package/dist/crypto/event-pii.js +3 -3
- package/dist/crypto/kek-source.d.ts +1 -1
- package/dist/crypto/kek-source.js +3 -2
- package/dist/crypto/kms-adapter.d.ts +1 -1
- package/dist/crypto/kms-adapter.js +4 -6
- package/dist/crypto/kms-wiring.d.ts +1 -2
- package/dist/crypto/kms-wiring.js +2 -2
- package/dist/crypto/owner-transition-rekey.d.ts +5 -1
- package/dist/crypto/owner-transition-rekey.js +1 -1
- package/dist/crypto/pg-kms-adapter.js +2 -6
- package/dist/crypto/pii-field-encryption.d.ts +5 -1
- package/dist/crypto/pii-field-encryption.js +7 -5
- package/dist/crypto/subject-resolver.d.ts +1 -1
- package/dist/crypto/subject-resolver.js +2 -2
- package/dist/db/aggregate-sql.d.ts +21 -0
- package/dist/db/aggregate-sql.js +155 -0
- package/dist/db/apply-entity-event.d.ts +6 -6
- package/dist/db/apply-entity-event.js +55 -55
- package/dist/db/blind-index-cleanup.js +7 -7
- package/dist/db/closed-connection.d.ts +9 -0
- package/dist/db/closed-connection.js +95 -0
- package/dist/db/collect-table-metas.js +10 -11
- package/dist/db/compound-types.d.ts +1 -1
- package/dist/db/compound-types.js +12 -12
- package/dist/db/cursor.js +3 -4
- package/dist/db/dialect.d.ts +2 -2
- package/dist/db/dialect.js +3 -3
- package/dist/db/distinct-on.d.ts +1 -1
- package/dist/db/driver-row-coercion.d.ts +2 -0
- package/dist/db/driver-row-coercion.js +186 -0
- package/dist/db/driver-value-preparation.d.ts +16 -0
- package/dist/db/driver-value-preparation.js +99 -0
- package/dist/db/eagerload.d.ts +3 -3
- package/dist/db/eagerload.js +21 -12
- package/dist/db/entity-field-encryption.d.ts +1 -0
- package/dist/db/entity-field-encryption.js +1 -1
- package/dist/db/entity-table-meta.d.ts +2 -2
- package/dist/db/entity-table-meta.js +105 -112
- package/dist/db/event-store-executor-context.d.ts +3 -2
- package/dist/db/event-store-executor-context.js +13 -13
- package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
- package/dist/db/event-store-executor-read-aggregate.js +112 -0
- package/dist/db/event-store-executor-read-filter.d.ts +31 -0
- package/dist/db/event-store-executor-read-filter.js +203 -0
- package/dist/db/event-store-executor-read-paging.d.ts +14 -0
- package/dist/db/event-store-executor-read-paging.js +99 -0
- package/dist/db/event-store-executor-read-rows.d.ts +6 -0
- package/dist/db/event-store-executor-read-rows.js +32 -0
- package/dist/db/event-store-executor-read-search.d.ts +28 -0
- package/dist/db/event-store-executor-read-search.js +221 -0
- package/dist/db/event-store-executor-read-sort.d.ts +41 -0
- package/dist/db/event-store-executor-read-sort.js +130 -0
- package/dist/db/event-store-executor-read-where.d.ts +24 -0
- package/dist/db/event-store-executor-read-where.js +81 -0
- package/dist/db/event-store-executor-read.d.ts +2 -9
- package/dist/db/event-store-executor-read.js +50 -704
- package/dist/db/event-store-executor-write-steps.d.ts +20 -0
- package/dist/db/event-store-executor-write-steps.js +100 -0
- package/dist/db/event-store-executor-write.js +401 -510
- package/dist/db/index.d.ts +6 -2
- package/dist/db/index.js +5 -1
- package/dist/db/list-aggregate-labels.d.ts +14 -0
- package/dist/db/list-aggregate-labels.js +26 -0
- package/dist/db/located-timestamp.d.ts +11 -11
- package/dist/db/located-timestamp.js +29 -30
- package/dist/db/migrate-generator.d.ts +1 -1
- package/dist/db/migrate-generator.js +97 -105
- package/dist/db/migrate-runner.js +29 -26
- package/dist/db/money.d.ts +6 -6
- package/dist/db/money.js +29 -27
- package/dist/db/queries/backfill-pii.js +5 -5
- package/dist/db/queries/ddl.js +1 -1
- package/dist/db/queries/es-ops.d.ts +1 -1
- package/dist/db/queries/es-ops.js +1 -1
- package/dist/db/queries/event-consumer.js +3 -3
- package/dist/db/queries/event-store.js +4 -2
- package/dist/db/queries/projection-rebuild.js +3 -3
- package/dist/db/queries/raw-sql.d.ts +2 -2
- package/dist/db/queries/raw-sql.js +2 -2
- package/dist/db/queries/shadow-swap.js +19 -19
- package/dist/db/query.d.ts +3 -16
- package/dist/db/query.js +8 -761
- package/dist/db/rebuild-marker.js +20 -21
- package/dist/db/reference-data.js +6 -1
- package/dist/db/replay-migration-sql.js +2 -2
- package/dist/db/schema-inspection.js +4 -5
- package/dist/db/table-builder.d.ts +1 -2
- package/dist/db/table-builder.js +137 -190
- package/dist/db/table-info.d.ts +10 -0
- package/dist/db/table-info.js +45 -0
- package/dist/db/temporal-value-guards.d.ts +6 -0
- package/dist/db/temporal-value-guards.js +15 -0
- package/dist/db/tenant-db-runner.d.ts +1 -0
- package/dist/db/tenant-db-runner.js +3 -0
- package/dist/db/tenant-db.d.ts +2 -2
- package/dist/db/tenant-db.js +48 -11
- package/dist/db/where-clause.d.ts +6 -0
- package/dist/db/where-clause.js +236 -0
- package/dist/derivatives/delete-stored-file.js +6 -2
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/engine/boot-validator/access-roles.js +4 -3
- package/dist/engine/boot-validator/api-ext.js +14 -14
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/config-deps.js +7 -6
- package/dist/engine/boot-validator/detail-screens.js +4 -4
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +77 -61
- package/dist/engine/boot-validator/form-derivations.js +35 -1
- package/dist/engine/boot-validator/gdpr-storage.js +4 -4
- package/dist/engine/boot-validator/i18n-keys.js +6 -4
- package/dist/engine/boot-validator/index.d.ts +2 -2
- package/dist/engine/boot-validator/index.js +22 -20
- package/dist/engine/boot-validator/nav.js +5 -5
- package/dist/engine/boot-validator/ownership.js +12 -9
- package/dist/engine/boot-validator/parent-ref.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +258 -187
- package/dist/engine/boot-validator/projection-list-screens.js +7 -7
- package/dist/engine/boot-validator/query-refs.js +1 -1
- package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
- package/dist/engine/boot-validator/screens-actions.js +23 -17
- package/dist/engine/boot-validator/screens-dashboard.js +2 -5
- package/dist/engine/boot-validator/screens-forms.js +12 -17
- package/dist/engine/boot-validator/screens-kind-config-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-config-edit.js +88 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-edit.js +55 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-entity-list.js +228 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-detail.js +235 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-projection-list.js +52 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.d.ts +3 -0
- package/dist/engine/boot-validator/screens-kind-secret-mint.js +28 -0
- package/dist/engine/boot-validator/screens-kind-shared.d.ts +37 -0
- package/dist/engine/boot-validator/screens-kind-shared.js +142 -0
- package/dist/engine/boot-validator/screens-lists.d.ts +2 -1
- package/dist/engine/boot-validator/screens-lists.js +26 -24
- package/dist/engine/boot-validator/screens.d.ts +0 -2
- package/dist/engine/boot-validator/screens.js +102 -869
- package/dist/engine/boot-validator/security-baseline.js +3 -2
- package/dist/engine/boot-validator/transfer-graph.js +2 -2
- package/dist/engine/boot-validator/workspaces.js +3 -3
- package/dist/engine/bridge-stub.js +3 -4
- package/dist/engine/build-app-schema.js +130 -170
- package/dist/engine/build-config-feature-schema.js +50 -50
- package/dist/engine/build-target.js +20 -23
- package/dist/engine/codemod/pipeline-codemod.js +20 -13
- package/dist/engine/config-helpers.js +4 -4
- package/dist/engine/constants.js +2 -2
- package/dist/engine/create-app.js +2 -2
- package/dist/engine/define-feature.js +21 -13
- package/dist/engine/define-handler.js +13 -11
- package/dist/engine/define-step.js +1 -1
- package/dist/engine/define-workflow.d.ts +5 -5
- package/dist/engine/define-workflow.js +6 -6
- package/dist/engine/entity-handlers.d.ts +13 -0
- package/dist/engine/entity-handlers.js +22 -34
- package/dist/engine/entity-list-runtime-options.d.ts +8 -0
- package/dist/engine/entity-list-runtime-options.js +18 -0
- package/dist/engine/event-def.js +2 -2
- package/dist/engine/extension-names.d.ts +16 -17
- package/dist/engine/extension-names.js +37 -38
- package/dist/engine/extensions/escape-hatch-usage.js +1 -1
- package/dist/engine/extensions/tenant-data.js +2 -2
- package/dist/engine/extensions/user-data.d.ts +23 -24
- package/dist/engine/factories.d.ts +14 -13
- package/dist/engine/factories.js +38 -37
- package/dist/engine/feature-ast/entity-field-types.js +1 -1
- package/dist/engine/feature-ast/extractors/handlers.js +2 -1
- package/dist/engine/feature-ast/extractors/hooks.js +1 -1
- package/dist/engine/feature-ast/extractors/round2.js +25 -6
- package/dist/engine/feature-ast/extractors/shared.d.ts +2 -3
- package/dist/engine/feature-ast/extractors/shared.js +2 -3
- package/dist/engine/feature-ast/parse.js +13 -13
- package/dist/engine/feature-ast/patch.js +4 -182
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +3 -0
- package/dist/engine/feature-ast/pattern-change-schema.js +2 -1
- package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
- package/dist/engine/feature-ast/pattern-id-matchers.js +210 -0
- package/dist/engine/feature-ast/patterns.d.ts +11 -2
- package/dist/engine/feature-ast/patterns.js +43 -51
- package/dist/engine/feature-ast/render.js +50 -89
- package/dist/engine/feature-config-events-jobs.js +15 -12
- package/dist/engine/feature-entity-handlers.js +12 -5
- package/dist/engine/feature-manifest.d.ts +9 -9
- package/dist/engine/feature-manifest.js +6 -7
- package/dist/engine/feature-ui-extensions.d.ts +3 -3
- package/dist/engine/feature-ui-extensions.js +30 -22
- package/dist/engine/field-access.js +5 -6
- package/dist/engine/form-derivations.js +18 -7
- package/dist/engine/handler-field-references.js +1 -1
- package/dist/engine/index.d.ts +1 -0
- package/dist/engine/index.js +4 -4
- package/dist/engine/object-form.d.ts +1 -0
- package/dist/engine/object-form.js +14 -4
- package/dist/engine/ownership.js +3 -3
- package/dist/engine/parse-ref-target.js +5 -7
- package/dist/engine/pattern-library/index.js +2 -2
- package/dist/engine/pattern-library/library.js +2 -2
- package/dist/engine/pattern-library/types.js +3 -3
- package/dist/engine/pipeline.js +2 -2
- package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
- package/dist/engine/project-app-schema-for-roles.js +7 -5
- package/dist/engine/projection-helpers.js +9 -9
- package/dist/engine/qualified-name.js +2 -2
- package/dist/engine/registry-facade.js +1 -1
- package/dist/engine/registry-ingest.js +13 -13
- package/dist/engine/registry-state.js +17 -19
- package/dist/engine/registry-validate.js +12 -12
- package/dist/engine/run-in.js +2 -2
- package/dist/engine/run-pipeline.js +7 -0
- package/dist/engine/schema-builder.js +251 -233
- package/dist/engine/screen-filter-ops.js +17 -20
- package/dist/engine/screen-helpers.js +4 -3
- package/dist/engine/screen-kind.d.ts +7 -0
- package/dist/engine/screen-kind.js +1 -0
- package/dist/engine/state-machine.d.ts +12 -12
- package/dist/engine/state-machine.js +7 -7
- package/dist/engine/steps/_resolver-utils.js +4 -3
- package/dist/engine/steps/for-each.js +4 -4
- package/dist/engine/steps/unsafe-projection-delete.js +1 -1
- package/dist/engine/steps/unsafe-projection-upsert.js +4 -4
- package/dist/engine/steps/wait-for-event.js +4 -4
- package/dist/engine/steps/workflow-system-events.js +3 -3
- package/dist/engine/strip-disabled-features-from-app-schema.js +51 -14
- package/dist/engine/tier-resolver-extension.d.ts +29 -27
- package/dist/engine/tier-resolver-extension.js +26 -26
- package/dist/engine/transfer-adjacency.js +2 -2
- package/dist/engine/validate-projection-allowlist.js +9 -28
- package/dist/engine/walk-step-instances.d.ts +2 -0
- package/dist/engine/walk-step-instances.js +38 -0
- package/dist/engine/where-rule-lint.js +1 -1
- package/dist/engine/write-handler-qn-extract.js +1 -1
- package/dist/entrypoint/index.d.ts +4 -1
- package/dist/entrypoint/index.js +10 -4
- package/dist/env/index.d.ts +7 -6
- package/dist/env/index.js +10 -9
- package/dist/errors/docs-url.d.ts +1 -1
- package/dist/errors/docs-url.js +22 -2
- package/dist/errors/transition-details.js +7 -7
- package/dist/errors/write-error-info.d.ts +6 -6
- package/dist/errors/write-error-info.js +6 -6
- package/dist/es-ops/context.d.ts +9 -9
- package/dist/es-ops/context.js +29 -29
- package/dist/es-ops/index.js +7 -9
- package/dist/es-ops/operations-schema.js +4 -10
- package/dist/es-ops/runner.d.ts +11 -11
- package/dist/es-ops/runner.js +49 -53
- package/dist/es-ops/types.d.ts +55 -55
- package/dist/es-ops/types.js +11 -11
- package/dist/event-store/admin-api.js +7 -8
- package/dist/event-store/errors.js +1 -1
- package/dist/event-store/event-store.js +12 -6
- package/dist/event-store/rebuild-dead-letter.js +1 -1
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/create-file-ref-from-bytes.js +4 -1
- package/dist/files/feature.js +9 -10
- package/dist/files/file-handle.js +7 -9
- package/dist/files/file-ref-entity.js +15 -15
- package/dist/files/file-ref-table.js +7 -8
- package/dist/files/file-routes.d.ts +7 -0
- package/dist/files/file-routes.js +22 -13
- package/dist/files/in-memory-provider.js +8 -9
- package/dist/files/local-provider.js +18 -25
- package/dist/files/provider-resolver.d.ts +7 -7
- package/dist/files/resolve-content-type.js +43 -0
- package/dist/files/storage-tracking.js +2 -2
- package/dist/files/upload-ledger.js +7 -0
- package/dist/files/zip-stream.d.ts +12 -19
- package/dist/files/zip-stream.js +44 -63
- package/dist/i18n/required-surface-keys.d.ts +1 -1
- package/dist/i18n/required-surface-keys.js +188 -212
- package/dist/internal/stack-support.d.ts +2 -1
- package/dist/internal/stack-support.js +1 -1
- package/dist/jobs/job-runner-context.d.ts +32 -0
- package/dist/jobs/job-runner-context.js +305 -0
- package/dist/jobs/job-runner-execution.d.ts +7 -0
- package/dist/jobs/job-runner-execution.js +160 -0
- package/dist/jobs/job-runner-gates.d.ts +19 -0
- package/dist/jobs/job-runner-gates.js +198 -0
- package/dist/jobs/job-runner-helpers.d.ts +17 -3
- package/dist/jobs/job-runner-helpers.js +69 -4
- package/dist/jobs/job-runner-schedulers.d.ts +1 -1
- package/dist/jobs/job-runner-schedulers.js +3 -4
- package/dist/jobs/job-runner-scope.d.ts +44 -0
- package/dist/jobs/job-runner-scope.js +102 -0
- package/dist/jobs/job-runner-types.d.ts +12 -0
- package/dist/jobs/job-runner.d.ts +0 -1
- package/dist/jobs/job-runner.js +57 -669
- package/dist/lifecycle/lifecycle.js +3 -5
- package/dist/lifecycle/signal-handlers.js +5 -4
- package/dist/logging/utils.d.ts +2 -1
- package/dist/logging/utils.js +38 -10
- package/dist/migrations/index.js +1 -1
- package/dist/migrations/kumiko-drift.js +14 -13
- package/dist/migrations/pending-rebuilds.d.ts +21 -20
- package/dist/migrations/pending-rebuilds.js +27 -27
- package/dist/migrations/projection-table-index.d.ts +3 -3
- package/dist/migrations/projection-table-index.js +8 -8
- package/dist/observability/escape-hatch-report.d.ts +3 -0
- package/dist/observability/escape-hatch-report.js +12 -1
- package/dist/observability/metric-validator.js +1 -1
- package/dist/observability/metrics-handle.js +3 -3
- package/dist/pipeline/append-event-core.js +11 -2
- package/dist/pipeline/cascade-handler.d.ts +1 -1
- package/dist/pipeline/cascade-handler.js +165 -138
- package/dist/pipeline/dispatch-batch.js +2 -2
- package/dist/pipeline/dispatch-query.js +20 -13
- package/dist/pipeline/dispatch-shared.d.ts +14 -3
- package/dist/pipeline/dispatch-shared.js +67 -721
- package/dist/pipeline/dispatch-stream.js +1 -1
- package/dist/pipeline/dispatch-write.js +297 -190
- package/dist/pipeline/dispatcher-utils.d.ts +1 -1
- package/dist/pipeline/dispatcher-utils.js +1 -1
- package/dist/pipeline/event-consumer-state.js +2 -2
- package/dist/pipeline/event-dispatcher-admin.js +2 -1
- package/dist/pipeline/event-dispatcher-delivery.js +4 -6
- package/dist/pipeline/event-dispatcher.js +17 -28
- package/dist/pipeline/handler-context-auth.d.ts +9 -0
- package/dist/pipeline/handler-context-auth.js +54 -0
- package/dist/pipeline/handler-context-config.d.ts +14 -0
- package/dist/pipeline/handler-context-config.js +113 -0
- package/dist/pipeline/handler-context-db.d.ts +24 -0
- package/dist/pipeline/handler-context-db.js +149 -0
- package/dist/pipeline/handler-context-dispatch.d.ts +21 -0
- package/dist/pipeline/handler-context-dispatch.js +89 -0
- package/dist/pipeline/handler-context-events.d.ts +43 -0
- package/dist/pipeline/handler-context-events.js +237 -0
- package/dist/pipeline/handler-context-notify.d.ts +12 -0
- package/dist/pipeline/handler-context-notify.js +55 -0
- package/dist/pipeline/handler-context-observability.d.ts +7 -0
- package/dist/pipeline/handler-context-observability.js +21 -0
- package/dist/pipeline/handler-context-scope.d.ts +48 -0
- package/dist/pipeline/handler-context-scope.js +88 -0
- package/dist/pipeline/irreversible-operation-gate.js +1 -1
- package/dist/pipeline/lifecycle-pipeline.js +9 -9
- package/dist/pipeline/member-read-only-transaction.js +1 -3
- package/dist/pipeline/msp-rebuild.js +1 -1
- package/dist/pipeline/notify-scope.js +12 -1
- package/dist/pipeline/pre-handler-gates.js +20 -4
- package/dist/pipeline/pre-transaction-reservations.d.ts +1 -0
- package/dist/pipeline/pre-transaction-reservations.js +9 -3
- package/dist/pipeline/projection-rebuild.js +15 -13
- package/dist/pipeline/system-hooks.js +39 -41
- package/dist/pipeline/system-identity-switch.js +1 -3
- package/dist/pipeline/tenant-timezone-cache.js +1 -1
- package/dist/random/generate.d.ts +29 -29
- package/dist/random/generate.js +30 -30
- package/dist/random/words.js +20 -20
- package/dist/rate-limit/middleware.js +9 -5
- package/dist/redis/client.js +2 -2
- package/dist/redis/pubsub-signal.js +14 -17
- package/dist/schema-cli/apply-command.d.ts +2 -0
- package/dist/schema-cli/apply-command.js +97 -0
- package/dist/schema-cli/baseline-command.d.ts +2 -0
- package/dist/schema-cli/baseline-command.js +33 -0
- package/dist/schema-cli/generate-command.d.ts +2 -0
- package/dist/schema-cli/generate-command.js +90 -0
- package/dist/schema-cli/shared.d.ts +39 -0
- package/dist/schema-cli/shared.js +95 -0
- package/dist/schema-cli/status-command.d.ts +2 -0
- package/dist/schema-cli/status-command.js +73 -0
- package/dist/schema-cli/validate-boot-input.d.ts +5 -0
- package/dist/schema-cli/validate-boot-input.js +130 -0
- package/dist/schema-cli/validate-command.d.ts +2 -0
- package/dist/schema-cli/validate-command.js +130 -0
- package/dist/schema-cli.d.ts +2 -18
- package/dist/schema-cli.js +47 -510
- package/dist/search/index.js +4 -4
- package/dist/search/meilisearch-adapter.js +2 -1
- package/dist/search/purge-subject.js +4 -5
- package/dist/search/reindex-entity.js +6 -9
- package/dist/time/boot-tz-warning.d.ts +2 -2
- package/dist/time/boot-tz-warning.js +8 -8
- package/dist/time/iana.js +10 -10
- package/dist/time/index.js +9 -9
- package/dist/time/legacy-date.d.ts +2 -2
- package/dist/time/legacy-date.js +6 -8
- package/dist/time/polyfill.js +1 -1
- package/dist/time/tz-context.d.ts +1 -1
- package/dist/time/tz-context.js +1 -1
- package/dist/ui-types/app-schema.js +7 -7
- package/dist/ui-types/list-row-meta.js +4 -4
- package/dist/upgrade-cli.js +159 -130
- package/dist/utils/compare.js +1 -1
- package/package.json +5 -5
- package/src/changes.json +358 -0
- package/src/scripts/codemod/crypto-shredding-testing-move.ts +2 -2
- package/src/scripts/codemod/feature-layout.ts +10 -2
- package/src/scripts/codemod/migrate-cross-tenant.ts +1 -1
- package/src/scripts/codemod/migrate-open-to-all.ts +1 -1
- package/src/scripts/codemod/object-form.ts +41 -4
- package/src/scripts/codemod/package-merge.ts +58 -8
- package/src/scripts/codemod/pii-personal-migration.ts +192 -153
- package/src/scripts/codemod/testing-injection.ts +122 -42
- package/src/scripts/codemod/testing-move.ts +29 -7
- package/src/scripts/codemod/unprocessable-error-details-reason.ts +2 -2
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { runInSavepoint } from "../db/query.js";
|
|
2
|
+
import { writeFailure } from "../errors/index.js";
|
|
3
|
+
import { runBatch, unwrapSingle } from "./dispatch-batch.js";
|
|
4
|
+
import { executeQuery } from "./dispatch-query.js";
|
|
5
|
+
import { perCommandNestedDenied } from "./dispatch-shared.js";
|
|
6
|
+
import { executeWrite } from "./dispatch-write.js";
|
|
7
|
+
import { isFailedWriteResult, NestedWriteRollback, } from "./dispatcher-utils.js";
|
|
8
|
+
import { createGatedIdentitySwitch } from "./system-identity-switch.js";
|
|
9
|
+
export function createCommitSinks(afterCommitHooks, transactionScope, outerNotifySink) {
|
|
10
|
+
// Cross-feature bridge: ctx.query/write share the current tx + afterCommitHooks sink.
|
|
11
|
+
// queryAs/writeAs to anyone but the caller itself needs r.systemScope() or { escapeHatch } (system-identity-switch.ts).
|
|
12
|
+
const bridgeSink = afterCommitHooks ?? [];
|
|
13
|
+
// Query bridges and post-commit contexts get a throwaway sink: hooks pushed there never run.
|
|
14
|
+
const hasLiveCommitSink = afterCommitHooks !== undefined && transactionScope !== "postCommit";
|
|
15
|
+
return {
|
|
16
|
+
bridgeSink,
|
|
17
|
+
hasLiveCommitSink,
|
|
18
|
+
notifyCommitSink: hasLiveCommitSink ? bridgeSink : outerNotifySink,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
// A failed inner write has already appended its event and projections when a later
|
|
22
|
+
// step (e.g. an inTransaction hook) fails, and the outer handler may swallow the
|
|
23
|
+
// failure result. The savepoint makes the failure undo exactly the inner write; the
|
|
24
|
+
// inner afterCommit hooks only reach the outer sink if it succeeded.
|
|
25
|
+
async function runNestedWriteInSavepoint(scope, sinks, asUser, targetType, payload) {
|
|
26
|
+
const { ctx, registry, origin, tx } = scope;
|
|
27
|
+
const { bridgeSink } = sinks;
|
|
28
|
+
if (registry.getWriteHandler(targetType)?.transaction === "perCommand") {
|
|
29
|
+
return writeFailure(perCommandNestedDenied(targetType));
|
|
30
|
+
}
|
|
31
|
+
if (!tx)
|
|
32
|
+
return executeWrite(ctx, targetType, payload, asUser, origin, tx, bridgeSink);
|
|
33
|
+
const innerAfterCommitHooks = [];
|
|
34
|
+
try {
|
|
35
|
+
const result = await runInSavepoint(tx, async (savepointTx) => {
|
|
36
|
+
const innerResult = await executeWrite(ctx, targetType, payload, asUser, origin, savepointTx, // @cast-boundary db-runner savepoint handle is a transaction handle
|
|
37
|
+
innerAfterCommitHooks);
|
|
38
|
+
if (isFailedWriteResult(innerResult))
|
|
39
|
+
throw new NestedWriteRollback(innerResult);
|
|
40
|
+
return innerResult;
|
|
41
|
+
});
|
|
42
|
+
bridgeSink.push(...innerAfterCommitHooks);
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
if (e instanceof NestedWriteRollback)
|
|
47
|
+
return e.failure;
|
|
48
|
+
throw e;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Savepoints on one connection are not serialized by the driver: two parallel nested writes would
|
|
52
|
+
// interleave their statements and a ROLLBACK TO of the first would also discard the second's rows.
|
|
53
|
+
// Writes nested one level deeper run on their own savepoint handle and therefore their own chain.
|
|
54
|
+
function createSerializedNestedWriter(scope, sinks) {
|
|
55
|
+
let nestedWriteChain = Promise.resolve();
|
|
56
|
+
return (asUser, targetType, payload) => {
|
|
57
|
+
const run = nestedWriteChain.then(() => runNestedWriteInSavepoint(scope, sinks, asUser, targetType, payload));
|
|
58
|
+
nestedWriteChain = run.catch(() => undefined);
|
|
59
|
+
return run;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function buildDispatchFacet(scope, sinks) {
|
|
63
|
+
const { ctx, type, user, origin, tx, transactionScope, hasIdentitySwitchGrant, identitySwitchAudit, } = scope;
|
|
64
|
+
const scheduleAfterCommit = (hook) => {
|
|
65
|
+
sinks.bridgeSink.push(hook);
|
|
66
|
+
};
|
|
67
|
+
const executeNestedWriteInSavepoint = createSerializedNestedWriter(scope, sinks);
|
|
68
|
+
// Without an open transaction (post-commit hooks, stream/query handlers, perCommand handlers) a bare
|
|
69
|
+
// executeWrite would auto-commit event append, projections and inTransaction hooks separately and its
|
|
70
|
+
// afterCommit hooks would land in a sink nobody flushes. A batch-of-one gives the write its own
|
|
71
|
+
// transaction and its own flush.
|
|
72
|
+
const writeAsUser = async (asUser, targetType, payload) => transactionScope === "inTransaction"
|
|
73
|
+
? executeNestedWriteInSavepoint(asUser, targetType, payload)
|
|
74
|
+
: unwrapSingle(await runBatch(ctx, [{ type: targetType, payload }], asUser, undefined, origin));
|
|
75
|
+
const identitySwitch = createGatedIdentitySwitch(`handler "${type}"`, user, hasIdentitySwitchGrant, {
|
|
76
|
+
// Inherits the caller's origin, so switching to SYSTEM cannot shed an anonymous root.
|
|
77
|
+
queryAs: (asUser, targetType, payload) => executeQuery(ctx, targetType, payload, asUser, origin, tx, sinks.notifyCommitSink), // @wrapper-known semantic-alias
|
|
78
|
+
writeAs: writeAsUser,
|
|
79
|
+
}, identitySwitchAudit);
|
|
80
|
+
return {
|
|
81
|
+
bridge: {
|
|
82
|
+
query: (targetType, payload) => executeQuery(ctx, targetType, payload, user, origin, tx, sinks.notifyCommitSink), // @wrapper-known semantic-alias
|
|
83
|
+
queryAs: identitySwitch.queryAs,
|
|
84
|
+
write: (targetType, payload) => writeAsUser(user, targetType, payload),
|
|
85
|
+
writeAs: identitySwitch.writeAs,
|
|
86
|
+
},
|
|
87
|
+
scheduleFragment: { ...(sinks.hasLiveCommitSink && { scheduleAfterCommit }) },
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { AggregateStreamHandle, AppendEventArgs, AppendEventFn, FetchForWritingArgs } from "../engine/types/index.ts";
|
|
2
|
+
import { IdempotentAppendConflictError as EventStoreIdempotentAppendConflictError, VersionConflictError as EventStoreVersionConflictError } from "../event-store/errors.ts";
|
|
3
|
+
import { type StoredEvent } from "../event-store/event-store.ts";
|
|
4
|
+
import { type LoadAggregateWithSnapshotOptions, type LoadAggregateWithSnapshotResult, type SnapshotReducer } from "../event-store/snapshot.ts";
|
|
5
|
+
import type { HandlerContextScope } from "./handler-context-scope.ts";
|
|
6
|
+
export declare function buildEventStoreFacet(scope: HandlerContextScope): {
|
|
7
|
+
appendEvent: AppendEventFn;
|
|
8
|
+
unsafeAppendEvent: (args: AppendEventArgs) => Promise<void>;
|
|
9
|
+
tryAppendEvent: (args: AppendEventArgs) => Promise<{
|
|
10
|
+
ok: true;
|
|
11
|
+
event: StoredEvent;
|
|
12
|
+
conflict?: undefined;
|
|
13
|
+
} | {
|
|
14
|
+
event?: undefined;
|
|
15
|
+
ok: false;
|
|
16
|
+
conflict: EventStoreVersionConflictError;
|
|
17
|
+
} | {
|
|
18
|
+
event?: undefined;
|
|
19
|
+
ok: false;
|
|
20
|
+
conflict: EventStoreIdempotentAppendConflictError;
|
|
21
|
+
}>;
|
|
22
|
+
fetchForWriting: (args: FetchForWritingArgs) => Promise<AggregateStreamHandle>;
|
|
23
|
+
loadAggregate: (aggregateId: string, loadOptions?: {
|
|
24
|
+
readonly asOf?: Temporal.Instant;
|
|
25
|
+
}) => Promise<readonly StoredEvent[]>;
|
|
26
|
+
archiveStream: (aggregateId: string, archiveArgs: {
|
|
27
|
+
readonly aggregateType: string;
|
|
28
|
+
readonly reason?: string;
|
|
29
|
+
}) => Promise<void>;
|
|
30
|
+
restoreStream: (aggregateId: string) => Promise<void>;
|
|
31
|
+
isStreamArchived: (aggregateId: string) => Promise<boolean>;
|
|
32
|
+
snapshotAggregate: (snapshotArgs: {
|
|
33
|
+
readonly aggregateId: string;
|
|
34
|
+
readonly aggregateType: string;
|
|
35
|
+
readonly version: number;
|
|
36
|
+
readonly state: Record<string, unknown>;
|
|
37
|
+
readonly snapshotVersion?: number;
|
|
38
|
+
}) => Promise<void>;
|
|
39
|
+
loadAggregateWithSnapshot: <TState extends Record<string, unknown>>(aggregateId: string, reducer: SnapshotReducer<TState>, initial: TState, loadOptions?: Omit<LoadAggregateWithSnapshotOptions, "upcastEvent" | "entityEventVersionOf">) => Promise<LoadAggregateWithSnapshotResult<TState>>;
|
|
40
|
+
queryProjection: <T = Record<string, unknown>>(qualifiedName: string, options?: {
|
|
41
|
+
readonly unsafeAllTenants?: boolean;
|
|
42
|
+
}) => Promise<readonly T[]>;
|
|
43
|
+
};
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { runInSavepoint, selectMany } from "../db/query.js";
|
|
2
|
+
import { hasTenantColumn } from "../db/tenant-db.js";
|
|
3
|
+
import { InternalError, memberResolutionReadOnlyDenied, VersionConflictError, } from "../errors/index.js";
|
|
4
|
+
import { archiveStream as archiveStreamHelper, isStreamArchived, restoreStream as restoreStreamHelper, } from "../event-store/archive.js";
|
|
5
|
+
import { IdempotentAppendConflictError as EventStoreIdempotentAppendConflictError, VersionConflictError as EventStoreVersionConflictError, } from "../event-store/errors.js";
|
|
6
|
+
import { loadAggregate, loadAggregateAsOf, loadAggregateForWriting, } from "../event-store/event-store.js";
|
|
7
|
+
import { effectiveSnapshotGeneration, loadAggregateWithSnapshot, saveSnapshot, } from "../event-store/snapshot.js";
|
|
8
|
+
import { upcastStoredEvent, upcastStoredEvents } from "../event-store/upcaster.js";
|
|
9
|
+
import { appendDomainEventCore } from "./append-event-core.js";
|
|
10
|
+
import { isMemberResolutionPrincipal, resolveDbSource, } from "./dispatch-shared.js";
|
|
11
|
+
import { createGatedProjectionReader } from "./system-identity-switch.js";
|
|
12
|
+
function requireDbSource(ctx, tx, surfaceCall) {
|
|
13
|
+
const dbSource = resolveDbSource(ctx, tx);
|
|
14
|
+
if (!dbSource) {
|
|
15
|
+
throw new InternalError({
|
|
16
|
+
message: `${surfaceCall} requires a database connection — none is configured.`,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return dbSource;
|
|
20
|
+
}
|
|
21
|
+
// ctx.appendEvent — append a domain event onto a specific aggregate stream
|
|
22
|
+
// in the current tx, then fire matching inline projections. Core logic
|
|
23
|
+
// lives in appendDomainEventCore; this wrapper just locates dbSource +
|
|
24
|
+
// stringifies the SessionUser id for the shared helper.
|
|
25
|
+
async function appendDomainEvent(ctx, args, user, tx, callerFeature) {
|
|
26
|
+
// Sink behind every append surface, so a resolved member stays read-only even via fetchForWriting handles.
|
|
27
|
+
if (isMemberResolutionPrincipal(user))
|
|
28
|
+
throw memberResolutionReadOnlyDenied();
|
|
29
|
+
const { registry } = ctx;
|
|
30
|
+
const dbSource = requireDbSource(ctx, tx, `ctx.appendEvent("${args.type}")`);
|
|
31
|
+
await appendDomainEventCore({
|
|
32
|
+
registry,
|
|
33
|
+
db: dbSource,
|
|
34
|
+
tenantId: user.tenantId,
|
|
35
|
+
userId: String(user.id),
|
|
36
|
+
callSiteLabel: "ctx.appendEvent",
|
|
37
|
+
callerFeature,
|
|
38
|
+
}, args);
|
|
39
|
+
}
|
|
40
|
+
// Savepoint-scoped append: catches a losing writer's VersionConflictError
|
|
41
|
+
// without poisoning the rest of the handler's transaction. postgres.js
|
|
42
|
+
// aborts the WHOLE begin() on an uncaught statement error
|
|
43
|
+
// (SQLSTATE 25P02) even if the JS error is caught — runInSavepoint
|
|
44
|
+
// wraps the append in a real SAVEPOINT so only that nested scope rolls
|
|
45
|
+
// back on conflict, and subsequent statements in the outer tx (e.g. a
|
|
46
|
+
// dedup-anchor insert) still succeed. Use when a handler must react to
|
|
47
|
+
// losing a concurrent-append race instead of failing the whole write.
|
|
48
|
+
function buildTryAppendEvent(scope) {
|
|
49
|
+
const { type, user, tx, registry } = scope;
|
|
50
|
+
return async (args) => {
|
|
51
|
+
if (!tx) {
|
|
52
|
+
throw new InternalError({
|
|
53
|
+
message: `ctx.tryAppendEvent("${args.type}") requires an active transaction — no tx is threaded through this call.`,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
const event = await runInSavepoint(tx, (sp) => appendDomainEventCore({
|
|
58
|
+
registry,
|
|
59
|
+
db: sp,
|
|
60
|
+
tenantId: user.tenantId,
|
|
61
|
+
userId: String(user.id),
|
|
62
|
+
callSiteLabel: "ctx.tryAppendEvent",
|
|
63
|
+
callerFeature: registry.getHandlerFeature(type),
|
|
64
|
+
}, args));
|
|
65
|
+
return { ok: true, event };
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
if (e instanceof EventStoreVersionConflictError) {
|
|
69
|
+
return { ok: false, conflict: e };
|
|
70
|
+
}
|
|
71
|
+
if (e instanceof EventStoreIdempotentAppendConflictError) {
|
|
72
|
+
return { ok: false, conflict: e };
|
|
73
|
+
}
|
|
74
|
+
throw e;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function buildFetchForWriting(scope) {
|
|
79
|
+
const { ctx, type, user, tx, registry } = scope;
|
|
80
|
+
return async (args) => {
|
|
81
|
+
const dbSource = requireDbSource(ctx, tx, `ctx.fetchForWriting("${args.aggregateId}")`);
|
|
82
|
+
// Stream-version authoritative (same policy as CRUD executor + Block 0), read together
|
|
83
|
+
// with the events so the pinned version covers exactly what the handler's rules saw.
|
|
84
|
+
const { events: storedEvents, streamVersion: fetchedVersion } = await loadAggregateForWriting(dbSource, args.aggregateId, user.tenantId, { aggregateType: args.aggregateType });
|
|
85
|
+
const events = await upcastStoredEvents(storedEvents, registry.getEventUpcasters(), {
|
|
86
|
+
db: dbSource,
|
|
87
|
+
tenantId: user.tenantId,
|
|
88
|
+
});
|
|
89
|
+
// Optimistic concurrency: if the caller knows the version they
|
|
90
|
+
// worked against (e.g. from a prior read-model row) and the stream
|
|
91
|
+
// has moved on, fail fast before any downstream work.
|
|
92
|
+
if (args.expectedVersion !== undefined && args.expectedVersion !== fetchedVersion) {
|
|
93
|
+
throw new VersionConflictError({
|
|
94
|
+
entityId: args.aggregateId,
|
|
95
|
+
expectedVersion: args.expectedVersion,
|
|
96
|
+
currentVersion: fetchedVersion,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// Pinned into every append: a write landing between fetch and append
|
|
100
|
+
// fails with VersionConflictError instead of stacking on a state the
|
|
101
|
+
// caller's business rules never saw. Bumps per appendOne so consecutive
|
|
102
|
+
// appends stay in order without re-reading the DB.
|
|
103
|
+
let handleVersion = fetchedVersion;
|
|
104
|
+
const appendOne = async (appendArgs) => {
|
|
105
|
+
await appendDomainEvent(ctx, {
|
|
106
|
+
aggregateId: args.aggregateId,
|
|
107
|
+
aggregateType: args.aggregateType,
|
|
108
|
+
type: appendArgs.type,
|
|
109
|
+
payload: appendArgs.payload,
|
|
110
|
+
expectedVersion: handleVersion,
|
|
111
|
+
}, user, tx, registry.getHandlerFeature(type));
|
|
112
|
+
handleVersion += 1;
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
events,
|
|
116
|
+
get version() {
|
|
117
|
+
return handleVersion;
|
|
118
|
+
},
|
|
119
|
+
appendOne,
|
|
120
|
+
hasEvent: (type) => events.some((e) => e.type === type),
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function buildLoadAggregate(scope) {
|
|
125
|
+
const { ctx, user, tx, registry } = scope;
|
|
126
|
+
return async (aggregateId, loadOptions) => {
|
|
127
|
+
const dbSource = requireDbSource(ctx, tx, `ctx.loadAggregate("${aggregateId}")`);
|
|
128
|
+
const events = loadOptions?.asOf
|
|
129
|
+
? await loadAggregateAsOf(dbSource, aggregateId, user.tenantId, loadOptions.asOf)
|
|
130
|
+
: await loadAggregate(dbSource, aggregateId, user.tenantId);
|
|
131
|
+
return upcastStoredEvents(events, registry.getEventUpcasters(), {
|
|
132
|
+
db: dbSource,
|
|
133
|
+
tenantId: user.tenantId,
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
function buildSnapshotAggregate(scope) {
|
|
138
|
+
const { ctx, user, tx, registry } = scope;
|
|
139
|
+
return async (snapshotArgs) => {
|
|
140
|
+
const dbSource = requireDbSource(ctx, tx, `ctx.snapshotAggregate("${snapshotArgs.aggregateId}")`);
|
|
141
|
+
await saveSnapshot(dbSource, {
|
|
142
|
+
aggregateId: snapshotArgs.aggregateId,
|
|
143
|
+
tenantId: user.tenantId,
|
|
144
|
+
aggregateType: snapshotArgs.aggregateType,
|
|
145
|
+
version: snapshotArgs.version,
|
|
146
|
+
state: snapshotArgs.state,
|
|
147
|
+
snapshotVersion: effectiveSnapshotGeneration(snapshotArgs.snapshotVersion ?? 1, registry.getEntity(snapshotArgs.aggregateType)?.eventVersion),
|
|
148
|
+
});
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function buildLoadAggregateWithSnapshot(scope) {
|
|
152
|
+
const { ctx, user, tx, registry } = scope;
|
|
153
|
+
return async (aggregateId, reducer, initial, loadOptions) => {
|
|
154
|
+
const dbSource = requireDbSource(ctx, tx, `ctx.loadAggregateWithSnapshot("${aggregateId}")`);
|
|
155
|
+
// Upcaster-aware: pass an upcastEvent callback so loadAggregateWithSnapshot
|
|
156
|
+
// walks every delta through the registered chain before invoking the
|
|
157
|
+
// user's (sync) reducer. Async upcasters (DB-enrichment) are awaited
|
|
158
|
+
// inside loadAggregateWithSnapshot — feature authors never see legacy
|
|
159
|
+
// payload shapes regardless of which load path they chose.
|
|
160
|
+
const upcasters = registry.getEventUpcasters();
|
|
161
|
+
const upcastCtx = { db: dbSource, tenantId: user.tenantId };
|
|
162
|
+
return loadAggregateWithSnapshot(dbSource, aggregateId, user.tenantId, reducer, initial, {
|
|
163
|
+
...loadOptions,
|
|
164
|
+
entityEventVersionOf: (aggregateType) => registry.getEntity(aggregateType)?.eventVersion,
|
|
165
|
+
upcastEvent: (event) => upcastStoredEvent(event, upcasters, upcastCtx), // @wrapper-known semantic-alias
|
|
166
|
+
});
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
// queryProjection works against both single-stream and multi-stream
|
|
170
|
+
// projections. MSPs without a table cannot be queried — those are
|
|
171
|
+
// side-effect-only consumers (no state to read back).
|
|
172
|
+
function buildHandlerProjectionReader(scope) {
|
|
173
|
+
const { ctx, type, user, tx, registry, hasIdentitySwitchGrant, identitySwitchAudit } = scope;
|
|
174
|
+
const findProjectionTable = (qualifiedName) => registry.getAllProjections().get(qualifiedName)?.table ??
|
|
175
|
+
registry.getAllMultiStreamProjections().get(qualifiedName)?.table;
|
|
176
|
+
const ungatedQueryProjection = async (qualifiedName, queryOptions) => {
|
|
177
|
+
const projTable = findProjectionTable(qualifiedName);
|
|
178
|
+
if (!projTable) {
|
|
179
|
+
const singleNames = [...registry.getAllProjections().keys()];
|
|
180
|
+
const mspNames = [...registry.getAllMultiStreamProjections().keys()].filter((n) => registry.getAllMultiStreamProjections().get(n)?.table);
|
|
181
|
+
const all = [...singleNames, ...mspNames];
|
|
182
|
+
throw new InternalError({
|
|
183
|
+
message: `ctx.queryProjection("${qualifiedName}") — projection not registered, or it is a ` +
|
|
184
|
+
`table-less MSP (side-effect-only). Known queryable projections: ${all.join(", ") || "(none)"}`,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const dbSource = requireDbSource(ctx, tx, `ctx.queryProjection("${qualifiedName}")`);
|
|
188
|
+
const where = hasTenantColumn(projTable) && queryOptions?.unsafeAllTenants !== true
|
|
189
|
+
? { tenantId: user.tenantId }
|
|
190
|
+
: undefined;
|
|
191
|
+
const rows = await selectMany(dbSource, projTable, where);
|
|
192
|
+
return rows; // @cast-boundary engine-payload
|
|
193
|
+
};
|
|
194
|
+
return createGatedProjectionReader(`handler "${type}"`, hasIdentitySwitchGrant, ungatedQueryProjection, identitySwitchAudit, (qualifiedName) => {
|
|
195
|
+
const projTable = findProjectionTable(qualifiedName);
|
|
196
|
+
// Unknown projection: keep the grant check first, the ungated reader reports the real error.
|
|
197
|
+
return projTable === undefined || hasTenantColumn(projTable);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
export function buildEventStoreFacet(scope) {
|
|
201
|
+
const { ctx, type, user, tx, registry } = scope;
|
|
202
|
+
return {
|
|
203
|
+
// Strict + unsafe share the same runtime — only the type-surface
|
|
204
|
+
// differs. The strict signature is what's exposed to typed callers;
|
|
205
|
+
// unsafe is the explicit escape-hatch for runtime-pluggable events.
|
|
206
|
+
appendEvent: (async (args) => {
|
|
207
|
+
await appendDomainEvent(ctx, args, user, tx, registry.getHandlerFeature(type));
|
|
208
|
+
}), // @cast-boundary engine-bridge
|
|
209
|
+
unsafeAppendEvent: async (args) => {
|
|
210
|
+
await appendDomainEvent(ctx, args, user, tx, registry.getHandlerFeature(type));
|
|
211
|
+
},
|
|
212
|
+
tryAppendEvent: buildTryAppendEvent(scope),
|
|
213
|
+
fetchForWriting: buildFetchForWriting(scope),
|
|
214
|
+
loadAggregate: buildLoadAggregate(scope),
|
|
215
|
+
archiveStream: async (aggregateId, archiveArgs) => {
|
|
216
|
+
const dbSource = requireDbSource(ctx, tx, `ctx.archiveStream("${aggregateId}")`);
|
|
217
|
+
await archiveStreamHelper(dbSource, {
|
|
218
|
+
tenantId: user.tenantId,
|
|
219
|
+
aggregateId,
|
|
220
|
+
aggregateType: archiveArgs.aggregateType,
|
|
221
|
+
archivedBy: user.id,
|
|
222
|
+
reason: archiveArgs.reason,
|
|
223
|
+
});
|
|
224
|
+
},
|
|
225
|
+
restoreStream: async (aggregateId) => {
|
|
226
|
+
const dbSource = requireDbSource(ctx, tx, `ctx.restoreStream("${aggregateId}")`);
|
|
227
|
+
await restoreStreamHelper(dbSource, user.tenantId, aggregateId);
|
|
228
|
+
},
|
|
229
|
+
isStreamArchived: async (aggregateId) => {
|
|
230
|
+
const dbSource = requireDbSource(ctx, tx, `ctx.isStreamArchived("${aggregateId}")`);
|
|
231
|
+
return isStreamArchived(dbSource, user.tenantId, aggregateId);
|
|
232
|
+
},
|
|
233
|
+
snapshotAggregate: buildSnapshotAggregate(scope),
|
|
234
|
+
loadAggregateWithSnapshot: buildLoadAggregateWithSnapshot(scope),
|
|
235
|
+
queryProjection: buildHandlerProjectionReader(scope),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DbConnection } from "../db/connection.ts";
|
|
2
|
+
import type { TenantId } from "../engine/types/identifiers.ts";
|
|
3
|
+
import type { NotifyFn, SessionUser } from "../engine/types/index.ts";
|
|
4
|
+
import type { CommitSinks } from "./handler-context-dispatch.ts";
|
|
5
|
+
import type { HandlerContextScope } from "./handler-context-scope.ts";
|
|
6
|
+
export declare function buildNotifyFacet(scope: HandlerContextScope, sinks: CommitSinks, outsideTxSource: DbConnection | undefined): {
|
|
7
|
+
notify: NotifyFn | undefined;
|
|
8
|
+
factoryFragment: {
|
|
9
|
+
_notifyInTransactionFactory?: ((notifyUser: SessionUser, notifyTenantId: TenantId) => NotifyFn) | undefined;
|
|
10
|
+
_notifyAfterCommitFactory?: ((notifyUser: SessionUser, notifyTenantId: TenantId) => NotifyFn) | undefined;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { createDeferringNotify } from "./notify-scope.js";
|
|
2
|
+
// DispatchContext.jobRunner is typed as the handleEvent-only JobRunnerRef but
|
|
3
|
+
// holds the concrete runner at boot; narrow instead of casting.
|
|
4
|
+
function isNotifyJobDispatcher(runner) {
|
|
5
|
+
return (typeof runner === "object" &&
|
|
6
|
+
runner !== null &&
|
|
7
|
+
"dispatch" in runner &&
|
|
8
|
+
typeof runner.dispatch === "function");
|
|
9
|
+
}
|
|
10
|
+
function buildNotify(scope, sinks, notifyAfterCommitFactory, notifyInTransactionFactory) {
|
|
11
|
+
const { user, registry } = scope;
|
|
12
|
+
if (!notifyAfterCommitFactory)
|
|
13
|
+
return undefined;
|
|
14
|
+
const notifyAfterCommit = notifyAfterCommitFactory(user, user.tenantId);
|
|
15
|
+
if (!notifyInTransactionFactory)
|
|
16
|
+
return notifyAfterCommit;
|
|
17
|
+
const notifyInTransaction = notifyInTransactionFactory(user, user.tenantId);
|
|
18
|
+
// A rolled-back write must leave no notification behind. Without any commit to wait for (a
|
|
19
|
+
// post-commit context) delivery stays immediate.
|
|
20
|
+
const { notifyCommitSink } = sinks;
|
|
21
|
+
if (notifyCommitSink === undefined)
|
|
22
|
+
return notifyInTransaction;
|
|
23
|
+
return createDeferringNotify({
|
|
24
|
+
registry,
|
|
25
|
+
tenantId: user.tenantId,
|
|
26
|
+
notifyAfterCommit,
|
|
27
|
+
notifyNow: notifyInTransaction,
|
|
28
|
+
scheduleAfterCommit: (hook) => {
|
|
29
|
+
notifyCommitSink.push(hook);
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export function buildNotifyFacet(scope, sinks, outsideTxSource) {
|
|
34
|
+
const { ctx, tx, context } = scope;
|
|
35
|
+
const notifyFactory = context._notifyFactory;
|
|
36
|
+
const notifyJobDispatcher = isNotifyJobDispatcher(ctx.jobRunner) ? ctx.jobRunner : undefined;
|
|
37
|
+
// Inside the write tx the app pool's slots can all be held by waiting write txs, so the
|
|
38
|
+
// delivery's own DB work (broadcast lookup, attempt log, channel sends) runs on the outside-tx pool.
|
|
39
|
+
const notifyInTransactionFactory = notifyFactory && tx !== undefined
|
|
40
|
+
? (notifyUser, notifyTenantId) => notifyFactory(notifyUser, notifyTenantId, notifyJobDispatcher, outsideTxSource, context._fileProviderResolver)
|
|
41
|
+
: undefined;
|
|
42
|
+
const notifyAfterCommitFactory = notifyFactory
|
|
43
|
+
? (notifyUser, notifyTenantId) => notifyFactory(notifyUser, notifyTenantId, notifyJobDispatcher, undefined, context._fileProviderResolver)
|
|
44
|
+
: undefined;
|
|
45
|
+
const notify = buildNotify(scope, sinks, notifyAfterCommitFactory, notifyInTransactionFactory);
|
|
46
|
+
return {
|
|
47
|
+
notify,
|
|
48
|
+
factoryFragment: {
|
|
49
|
+
...(notifyInTransactionFactory && {
|
|
50
|
+
_notifyInTransactionFactory: notifyInTransactionFactory,
|
|
51
|
+
}),
|
|
52
|
+
...(notifyAfterCommitFactory && { _notifyAfterCommitFactory: notifyAfterCommitFactory }),
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { HandlerContextScope } from "./handler-context-scope.ts";
|
|
2
|
+
export declare function buildObservabilityFacet(scope: HandlerContextScope): {
|
|
3
|
+
log: import("@cosmicdrift/kumiko-types/logger").Logger | undefined;
|
|
4
|
+
tracer: import("@cosmicdrift/kumiko-types/observability/span").Tracer;
|
|
5
|
+
metrics: import("@cosmicdrift/kumiko-types/observability/metric").MetricsHandle;
|
|
6
|
+
metricsFor: (featureName: string) => import("@cosmicdrift/kumiko-types/observability/metric").MetricsHandle;
|
|
7
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createMetricsForFactory, createMetricsHandle, createNoopMetricsHandle, getFallbackTracer, } from "../observability/index.js";
|
|
2
|
+
export function buildObservabilityFacet(scope) {
|
|
3
|
+
const { type, user, context, reqCtx, featureName } = scope;
|
|
4
|
+
const log = context.log?.child({
|
|
5
|
+
handler: type,
|
|
6
|
+
tenantId: user.tenantId,
|
|
7
|
+
userId: user.id,
|
|
8
|
+
...(reqCtx && { requestId: reqCtx.requestId }),
|
|
9
|
+
});
|
|
10
|
+
// Observability — feature-bound metrics handle, so ctx.metrics.inc("foo")
|
|
11
|
+
// resolves to kumiko_<feature>_foo. Unknown feature falls back to noop
|
|
12
|
+
// so legacy internal handlers don't crash.
|
|
13
|
+
const tracer = context.tracer ?? getFallbackTracer();
|
|
14
|
+
const meter = context.meter;
|
|
15
|
+
const metrics = meter && featureName ? createMetricsHandle(meter, featureName) : createNoopMetricsHandle();
|
|
16
|
+
// ctx.metricsFor(featureName) — shared/library code binds to a feature
|
|
17
|
+
// name of its own choosing instead of the dispatching handler's.
|
|
18
|
+
// Unregistered names no-op rather than throw.
|
|
19
|
+
const metricsFor = createMetricsForFactory(meter);
|
|
20
|
+
return { log, tracer, metrics, metricsFor };
|
|
21
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { type EscapeHatchDeclaration, type EscapeHatchReporter } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
|
+
import { type RequestContextData } from "../api/request-context.ts";
|
|
3
|
+
import type { DbConnection, DbTx } from "../db/connection.ts";
|
|
4
|
+
import type { AppContext, Registry, SessionUser } from "../engine/types/index.ts";
|
|
5
|
+
import { type DispatchContext, type HandlerTransactionScope } from "./dispatch-shared.ts";
|
|
6
|
+
import type { WriteOrigin } from "./write-origin.ts";
|
|
7
|
+
export type HandlerContextScope = {
|
|
8
|
+
readonly ctx: DispatchContext;
|
|
9
|
+
readonly type: string;
|
|
10
|
+
readonly user: SessionUser;
|
|
11
|
+
readonly origin: WriteOrigin;
|
|
12
|
+
readonly tx: DbTx | undefined;
|
|
13
|
+
readonly transactionScope: HandlerTransactionScope;
|
|
14
|
+
readonly registry: Registry;
|
|
15
|
+
readonly context: AppContext;
|
|
16
|
+
readonly isSystem: boolean;
|
|
17
|
+
readonly dbSource: DbConnection | DbTx | undefined;
|
|
18
|
+
readonly reqCtx: RequestContextData | undefined;
|
|
19
|
+
readonly writeEscapeHatch: EscapeHatchDeclaration | undefined;
|
|
20
|
+
readonly handlerEscapeHatch: EscapeHatchDeclaration | undefined;
|
|
21
|
+
readonly isWriteHandler: boolean;
|
|
22
|
+
readonly isPerCommand: boolean;
|
|
23
|
+
readonly denyDbWrites: boolean;
|
|
24
|
+
readonly featureName: string | undefined;
|
|
25
|
+
readonly hasIdentitySwitchGrant: boolean;
|
|
26
|
+
readonly reportEscapeHatch: EscapeHatchReporter;
|
|
27
|
+
readonly identitySwitchAudit: {
|
|
28
|
+
readonly reason: string | undefined;
|
|
29
|
+
readonly report: EscapeHatchReporter;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
export declare function resolveHandlerContextScope(input: {
|
|
33
|
+
readonly ctx: DispatchContext;
|
|
34
|
+
readonly type: string;
|
|
35
|
+
readonly user: SessionUser;
|
|
36
|
+
readonly origin: WriteOrigin;
|
|
37
|
+
readonly tx: DbTx | undefined;
|
|
38
|
+
readonly transactionScope: HandlerTransactionScope;
|
|
39
|
+
}): HandlerContextScope;
|
|
40
|
+
export declare function buildPreSaveFragment(scope: HandlerContextScope): {
|
|
41
|
+
runPreSave?: ((handlerName: string, changes: Record<string, unknown>, previous: Readonly<Record<string, unknown>>, isNew: boolean) => Promise<Record<string, unknown>>) | undefined;
|
|
42
|
+
};
|
|
43
|
+
export declare function buildRuntimeFragment(scope: HandlerContextScope): {
|
|
44
|
+
signal?: AbortSignal | undefined;
|
|
45
|
+
effectiveFeatures?: import("@cosmicdrift/kumiko-framework/engine").EffectiveFeaturesResolver | undefined;
|
|
46
|
+
cacheSync?: import("@cosmicdrift/kumiko-types/cache-sync-types").CacheSyncBus | undefined;
|
|
47
|
+
jobRunner?: import("@cosmicdrift/kumiko-types/handlers").JobRunnerRef | undefined;
|
|
48
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { escapeHatchFor, } from "@cosmicdrift/kumiko-types/handlers";
|
|
2
|
+
import { requestContext } from "../api/request-context.js";
|
|
3
|
+
import { createFallbackLogger } from "../logging/utils.js";
|
|
4
|
+
import { createEscapeHatchReporter } from "../observability/index.js";
|
|
5
|
+
import { resolveDbSource, } from "./dispatch-shared.js";
|
|
6
|
+
export function resolveHandlerContextScope(input) {
|
|
7
|
+
const { ctx, type, user, origin, tx, transactionScope } = input;
|
|
8
|
+
const { registry, appContext: context } = ctx;
|
|
9
|
+
const isSystem = registry.isHandlerSystemScoped(type);
|
|
10
|
+
// The outer dispatcher receives a DbConnection from the server/stack;
|
|
11
|
+
// AppContext's `db` union also allows TenantDb (for downstream hook calls),
|
|
12
|
+
// but at this point we're the root of the pipeline — cast is safe.
|
|
13
|
+
const dbSource = resolveDbSource(ctx, tx);
|
|
14
|
+
const reqCtx = requestContext.get();
|
|
15
|
+
// global() writes are write-handler-only; SYSTEM identity switch and unsafeRaw accept write, query or stream escapeHatch.
|
|
16
|
+
const writeHandler = registry.getWriteHandler(type);
|
|
17
|
+
const writeEscapeHatch = writeHandler?.escapeHatch;
|
|
18
|
+
const isPerCommand = writeHandler?.transaction === "perCommand";
|
|
19
|
+
const denyDbWrites = transactionScope === "withoutTransaction";
|
|
20
|
+
const handlerEscapeHatch = writeEscapeHatch ??
|
|
21
|
+
registry.getQueryHandler(type)?.escapeHatch ??
|
|
22
|
+
registry.getStreamHandler(type)?.escapeHatch;
|
|
23
|
+
const identitySwitchEscapeHatch = escapeHatchFor(handlerEscapeHatch, "systemIdentity");
|
|
24
|
+
const hasIdentitySwitchGrant = isSystem || identitySwitchEscapeHatch !== undefined;
|
|
25
|
+
const featureName = registry.getHandlerFeature(type);
|
|
26
|
+
const reportEscapeHatch = createEscapeHatchReporter({
|
|
27
|
+
handler: type,
|
|
28
|
+
tenantId: user.tenantId,
|
|
29
|
+
actor: user.id,
|
|
30
|
+
sink: context._escapeHatchAuditSink,
|
|
31
|
+
log: context.log ?? createFallbackLogger("dispatcher"),
|
|
32
|
+
window: ctx.escapeHatchReportWindow,
|
|
33
|
+
meter: context.meter,
|
|
34
|
+
});
|
|
35
|
+
const identitySwitchGrantReason = identitySwitchEscapeHatch?.reason ??
|
|
36
|
+
(isSystem ? `r.systemScope() feature "${featureName ?? "unknown"}"` : undefined);
|
|
37
|
+
return {
|
|
38
|
+
ctx,
|
|
39
|
+
type,
|
|
40
|
+
user,
|
|
41
|
+
origin,
|
|
42
|
+
tx,
|
|
43
|
+
transactionScope,
|
|
44
|
+
registry,
|
|
45
|
+
context,
|
|
46
|
+
isSystem,
|
|
47
|
+
dbSource,
|
|
48
|
+
reqCtx,
|
|
49
|
+
writeEscapeHatch,
|
|
50
|
+
handlerEscapeHatch,
|
|
51
|
+
isWriteHandler: writeHandler !== undefined,
|
|
52
|
+
isPerCommand,
|
|
53
|
+
denyDbWrites,
|
|
54
|
+
featureName,
|
|
55
|
+
hasIdentitySwitchGrant,
|
|
56
|
+
reportEscapeHatch,
|
|
57
|
+
identitySwitchAudit: { reason: identitySwitchGrantReason, report: reportEscapeHatch },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
// preSave hooks need `changes`/`previous`/`isNew`, which only exist once
|
|
61
|
+
// a handler actually starts building its write — bound here so entity
|
|
62
|
+
// CRUD handlers (entity-handlers.ts) can forward it to the executor.
|
|
63
|
+
export function buildPreSaveFragment(scope) {
|
|
64
|
+
const { lifecycle } = scope.ctx;
|
|
65
|
+
return {
|
|
66
|
+
...(lifecycle && {
|
|
67
|
+
runPreSave: (handlerName, changes, previous, isNew) => lifecycle.runPreSave(handlerName, changes, previous, isNew, scope.context),
|
|
68
|
+
}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function buildRuntimeFragment(scope) {
|
|
72
|
+
const { ctx, reqCtx } = scope;
|
|
73
|
+
const { effectiveFeatures, jobRunner } = ctx;
|
|
74
|
+
return {
|
|
75
|
+
// Cancellation signal flows from the HTTP middleware via
|
|
76
|
+
// requestContext. Conditional spread so non-HTTP entry-points
|
|
77
|
+
// (jobs, dispatcher MSP-applies) don't get a phantom signal that
|
|
78
|
+
// would always read aborted=false but feel meaningful.
|
|
79
|
+
...(reqCtx?.signal ? { signal: reqCtx.signal } : {}),
|
|
80
|
+
// Propagate the feature-toggle resolver so the lifecycle pipeline,
|
|
81
|
+
// MSP runner, and ctx.hasFeature all pull from the same source.
|
|
82
|
+
...(effectiveFeatures && { effectiveFeatures }),
|
|
83
|
+
...(ctx.cacheSync && { cacheSync: ctx.cacheSync }),
|
|
84
|
+
// Lets write handlers call ctx.jobRunner.dispatch(...) directly, same
|
|
85
|
+
// as a follow-up job would (test-stack.ts wires the matching runner).
|
|
86
|
+
...(jobRunner && { jobRunner }),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
@@ -46,7 +46,7 @@ export function assertIrreversibleOperationAllowed(operation) {
|
|
|
46
46
|
// Fields flagged readAsInstruction: true have their write payload presence checked here — the
|
|
47
47
|
// same reasoning as assertIrreversibleOperationAllowed, but the irreversible property belongs to
|
|
48
48
|
// the target field, not the verb. Presence in the payload (not a value comparison against the
|
|
49
|
-
// loaded row) is the deliberate fail-closed choice
|
|
49
|
+
// loaded row) is the deliberate fail-closed choice.
|
|
50
50
|
export function assertInstructionFieldWriteAllowed(entityName, verb, writtenFieldNames, instructionFieldNames) {
|
|
51
51
|
const fields = writtenFieldNames.filter((name) => instructionFieldNames.includes(name));
|
|
52
52
|
// skip: the write touches no readAsInstruction field.
|
|
@@ -5,17 +5,17 @@ function resolveTracer(context) {
|
|
|
5
5
|
return context.tracer ?? getFallbackTracer();
|
|
6
6
|
}
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Resolves the effective feature set for the active context's tenant.
|
|
9
9
|
*
|
|
10
|
-
* `_tenantId`
|
|
11
|
-
*
|
|
12
|
-
* pipeline)
|
|
13
|
-
*
|
|
14
|
-
*
|
|
10
|
+
* `_tenantId` is copied from `user.tenantId` when the dispatcher builds the
|
|
11
|
+
* HandlerContext. Legacy call sites without a user (system jobs, boot-time
|
|
12
|
+
* pipeline) leave it undefined; `effectiveFeatures(undefined)` is type-unsafe,
|
|
13
|
+
* so this returns undefined and treats that as "all features on" (registry
|
|
14
|
+
* filter hooks accept undefined to skip filtering).
|
|
15
15
|
*
|
|
16
|
-
*
|
|
17
|
-
* preDelete, postDelete)
|
|
18
|
-
*
|
|
16
|
+
* This is the single path used by all four lifecycle-hook call sites
|
|
17
|
+
* (preSave, postSave, preDelete, postDelete), keeping tenant scoping
|
|
18
|
+
* consistent.
|
|
19
19
|
*/
|
|
20
20
|
function currentEffectiveFeatures(context) {
|
|
21
21
|
if (context._tenantId === undefined)
|