@cosmicdrift/kumiko-framework 0.387.0 → 0.389.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 +9 -1
- 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/golden-path.md +13 -1
- package/agents/examples/legal-pages.md +1 -1
- package/agents/examples/lifecycle-hooks.md +55 -2
- package/agents/examples/list-table-options.md +466 -0
- package/agents/examples/pdf-audit-export.md +4 -2
- 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/state-machine.md +3 -2
- package/agents/examples/tenant-isolation.md +73 -1
- package/agents/examples/wizard-form.md +941 -0
- package/agents/examples/writeform-section.md +548 -0
- package/agents/examples.md +1 -0
- package/agents/features.md +19 -2
- package/agents/widgets.md +2 -0
- package/dist/api/api-constants.d.ts +1 -0
- package/dist/api/api-constants.js +2 -0
- package/dist/api/auth-routes-shared.d.ts +2 -0
- package/dist/api/auth-routes-shared.js +5 -0
- package/dist/api/client-error-route.js +10 -5
- package/dist/api/entity-read-access.d.ts +2 -1
- package/dist/api/entity-read-access.js +14 -0
- package/dist/api/export/export-cells-reference.d.ts +9 -0
- package/dist/api/export/export-cells-reference.js +21 -0
- package/dist/api/export/export-cells.d.ts +11 -0
- package/dist/api/export/export-cells.js +136 -0
- package/dist/api/export/export-ciphertext-guard.d.ts +2 -0
- package/dist/api/export/export-ciphertext-guard.js +27 -0
- package/dist/api/export/export-columns.d.ts +29 -0
- package/dist/api/export/export-columns.js +85 -0
- package/dist/api/export/export-errors.d.ts +4 -0
- package/dist/api/export/export-errors.js +14 -0
- package/dist/api/export/export-route.d.ts +4 -0
- package/dist/api/export/export-route.js +74 -0
- package/dist/api/export/export-rows.d.ts +13 -0
- package/dist/api/export/export-rows.js +62 -0
- package/dist/api/export/export-screen.d.ts +12 -0
- package/dist/api/export/export-screen.js +47 -0
- package/dist/api/export/export-xlsx-writer.d.ts +4 -0
- package/dist/api/export/export-xlsx-writer.js +21 -0
- package/dist/api/extra-route.d.ts +4 -0
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +3 -1
- package/dist/api/pii-leak-guard.d.ts +3 -0
- package/dist/api/pii-leak-guard.js +3 -2
- package/dist/api/request-context.d.ts +1 -1
- package/dist/api/request-context.js +1 -1
- package/dist/api/routes.d.ts +16 -0
- 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 +206 -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 +299 -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 +238 -0
- package/dist/api/server.d.ts +10 -15
- package/dist/api/server.js +48 -1251
- package/dist/api/session-cookies.js +4 -4
- package/dist/api/sse-route.d.ts +2 -1
- package/dist/api/sse-route.js +27 -13
- package/dist/config/index.d.ts +3 -1
- 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.d.ts +1 -0
- package/dist/crypto/pg-kms-adapter.js +12 -18
- package/dist/crypto/pii-field-encryption.d.ts +5 -1
- package/dist/crypto/pii-field-encryption.js +3 -1
- package/dist/db/aggregate-sql.d.ts +23 -0
- package/dist/db/aggregate-sql.js +165 -0
- package/dist/db/closed-connection.d.ts +9 -0
- package/dist/db/closed-connection.js +88 -0
- package/dist/db/connection.d.ts +4 -5
- package/dist/db/connection.js +29 -20
- package/dist/db/dialect.js +1 -1
- 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 +101 -0
- 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.js +103 -110
- package/dist/db/event-store-executor-context.d.ts +2 -1
- package/dist/db/event-store-executor-read-aggregate-currencies.d.ts +17 -0
- package/dist/db/event-store-executor-read-aggregate-currencies.js +40 -0
- package/dist/db/event-store-executor-read-aggregate.d.ts +8 -0
- package/dist/db/event-store-executor-read-aggregate.js +143 -0
- package/dist/db/event-store-executor-read-filter.d.ts +31 -0
- package/dist/db/event-store-executor-read-filter.js +206 -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 +48 -701
- package/dist/db/event-store-executor-write-atomic.d.ts +4 -0
- package/dist/db/event-store-executor-write-atomic.js +41 -0
- package/dist/db/event-store-executor-write-steps.d.ts +17 -0
- package/dist/db/event-store-executor-write-steps.js +95 -0
- package/dist/db/event-store-executor-write.js +405 -509
- package/dist/db/event-store-executor.js +2 -1
- package/dist/db/executor-managed-tables.js +1 -1
- package/dist/db/index.d.ts +7 -3
- package/dist/db/index.js +6 -2
- package/dist/db/list-aggregate-labels.d.ts +14 -0
- package/dist/db/list-aggregate-labels.js +26 -0
- package/dist/db/migrate-generator.js +71 -86
- package/dist/db/migrate-runner.js +7 -4
- package/dist/db/pg-error.js +26 -21
- package/dist/db/queries/backfill-pii.js +3 -6
- package/dist/db/queries/database-now.js +2 -1
- package/dist/db/queries/shadow-swap.js +2 -3
- package/dist/db/query.d.ts +16 -15
- package/dist/db/query.js +76 -804
- package/dist/db/reference-data.js +19 -2
- package/dist/db/statement-timeout.js +2 -1
- package/dist/db/table-builder.js +113 -166
- 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-upsert.d.ts +5 -0
- package/dist/db/tenant-db-upsert.js +91 -0
- package/dist/db/tenant-db.js +50 -10
- package/dist/db/where-clause.d.ts +6 -0
- package/dist/db/where-clause.js +236 -0
- package/dist/derivatives/delete-stored-file.js +2 -1
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/bundled-lists-table.d.ts +2 -0
- package/dist/engine/boot-validator/bundled-lists-table.js +36 -0
- package/dist/engine/boot-validator/detail-screens.js +2 -2
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +17 -0
- package/dist/engine/boot-validator/entity-list-aggregate-options.d.ts +3 -0
- package/dist/engine/boot-validator/entity-list-aggregate-options.js +23 -0
- package/dist/engine/boot-validator/form-derivations.js +35 -1
- package/dist/engine/boot-validator/i18n-keys.js +5 -3
- package/dist/engine/boot-validator/index.js +2 -0
- package/dist/engine/boot-validator/list-bulk-actions.d.ts +3 -0
- package/dist/engine/boot-validator/list-bulk-actions.js +49 -0
- package/dist/engine/boot-validator/list-column-layout.d.ts +8 -0
- package/dist/engine/boot-validator/list-column-layout.js +41 -0
- package/dist/engine/boot-validator/list-table-aggregate-options.d.ts +24 -0
- package/dist/engine/boot-validator/list-table-aggregate-options.js +145 -0
- package/dist/engine/boot-validator/list-table-aggregate-query.d.ts +10 -0
- package/dist/engine/boot-validator/list-table-aggregate-query.js +31 -0
- package/dist/engine/boot-validator/list-table-export-options.d.ts +1 -0
- package/dist/engine/boot-validator/list-table-export-options.js +27 -0
- package/dist/engine/boot-validator/list-table-options.d.ts +3 -0
- package/dist/engine/boot-validator/list-table-options.js +77 -0
- package/dist/engine/boot-validator/list-table-reject-unknown-keys.d.ts +1 -0
- package/dist/engine/boot-validator/list-table-reject-unknown-keys.js +7 -0
- package/dist/engine/boot-validator/pii-retention.js +233 -155
- package/dist/engine/boot-validator/projection-list-screens.d.ts +1 -1
- package/dist/engine/boot-validator/projection-list-screens.js +3 -13
- package/dist/engine/boot-validator/related-list-table-rules.d.ts +4 -0
- package/dist/engine/boot-validator/related-list-table-rules.js +42 -0
- package/dist/engine/boot-validator/screens-actions.d.ts +1 -0
- package/dist/engine/boot-validator/screens-actions.js +8 -1
- package/dist/engine/boot-validator/screens-dashboard.js +2 -5
- package/dist/engine/boot-validator/screens-forms.js +2 -6
- 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 +235 -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 +56 -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 +24 -11
- package/dist/engine/boot-validator/screens.d.ts +0 -2
- package/dist/engine/boot-validator/screens.js +99 -860
- package/dist/engine/build-app-schema.d.ts +2 -1
- package/dist/engine/build-app-schema.js +65 -97
- package/dist/engine/build-config-feature-schema.js +6 -1
- package/dist/engine/define-feature.js +13 -2
- package/dist/engine/define-handler.js +1 -0
- package/dist/engine/entity-handlers.d.ts +13 -0
- package/dist/engine/entity-handlers.js +10 -21
- package/dist/engine/entity-list-runtime-options.d.ts +8 -0
- package/dist/engine/entity-list-runtime-options.js +18 -0
- package/dist/engine/feature-ast/extractors/handlers.js +1 -0
- package/dist/engine/feature-ast/extractors/index.d.ts +1 -1
- package/dist/engine/feature-ast/extractors/index.js +1 -1
- package/dist/engine/feature-ast/extractors/round1.d.ts +2 -1
- package/dist/engine/feature-ast/extractors/round1.js +6 -0
- package/dist/engine/feature-ast/extractors/round2.js +25 -6
- package/dist/engine/feature-ast/index.d.ts +1 -1
- package/dist/engine/feature-ast/parse.js +3 -1
- package/dist/engine/feature-ast/patch.d.ts +2 -0
- package/dist/engine/feature-ast/patch.js +5 -182
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +10 -245
- package/dist/engine/feature-ast/pattern-change-schema.js +10 -163
- package/dist/engine/feature-ast/pattern-id-matchers.d.ts +7 -0
- package/dist/engine/feature-ast/pattern-id-matchers.js +211 -0
- package/dist/engine/feature-ast/pattern-id-schema.d.ts +251 -0
- package/dist/engine/feature-ast/pattern-id-schema.js +166 -0
- package/dist/engine/feature-ast/patterns.d.ts +14 -1
- package/dist/engine/feature-ast/patterns.js +44 -51
- package/dist/engine/feature-ast/render.js +52 -87
- package/dist/engine/feature-builder-state.d.ts +1 -0
- package/dist/engine/feature-builder-state.js +1 -0
- package/dist/engine/feature-entity-handlers.js +3 -0
- package/dist/engine/feature-manifest.d.ts +1 -0
- package/dist/engine/feature-manifest.js +1 -0
- package/dist/engine/field-access.d.ts +1 -0
- package/dist/engine/field-access.js +12 -0
- package/dist/engine/form-derivations.js +18 -7
- package/dist/engine/handler-field-references.js +1 -1
- package/dist/engine/index.d.ts +2 -2
- package/dist/engine/index.js +1 -1
- package/dist/engine/pattern-library/library.js +2 -1
- package/dist/engine/pattern-library/static-schemas.d.ts +1 -0
- package/dist/engine/pattern-library/static-schemas.js +9 -0
- package/dist/engine/project-app-schema-for-roles.d.ts +2 -0
- package/dist/engine/project-app-schema-for-roles.js +6 -4
- package/dist/engine/registry-ingest.js +1 -1
- package/dist/engine/schema-builder.js +228 -212
- package/dist/engine/screen-filter-ops.js +2 -0
- package/dist/engine/screen-helpers.js +1 -1
- package/dist/engine/screen-kind.d.ts +7 -0
- package/dist/engine/screen-kind.js +1 -0
- package/dist/engine/strip-disabled-features-from-app-schema.js +58 -17
- package/dist/engine/types/feature.d.ts +1 -0
- package/dist/engine/types/index.d.ts +4 -3
- package/dist/engine/types/screen.d.ts +1 -1
- package/dist/engine/types/screen.js +1 -1
- package/dist/engine/validate-projection-allowlist.js +2 -1
- package/dist/entrypoint/index.d.ts +6 -1
- package/dist/entrypoint/index.js +10 -1
- package/dist/errors/docs-url.d.ts +1 -1
- package/dist/errors/docs-url.js +22 -2
- package/dist/es-ops/context.js +1 -1
- package/dist/event-store/admin-api.js +1 -1
- package/dist/event-store/event-store.d.ts +1 -0
- package/dist/event-store/event-store.js +17 -8
- package/dist/event-store/provenance-append.js +1 -1
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/create-file-ref-from-bytes.js +7 -1
- package/dist/files/create-file-ref.d.ts +3 -0
- package/dist/files/create-file-ref.js +11 -2
- package/dist/files/file-handle.js +2 -3
- package/dist/files/file-routes.d.ts +2 -0
- package/dist/files/file-routes.js +40 -10
- package/dist/files/provider-resolver.d.ts +1 -0
- package/dist/files/provider-resolver.js +1 -1
- package/dist/files/record-write-check.d.ts +6 -0
- package/dist/files/record-write-check.js +13 -0
- package/dist/files/resolve-content-type.js +43 -0
- package/dist/files/upload-ledger.d.ts +2 -2
- package/dist/files/upload-ledger.js +11 -4
- package/dist/i18n/required-surface-keys.js +189 -208
- package/dist/internal/stack-support.d.ts +2 -1
- package/dist/internal/stack-support.js +1 -1
- package/dist/internal/testing.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 +61 -2
- 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 +50 -657
- package/dist/observability/escape-hatch-report.d.ts +3 -0
- package/dist/observability/escape-hatch-report.js +21 -3
- package/dist/pipeline/after-rollback-sink.d.ts +6 -0
- package/dist/pipeline/after-rollback-sink.js +32 -0
- package/dist/pipeline/cascade-handler.js +165 -138
- package/dist/pipeline/dispatch-batch.js +8 -1
- package/dist/pipeline/dispatch-query.js +10 -3
- package/dist/pipeline/dispatch-shared.d.ts +13 -3
- package/dist/pipeline/dispatch-shared.js +62 -710
- package/dist/pipeline/dispatch-write.js +296 -189
- package/dist/pipeline/event-dispatcher-admin.js +2 -1
- package/dist/pipeline/event-dispatcher-delivery.js +6 -6
- package/dist/pipeline/event-dispatcher.d.ts +1 -0
- package/dist/pipeline/event-dispatcher.js +6 -9
- 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 +23 -0
- package/dist/pipeline/handler-context-dispatch.js +101 -0
- package/dist/pipeline/handler-context-events.d.ts +41 -0
- package/dist/pipeline/handler-context-events.js +239 -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/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 +2 -1
- package/dist/pipeline/system-hooks.d.ts +3 -1
- package/dist/pipeline/system-hooks.js +55 -6
- package/dist/pipeline/write-result-masking.js +13 -10
- 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 -604
- package/dist/search/meilisearch-adapter.js +2 -1
- package/dist/search/reindex-entity.js +15 -5
- package/dist/ui-types/index.d.ts +2 -2
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-changelog.d.ts +2 -0
- package/dist/upgrade-changelog.js +14 -0
- package/dist/upgrade-cli.d.ts +3 -12
- package/dist/upgrade-cli.js +171 -197
- package/dist/upgrade-marker-parse.d.ts +32 -0
- package/dist/upgrade-marker-parse.js +73 -0
- package/package.json +9 -6
- package/src/changes.json +555 -0
- package/src/scripts/codemod/codemod-support.ts +1 -1
- package/src/scripts/codemod/db-factory.ts +1 -1
- package/src/scripts/codemod/feature-layout.ts +10 -2
- 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 +175 -145
- package/src/scripts/codemod/testing-injection.ts +122 -42
- package/src/scripts/codemod/testing-move.ts +29 -7
- package/dist/engine/provider-plugin-context.d.ts +0 -3
- package/dist/engine/provider-plugin-context.js +0 -28
|
@@ -12,6 +12,9 @@ export declare function createEscapeHatchReportWindow(opts?: {
|
|
|
12
12
|
export type EscapeHatchProcessDedup = {
|
|
13
13
|
readonly kinds: ReadonlySet<EscapeHatchKind>;
|
|
14
14
|
readonly lastAuditedAt: Map<string, number>;
|
|
15
|
+
readonly lastSweep: {
|
|
16
|
+
atMs: number;
|
|
17
|
+
};
|
|
15
18
|
};
|
|
16
19
|
export declare function createEscapeHatchProcessDedup(kinds: readonly EscapeHatchKind[]): EscapeHatchProcessDedup;
|
|
17
20
|
export declare function withEscapeHatchAuditScope<T>(run: () => Promise<T>): Promise<T>;
|
|
@@ -40,8 +40,24 @@ const READ_WINDOW_KINDS = new Set([
|
|
|
40
40
|
// A long-lived worker must leave an audit event inside every retention window
|
|
41
41
|
// (90 days), or its crons vanish from the log once the first event is pruned.
|
|
42
42
|
const PROCESS_REAUDIT_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
43
|
+
// Declared escape hatches of system crons are audited once per day and process for each
|
|
44
|
+
// (handler, kind, tenant, target): the declaration, the system actor and the reason are
|
|
45
|
+
// identical on every run, so repeats carry no information. The target is part of the
|
|
46
|
+
// key only where it names a table (one run can write several global tables); a row target
|
|
47
|
+
// would add a key and an audit event per touched foreign row.
|
|
48
|
+
const TABLE_TARGET_KINDS = new Set(["global-write"]);
|
|
49
|
+
// Row-level targets add a key per touched row; without a sweep a long-lived worker keeps them all.
|
|
50
|
+
function sweepExpiredProcessDedup(dedup, nowMs) {
|
|
51
|
+
if (nowMs - dedup.lastSweep.atMs >= PROCESS_REAUDIT_INTERVAL_MS) {
|
|
52
|
+
dedup.lastSweep.atMs = nowMs;
|
|
53
|
+
for (const [key, auditedAt] of dedup.lastAuditedAt) {
|
|
54
|
+
if (nowMs - auditedAt >= PROCESS_REAUDIT_INTERVAL_MS)
|
|
55
|
+
dedup.lastAuditedAt.delete(key);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
43
59
|
export function createEscapeHatchProcessDedup(kinds) {
|
|
44
|
-
return { kinds: new Set(kinds), lastAuditedAt: new Map() };
|
|
60
|
+
return { kinds: new Set(kinds), lastAuditedAt: new Map(), lastSweep: { atMs: 0 } };
|
|
45
61
|
}
|
|
46
62
|
// Default window for createEscapeHatchReporter callers that don't pass their own.
|
|
47
63
|
const fallbackReportWindow = createEscapeHatchReportWindow();
|
|
@@ -147,12 +163,13 @@ export function createEscapeHatchReporter(opts) {
|
|
|
147
163
|
emitEscapeHatchUse(opts.meter, opts.handler, kind);
|
|
148
164
|
const { processDedup } = opts;
|
|
149
165
|
if (processDedup?.kinds.has(kind)) {
|
|
166
|
+
const keyTarget = TABLE_TARGET_KINDS.has(kind) ? target : undefined;
|
|
150
167
|
const processKey = JSON.stringify([
|
|
151
168
|
opts.handler,
|
|
152
169
|
kind,
|
|
153
170
|
opts.tenantId,
|
|
154
|
-
|
|
155
|
-
|
|
171
|
+
keyTarget?.id ?? null,
|
|
172
|
+
keyTarget?.tenantId ?? null,
|
|
156
173
|
]);
|
|
157
174
|
const nowMs = now();
|
|
158
175
|
const lastAuditedAt = processDedup.lastAuditedAt.get(processKey);
|
|
@@ -160,6 +177,7 @@ export function createEscapeHatchReporter(opts) {
|
|
|
160
177
|
if (lastAuditedAt !== undefined && nowMs - lastAuditedAt < PROCESS_REAUDIT_INTERVAL_MS) {
|
|
161
178
|
return;
|
|
162
179
|
}
|
|
180
|
+
sweepExpiredProcessDedup(processDedup, nowMs);
|
|
163
181
|
processDedup.lastAuditedAt.set(processKey, nowMs);
|
|
164
182
|
reportEscapeHatchUse(eventFor(kind, reason, target), deps);
|
|
165
183
|
// skip: audited above, the window dedup below only serves the other kinds
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { createFallbackLogger } from "../logging/utils.ts";
|
|
2
|
+
import type { AfterCommitHook } from "./dispatcher-utils.ts";
|
|
3
|
+
export type AfterRollbackHook = () => Promise<void>;
|
|
4
|
+
export declare function afterRollbackHooksOf(commitSink: AfterCommitHook[]): AfterRollbackHook[];
|
|
5
|
+
export declare function carrySavepointRollbackHooks(innerCommitSink: AfterCommitHook[], outerCommitSink: AfterCommitHook[], savepointRolledBack: boolean): void;
|
|
6
|
+
export declare function flushAfterRollback(commitSink: AfterCommitHook[], logError: Pick<ReturnType<typeof createFallbackLogger>, "error">): Promise<void>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Side channel keyed by the afterCommit sink array, so executeWrite and friends keep their signatures.
|
|
2
|
+
// Invariant: every registration runs at most once, and only if the work it compensates was discarded.
|
|
3
|
+
const rollbackSinks = new WeakMap();
|
|
4
|
+
export function afterRollbackHooksOf(commitSink) {
|
|
5
|
+
const existing = rollbackSinks.get(commitSink);
|
|
6
|
+
if (existing)
|
|
7
|
+
return existing;
|
|
8
|
+
const created = [];
|
|
9
|
+
rollbackSinks.set(commitSink, created);
|
|
10
|
+
return created;
|
|
11
|
+
}
|
|
12
|
+
// A rolled-back savepoint is not compensated immediately: the compensation could wait on locks of
|
|
13
|
+
// the still-open outer transaction. It runs once after that transaction ends, via the commit sink
|
|
14
|
+
// on commit or the rollback sink on rollback.
|
|
15
|
+
export function carrySavepointRollbackHooks(innerCommitSink, outerCommitSink, savepointRolledBack) {
|
|
16
|
+
const innerHooks = rollbackSinks.get(innerCommitSink) ?? [];
|
|
17
|
+
if (innerHooks.length > 0) {
|
|
18
|
+
if (savepointRolledBack)
|
|
19
|
+
outerCommitSink.push(...innerHooks);
|
|
20
|
+
afterRollbackHooksOf(outerCommitSink).push(...innerHooks);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function flushAfterRollback(commitSink, logError) {
|
|
24
|
+
const hooks = rollbackSinks.get(commitSink) ?? [];
|
|
25
|
+
const outcomes = await Promise.allSettled(hooks.map((hook) => hook()));
|
|
26
|
+
for (const outcome of outcomes) {
|
|
27
|
+
if (outcome.status === "rejected") {
|
|
28
|
+
const detail = outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason);
|
|
29
|
+
logError.error("afterRollback hook failed", { error: detail });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -22,151 +22,178 @@ export function createCascadeDeleteHook(registry, tables) {
|
|
|
22
22
|
return;
|
|
23
23
|
}
|
|
24
24
|
const writeDb = isBoundTenantDb(db) ? db : undefined;
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
const created = createEventStoreExecutor(table, entity, { entityName: name });
|
|
36
|
-
executors.set(name, created);
|
|
37
|
-
return created;
|
|
38
|
-
};
|
|
39
|
-
const requireWriteDb = () => {
|
|
40
|
-
if (!writeDb) {
|
|
41
|
-
throw new InternalError({
|
|
42
|
-
message: "cascadeDelete: a TenantDb is required to write children",
|
|
43
|
-
});
|
|
44
|
-
}
|
|
45
|
-
return writeDb;
|
|
46
|
-
};
|
|
47
|
-
const unwrap = (result) => {
|
|
48
|
-
if (!result.isSuccess)
|
|
49
|
-
throw reraiseAsKumikoError(result.error);
|
|
50
|
-
};
|
|
51
|
-
const childIds = async (targetTable, where, entity) => {
|
|
52
|
-
// A soft-deleted child is already invisible; deleting it again would fail.
|
|
53
|
-
const rows = await selectMany(db, targetTable, {
|
|
54
|
-
...where,
|
|
55
|
-
...(entity?.softDelete === true && { isDeleted: false }),
|
|
56
|
-
});
|
|
57
|
-
return rows.map((row) => row.id);
|
|
25
|
+
const run = {
|
|
26
|
+
registry,
|
|
27
|
+
db,
|
|
28
|
+
entityName,
|
|
29
|
+
parentId: payload.id,
|
|
30
|
+
lockGuardBypassed: payload.lockGuardBypassed === true,
|
|
31
|
+
cascadeUser: createSystemUser(writeDb?.tenantId ?? payload.event.tenantId),
|
|
32
|
+
executorFor: createExecutorCache(tables),
|
|
33
|
+
writeDb,
|
|
58
34
|
};
|
|
59
35
|
// Cascade applies to outgoing hasMany / manyToMany relations only —
|
|
60
36
|
// the parent side of the link is where `onDelete` lives (see
|
|
61
37
|
// relations.ts). Incoming belongsTo edges are covered because their
|
|
62
38
|
// counterpart hasMany declares the strategy.
|
|
63
|
-
const outgoing = registry.getRelations(entityName);
|
|
64
39
|
// Outgoing: e.g. department.users (hasMany target: user) — users have FK to department
|
|
65
|
-
for (const
|
|
66
|
-
|
|
67
|
-
// at a parent; the parent's onDelete decides what happens to this node.
|
|
68
|
-
if (relation.type !== "hasMany" && relation.type !== "manyToMany")
|
|
69
|
-
continue;
|
|
70
|
-
const strategy = relation.onDelete ?? OnDeleteStrategies.nothing;
|
|
71
|
-
if (strategy === OnDeleteStrategies.nothing)
|
|
72
|
-
continue;
|
|
73
|
-
if (relation.type === "hasMany" && relation.foreignKey) {
|
|
74
|
-
const targetTable = tables.get(relation.target);
|
|
75
|
-
if (!targetTable)
|
|
76
|
-
continue;
|
|
77
|
-
const childEntity = registry.getEntity(relation.target);
|
|
78
|
-
if (!childEntity &&
|
|
79
|
-
(strategy === OnDeleteStrategies.cascade || strategy === OnDeleteStrategies.setNull)) {
|
|
80
|
-
throw new InternalError({
|
|
81
|
-
message: `cascadeDelete: child "${relation.target}" is not an entity; changing its rows needs an executor`,
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
if (strategy === OnDeleteStrategies.restrict) {
|
|
85
|
-
const row = await fetchOne(db, targetTable, { [relation.foreignKey]: payload.id });
|
|
86
|
-
if (row) {
|
|
87
|
-
throw new ConflictError({
|
|
88
|
-
message: `${relation.target} has records referencing ${entityName}#${payload.id}`,
|
|
89
|
-
i18nKey: "errors.deleteRestricted",
|
|
90
|
-
details: {
|
|
91
|
-
reason: FrameworkReasons.deleteRestricted,
|
|
92
|
-
blockingEntity: relation.target,
|
|
93
|
-
entity: entityName,
|
|
94
|
-
entityId: payload.id,
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
// The child writes below skip the executor's lock guard (decided here once), so a locked
|
|
100
|
-
// child must block the parent delete instead of changing with it. The error names
|
|
101
|
-
// the parent only: the caller may delete the parent without reading the child.
|
|
102
|
-
if (strategy === OnDeleteStrategies.cascade || strategy === OnDeleteStrategies.setNull) {
|
|
103
|
-
const childLock = childEntity?.lockedWhen;
|
|
104
|
-
if (childLock !== undefined && payload.lockGuardBypassed !== true) {
|
|
105
|
-
// A soft-deleted child is invisible to the caller and must not block.
|
|
106
|
-
const children = await selectMany(db, targetTable, {
|
|
107
|
-
[relation.foreignKey]: payload.id,
|
|
108
|
-
...(childEntity?.softDelete === true && { isDeleted: false }),
|
|
109
|
-
});
|
|
110
|
-
if (children.some((child) => isRecordLocked(childLock, child))) {
|
|
111
|
-
throw new RecordLockedError({ entityName, entityId: payload.id });
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
if (strategy === OnDeleteStrategies.cascade && childEntity) {
|
|
116
|
-
const executor = executorFor(relation.target, childEntity);
|
|
117
|
-
for (const id of await childIds(targetTable, { [relation.foreignKey]: payload.id }, childEntity)) {
|
|
118
|
-
unwrap(await executor.delete({ id }, cascadeUser, requireWriteDb(), CASCADE_CHILD_WRITE_OPTIONS));
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (strategy === OnDeleteStrategies.setNull && childEntity) {
|
|
122
|
-
const executor = executorFor(relation.target, childEntity);
|
|
123
|
-
const rows = await selectMany(db, targetTable, {
|
|
124
|
-
[relation.foreignKey]: payload.id,
|
|
125
|
-
});
|
|
126
|
-
for (const row of rows) {
|
|
127
|
-
unwrap(await executor.update({ id: row.id, changes: { [relation.foreignKey]: null } }, cascadeUser, requireWriteDb(), {
|
|
128
|
-
...CASCADE_CHILD_WRITE_OPTIONS,
|
|
129
|
-
skipOptimisticLock: true,
|
|
130
|
-
includeDeleted: true,
|
|
131
|
-
}));
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
if (relation.type === "manyToMany" && relation.through) {
|
|
136
|
-
const throughTable = tables.get(relation.through.table);
|
|
137
|
-
if (!throughTable)
|
|
138
|
-
continue;
|
|
139
|
-
// sourceKey points at the owner side (the entity being deleted).
|
|
140
|
-
const sourceKey = relation.through.sourceKey;
|
|
141
|
-
if (strategy === OnDeleteStrategies.restrict) {
|
|
142
|
-
const row = await fetchOne(db, throughTable, { [sourceKey]: payload.id });
|
|
143
|
-
if (row) {
|
|
144
|
-
throw new ConflictError({
|
|
145
|
-
message: `${relation.through.table} has records referencing ${entityName}#${payload.id}`,
|
|
146
|
-
i18nKey: "errors.deleteRestricted",
|
|
147
|
-
details: {
|
|
148
|
-
reason: FrameworkReasons.deleteRestricted,
|
|
149
|
-
blockingEntity: relation.through.table,
|
|
150
|
-
entity: entityName,
|
|
151
|
-
entityId: payload.id,
|
|
152
|
-
},
|
|
153
|
-
});
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
if (strategy === OnDeleteStrategies.cascade) {
|
|
157
|
-
const throughEntity = registry.getEntity(relation.through.table);
|
|
158
|
-
if (!throughEntity) {
|
|
159
|
-
throw new InternalError({
|
|
160
|
-
message: `cascadeDelete: through table "${relation.through.table}" is not an entity; deleting its rows needs an executor`,
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
const executor = executorFor(relation.through.table, throughEntity);
|
|
164
|
-
for (const id of await childIds(throughTable, { [sourceKey]: payload.id }, throughEntity)) {
|
|
165
|
-
unwrap(await executor.delete({ id }, cascadeUser, requireWriteDb(), CASCADE_CHILD_WRITE_OPTIONS));
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
40
|
+
for (const relation of Object.values(registry.getRelations(entityName))) {
|
|
41
|
+
await applyOnDelete(run, tables, relation);
|
|
169
42
|
}
|
|
170
43
|
},
|
|
171
44
|
};
|
|
172
45
|
}
|
|
46
|
+
function createExecutorCache(tables) {
|
|
47
|
+
const executors = new Map();
|
|
48
|
+
return (name, entity) => {
|
|
49
|
+
const cached = executors.get(name);
|
|
50
|
+
if (cached)
|
|
51
|
+
return cached;
|
|
52
|
+
const table = tables.get(name);
|
|
53
|
+
if (!table) {
|
|
54
|
+
throw new InternalError({ message: `cascadeDelete: no table registered for "${name}"` });
|
|
55
|
+
}
|
|
56
|
+
const created = createEventStoreExecutor(table, entity, { entityName: name });
|
|
57
|
+
executors.set(name, created);
|
|
58
|
+
return created;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function requireCascadeWriteDb(writeDb) {
|
|
62
|
+
if (!writeDb) {
|
|
63
|
+
throw new InternalError({
|
|
64
|
+
message: "cascadeDelete: a TenantDb is required to write children",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return writeDb;
|
|
68
|
+
}
|
|
69
|
+
function unwrap(result) {
|
|
70
|
+
if (!result.isSuccess)
|
|
71
|
+
throw reraiseAsKumikoError(result.error);
|
|
72
|
+
}
|
|
73
|
+
async function childIds(run, targetTable, where, entity) {
|
|
74
|
+
// A soft-deleted child is already invisible; deleting it again would fail.
|
|
75
|
+
const rows = await selectMany(run.db, targetTable, { ...where, ...(entity.softDelete === true && { isDeleted: false }) }, { fields: ["id"] });
|
|
76
|
+
return rows.map((row) => row.id);
|
|
77
|
+
}
|
|
78
|
+
// Only hasMany / manyToMany carry cascade semantics. belongsTo points
|
|
79
|
+
// at a parent; the parent's onDelete decides what happens to this node.
|
|
80
|
+
async function applyOnDelete(run, tables, relation) {
|
|
81
|
+
if (relation.type === "hasMany" && relation.foreignKey) {
|
|
82
|
+
const table = tables.get(relation.target);
|
|
83
|
+
if (table) {
|
|
84
|
+
const link = { target: relation.target, foreignKey: relation.foreignKey, table };
|
|
85
|
+
await HAS_MANY_ON_DELETE[onDeleteStrategyOf(relation)](run, link);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (relation.type === "manyToMany" && relation.through) {
|
|
89
|
+
const table = tables.get(relation.through.table);
|
|
90
|
+
if (table) {
|
|
91
|
+
// sourceKey points at the owner side (the entity being deleted).
|
|
92
|
+
const link = {
|
|
93
|
+
throughTable: relation.through.table,
|
|
94
|
+
sourceKey: relation.through.sourceKey,
|
|
95
|
+
table,
|
|
96
|
+
};
|
|
97
|
+
await THROUGH_ON_DELETE[onDeleteStrategyOf(relation)](run, link);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function onDeleteStrategyOf(relation) {
|
|
102
|
+
return relation.onDelete ?? OnDeleteStrategies.nothing;
|
|
103
|
+
}
|
|
104
|
+
async function leaveChildrenUnchanged() { }
|
|
105
|
+
const HAS_MANY_ON_DELETE = {
|
|
106
|
+
nothing: leaveChildrenUnchanged,
|
|
107
|
+
restrict: rejectReferencedParent,
|
|
108
|
+
cascade: deleteHasManyChildren,
|
|
109
|
+
setNull: detachHasManyChildren,
|
|
110
|
+
};
|
|
111
|
+
// A link row has no nullable side: setNull on a manyToMany leaves the through rows in place.
|
|
112
|
+
const THROUGH_ON_DELETE = {
|
|
113
|
+
nothing: leaveChildrenUnchanged,
|
|
114
|
+
restrict: rejectReferencedThroughRows,
|
|
115
|
+
cascade: deleteThroughRows,
|
|
116
|
+
setNull: leaveChildrenUnchanged,
|
|
117
|
+
};
|
|
118
|
+
async function rejectReferencedParent(run, link) {
|
|
119
|
+
const row = await fetchOne(run.db, link.table, { [link.foreignKey]: run.parentId });
|
|
120
|
+
if (row)
|
|
121
|
+
throw deleteRestrictedError(run, link.target);
|
|
122
|
+
}
|
|
123
|
+
async function rejectReferencedThroughRows(run, link) {
|
|
124
|
+
const row = await fetchOne(run.db, link.table, { [link.sourceKey]: run.parentId });
|
|
125
|
+
if (row)
|
|
126
|
+
throw deleteRestrictedError(run, link.throughTable);
|
|
127
|
+
}
|
|
128
|
+
function deleteRestrictedError(run, blockingEntity) {
|
|
129
|
+
return new ConflictError({
|
|
130
|
+
message: `${blockingEntity} has records referencing ${run.entityName}#${run.parentId}`,
|
|
131
|
+
i18nKey: "errors.deleteRestricted",
|
|
132
|
+
details: {
|
|
133
|
+
reason: FrameworkReasons.deleteRestricted,
|
|
134
|
+
blockingEntity,
|
|
135
|
+
entity: run.entityName,
|
|
136
|
+
entityId: run.parentId,
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
// ponytail: one executor call per child inside the parent's transaction, batch when child counts need it
|
|
141
|
+
async function deleteHasManyChildren(run, link) {
|
|
142
|
+
const childEntity = requireChildEntity(run, link);
|
|
143
|
+
await rejectLockedChildren(run, link, childEntity);
|
|
144
|
+
const executor = run.executorFor(link.target, childEntity);
|
|
145
|
+
for (const id of await childIds(run, link.table, { [link.foreignKey]: run.parentId }, childEntity)) {
|
|
146
|
+
unwrap(await executor.delete({ id }, run.cascadeUser, requireCascadeWriteDb(run.writeDb), CASCADE_CHILD_WRITE_OPTIONS));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function detachHasManyChildren(run, link) {
|
|
150
|
+
const childEntity = requireChildEntity(run, link);
|
|
151
|
+
await rejectLockedChildren(run, link, childEntity);
|
|
152
|
+
const executor = run.executorFor(link.target, childEntity);
|
|
153
|
+
const rows = await selectMany(run.db, link.table, { [link.foreignKey]: run.parentId }, { fields: ["id"] });
|
|
154
|
+
for (const row of rows) {
|
|
155
|
+
unwrap(await executor.update({ id: row.id, changes: { [link.foreignKey]: null } }, run.cascadeUser, requireCascadeWriteDb(run.writeDb), {
|
|
156
|
+
...CASCADE_CHILD_WRITE_OPTIONS,
|
|
157
|
+
skipOptimisticLock: true,
|
|
158
|
+
includeDeleted: true,
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function requireChildEntity(run, link) {
|
|
163
|
+
const childEntity = run.registry.getEntity(link.target);
|
|
164
|
+
if (!childEntity) {
|
|
165
|
+
throw new InternalError({
|
|
166
|
+
message: `cascadeDelete: child "${link.target}" is not an entity; changing its rows needs an executor`,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return childEntity;
|
|
170
|
+
}
|
|
171
|
+
// The child writes skip the executor's lock guard (decided here once), so a locked
|
|
172
|
+
// child must block the parent delete instead of changing with it. The error names
|
|
173
|
+
// the parent only: the caller may delete the parent without reading the child.
|
|
174
|
+
async function rejectLockedChildren(run, link, childEntity) {
|
|
175
|
+
const childLock = childEntity.lockedWhen;
|
|
176
|
+
// skip: an unlockable child or an already bypassed parent delete has nothing to block
|
|
177
|
+
if (childLock === undefined || run.lockGuardBypassed)
|
|
178
|
+
return;
|
|
179
|
+
// A soft-deleted child is invisible to the caller and must not block.
|
|
180
|
+
const children = await selectMany(run.db, link.table, {
|
|
181
|
+
[link.foreignKey]: run.parentId,
|
|
182
|
+
...(childEntity.softDelete === true && { isDeleted: false }),
|
|
183
|
+
});
|
|
184
|
+
if (children.some((child) => isRecordLocked(childLock, child))) {
|
|
185
|
+
throw new RecordLockedError({ entityName: run.entityName, entityId: run.parentId });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function deleteThroughRows(run, link) {
|
|
189
|
+
const throughEntity = run.registry.getEntity(link.throughTable);
|
|
190
|
+
if (!throughEntity) {
|
|
191
|
+
throw new InternalError({
|
|
192
|
+
message: `cascadeDelete: through table "${link.throughTable}" is not an entity; deleting its rows needs an executor`,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const executor = run.executorFor(link.throughTable, throughEntity);
|
|
196
|
+
for (const id of await childIds(run, link.table, { [link.sourceKey]: run.parentId }, throughEntity)) {
|
|
197
|
+
unwrap(await executor.delete({ id }, run.cascadeUser, requireCascadeWriteDb(run.writeDb), CASCADE_CHILD_WRITE_OPTIONS));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -3,6 +3,7 @@ import { transaction } from "../db/query.js";
|
|
|
3
3
|
import { InternalError, toWriteErrorInfo, UnprocessableError, writeFailure, } from "../errors/index.js";
|
|
4
4
|
import { createFallbackLogger } from "../logging/utils.js";
|
|
5
5
|
import { parseJsonSafe } from "../utils/safe-json.js";
|
|
6
|
+
import { flushAfterRollback } from "./after-rollback-sink.js";
|
|
6
7
|
import { resolveAppPoolDb } from "./dispatch-shared.js";
|
|
7
8
|
import { executeNestedWrite, executeWrite } from "./dispatch-write.js";
|
|
8
9
|
import { BatchRollback, isLifecycleResult, wrapToKumiko, } from "./dispatcher-utils.js";
|
|
@@ -36,6 +37,9 @@ async function runReservedCommand(scope, cmd, index, tx) {
|
|
|
36
37
|
rewrapHooksWithOrigin(afterCommitHooks, hookStart, origin);
|
|
37
38
|
return res;
|
|
38
39
|
}
|
|
40
|
+
function rollbackLogger(scope) {
|
|
41
|
+
return createFallbackLogger("dispatcher", scope.ctx.appContext.log);
|
|
42
|
+
}
|
|
39
43
|
// Sequential fallback without an outer transaction. A failure result and a throw both end the
|
|
40
44
|
// batch, so both give the reserved capacity back.
|
|
41
45
|
async function runCommandsWithoutTransaction(scope, commands, results) {
|
|
@@ -50,12 +54,14 @@ async function runCommandsWithoutTransaction(scope, commands, results) {
|
|
|
50
54
|
// No tx means no rollback — but we still drop afterCommit hooks,
|
|
51
55
|
// matching the semantic "failure = side-effects don't fire".
|
|
52
56
|
await scope.reservations.releaseAll();
|
|
57
|
+
await flushAfterRollback(scope.afterCommitHooks, rollbackLogger(scope));
|
|
53
58
|
return { index: i, error: res.error };
|
|
54
59
|
}
|
|
55
60
|
}
|
|
56
61
|
}
|
|
57
62
|
catch (e) {
|
|
58
63
|
await scope.reservations.releaseAll();
|
|
64
|
+
await flushAfterRollback(scope.afterCommitHooks, rollbackLogger(scope));
|
|
59
65
|
throw e;
|
|
60
66
|
}
|
|
61
67
|
return undefined;
|
|
@@ -208,7 +214,7 @@ async function runBatchBody(ctx, commands, user, requestId, inheritedOrigin) {
|
|
|
208
214
|
}, reservations.error.httpStatus >= 500);
|
|
209
215
|
}
|
|
210
216
|
// batch() opens its own outer transaction — needs the top-level
|
|
211
|
-
// connection's `.begin()` (
|
|
217
|
+
// connection's `.begin()` (a transaction handle cannot begin another one).
|
|
212
218
|
const db = resolveAppPoolDb(ctx);
|
|
213
219
|
if (!db || runsWithoutTransaction) {
|
|
214
220
|
// Without a DB connection (or for a perCommand handler, whose writes open their own transactions)
|
|
@@ -250,6 +256,7 @@ async function runBatchBody(ctx, commands, user, requestId, inheritedOrigin) {
|
|
|
250
256
|
// Rollback or failed COMMIT: the transaction is over, so the reserved capacity goes back.
|
|
251
257
|
// The release is outcome-safe: it only gives back a reservation row the handler tx did not delete.
|
|
252
258
|
await reservations.releaseAll();
|
|
259
|
+
await flushAfterRollback(afterCommitHooks, createFallbackLogger("dispatcher", context.log));
|
|
253
260
|
if (e instanceof BatchRollback) {
|
|
254
261
|
// Thrown inside the callback, so the tx rolled back. A 4xx is
|
|
255
262
|
// deterministic and stays cached; a 5xx may be transient.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isListAggregateResult } from "@cosmicdrift/kumiko-types/list-aggregate-types";
|
|
1
2
|
import { isPlainObject, propertyOf } from "@cosmicdrift/kumiko-types/type-guards";
|
|
2
3
|
import { hasAccess } from "../engine/access.js";
|
|
3
4
|
import { tenantOverrideDenied } from "../engine/cross-tenant.js";
|
|
@@ -58,7 +59,11 @@ async function executeQueryInner(ctx, type, payload, user, origin, tx, outerNoti
|
|
|
58
59
|
}
|
|
59
60
|
async function runQueryHandler(ctx, type, handler, payload, includeDeleted, user, origin, tx, outerNotifySink) {
|
|
60
61
|
const { registry } = ctx;
|
|
61
|
-
const handlerContext = await buildHandlerContext(ctx, type, user, origin,
|
|
62
|
+
const handlerContext = await buildHandlerContext(ctx, type, user, origin, {
|
|
63
|
+
tx,
|
|
64
|
+
includeDeleted,
|
|
65
|
+
outerNotifySink,
|
|
66
|
+
});
|
|
62
67
|
let result = await handler.handler({ type, payload, user }, handlerContext);
|
|
63
68
|
// postQuery hooks run BEFORE field-access filtering so they see raw data and can
|
|
64
69
|
// merge custom fields, computed counts, tags, and similar values. Each hook is
|
|
@@ -78,7 +83,9 @@ async function runQueryHandler(ctx, type, handler, payload, includeDeleted, user
|
|
|
78
83
|
const handlerHooks = registry.getPostQueryHooks(type);
|
|
79
84
|
const entityHooks = entityName ? registry.getEntityPostQueryHooks(entityName) : [];
|
|
80
85
|
const postQueryHooks = [...handlerHooks, ...entityHooks];
|
|
81
|
-
|
|
86
|
+
// Aggregate groups are not rows: their values are already read-gated and the hooks are row-shaped.
|
|
87
|
+
const isAggregate = isListAggregateResult(result);
|
|
88
|
+
if (!isAggregate && postQueryHooks.length > 0 && result && typeof result === "object") {
|
|
82
89
|
if (Array.isArray(result)) {
|
|
83
90
|
let rows = result; // @cast-boundary engine-payload
|
|
84
91
|
for (const hook of postQueryHooks) {
|
|
@@ -115,7 +122,7 @@ async function runQueryHandler(ctx, type, handler, payload, includeDeleted, user
|
|
|
115
122
|
}
|
|
116
123
|
// Field-level read filter — only applies to entity-bound results.
|
|
117
124
|
const entity = entityName ? registry.getEntity(entityName) : undefined;
|
|
118
|
-
if (entity && result && typeof result === "object") {
|
|
125
|
+
if (entity && !isAggregate && result && typeof result === "object") {
|
|
119
126
|
if (Array.isArray(result)) {
|
|
120
127
|
result = result.map((row) => filterReadFields(entity, row, user));
|
|
121
128
|
}
|
|
@@ -2,12 +2,13 @@ import type { CacheSyncBus } from "@cosmicdrift/kumiko-types/cache-sync-types";
|
|
|
2
2
|
import type { SseBroker } from "../api/sse-broker.ts";
|
|
3
3
|
import type { DbConnection, DbTx } from "../db/connection.ts";
|
|
4
4
|
import type { buildEntityTable } from "../db/table-builder.ts";
|
|
5
|
+
import { type TenantDb } from "../db/tenant-db.ts";
|
|
5
6
|
import type { defineTransitions } from "../engine/state-machine.ts";
|
|
6
7
|
import type { EffectiveFeaturesResolver } from "../engine/tier-resolver-extension.ts";
|
|
7
8
|
import type { TenantId } from "../engine/types/identifiers.ts";
|
|
8
9
|
import type { AppContext, HandlerContext, JobRunnerRef, PayloadRateLimitOption, RateLimitDeclaration, Registry, SessionUser, WriteResult } from "../engine/types/index.ts";
|
|
9
10
|
import { InternalError, type WriteErrorInfo } from "../errors/index.ts";
|
|
10
|
-
import { type EscapeHatchReportWindow, type getFallbackMeter, getFallbackTracer } from "../observability/index.ts";
|
|
11
|
+
import { type EscapeHatchReportWindow, type getFallbackMeter, type getFallbackTracer } from "../observability/index.ts";
|
|
11
12
|
import { type AfterCommitHook } from "./dispatcher-utils.ts";
|
|
12
13
|
import type { IdempotencyGuard } from "./idempotency.ts";
|
|
13
14
|
import type { LifecycleHooks } from "./lifecycle-pipeline.ts";
|
|
@@ -53,18 +54,27 @@ export type DispatchContext = {
|
|
|
53
54
|
export declare const DEFAULT_MEMBER_READ_TIMEOUT_MS = 10000;
|
|
54
55
|
export declare function resolveDbSource(ctx: DispatchContext, tx: DbTx | undefined): DbConnection | DbTx | undefined;
|
|
55
56
|
export declare function resolveAppPoolDb(ctx: DispatchContext): DbConnection | undefined;
|
|
57
|
+
export declare function createSystemScopedDbGuard(handlerType: string, fieldName: "db" | "dbOutsideTransaction", hint: string): TenantDb;
|
|
56
58
|
export declare function isMemberResolutionPrincipal(user: SessionUser): boolean;
|
|
57
59
|
export declare function applyMemberResolutionReadOnly(handlerContext: HandlerContext): HandlerContext;
|
|
58
60
|
export declare function perCommandNestedDenied(targetType: string): InternalError;
|
|
59
61
|
export declare function denyTransactionBoundSurfaces(handlerContext: HandlerContext): HandlerContext;
|
|
60
62
|
export declare function applyBeforeTransactionReadOnly(handlerContext: HandlerContext): HandlerContext;
|
|
61
63
|
export type HandlerTransactionScope = "inTransaction" | "withoutTransaction" | "postCommit";
|
|
62
|
-
export
|
|
64
|
+
export type BuildHandlerContextOptions = {
|
|
65
|
+
tx?: DbTx;
|
|
66
|
+
afterCommitHooks?: AfterCommitHook[];
|
|
67
|
+
includeDeleted?: boolean;
|
|
68
|
+
transactionScope?: HandlerTransactionScope;
|
|
69
|
+
outerNotifySink?: AfterCommitHook[];
|
|
70
|
+
readOnlyForTenant?: boolean;
|
|
71
|
+
};
|
|
72
|
+
export declare function buildHandlerContext(ctx: DispatchContext, type: string, user: SessionUser, origin: WriteOrigin, options?: BuildHandlerContextOptions): Promise<HandlerContext>;
|
|
63
73
|
export declare function createHandlerBoundDigest(digest: (value: string) => string, handlerName: string): (value: string) => string;
|
|
64
74
|
export declare function runHandlerInstrumented<T>(ctx: DispatchContext, type: string, operation: "query" | "write" | "stream", user: SessionUser, inner: () => Promise<T>): Promise<T>;
|
|
65
75
|
export declare function runStreamInstrumented<T>(ctx: DispatchContext, type: string, user: SessionUser, inner: () => AsyncGenerator<T>): AsyncGenerator<T>;
|
|
66
76
|
export declare function checkFeatureEnabled(ctx: DispatchContext, qualifiedHandler: string, tenantId: TenantId): Promise<import("../errors/index.ts").FeatureDisabledError | undefined>;
|
|
67
77
|
export declare function ensureFeatureEnabled(ctx: DispatchContext, qualifiedHandler: string, tenantId: TenantId): Promise<void>;
|
|
68
|
-
export declare function enforceRateLimit(ctx: DispatchContext, rateLimit: RateLimitDeclaration | undefined, handlerName: string, user: SessionUser, isSystemScope: boolean): Promise<void>;
|
|
78
|
+
export declare function enforceRateLimit(ctx: DispatchContext, rateLimit: RateLimitDeclaration | undefined, handlerName: string, user: SessionUser, isSystemScope: boolean, costOverride?: number): Promise<void>;
|
|
69
79
|
export declare function enforcePayloadRateLimits(ctx: DispatchContext, limits: readonly PayloadRateLimitOption[] | undefined, handlerName: string, user: SessionUser, data: unknown): Promise<void>;
|
|
70
80
|
export declare function resolveAuthClaimsFn(ctx: DispatchContext, user: SessionUser): Promise<Record<string, unknown>>;
|