@cosmicdrift/kumiko-framework 0.363.0 → 0.365.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 +338 -0
- package/agents/examples/access-control.md +355 -0
- package/agents/examples/anonymous-access-multitenant.md +446 -0
- package/agents/examples/anonymous-access.md +288 -0
- package/agents/examples/apex-landing.md +528 -0
- package/agents/examples/apex-surface-auth.md +355 -0
- package/agents/examples/api-worker-split.md +434 -0
- package/agents/examples/auth-claims.md +351 -0
- package/agents/examples/auth-foundation-providers.md +155 -0
- package/agents/examples/auth-mfa.md +319 -0
- package/agents/examples/basic-entity.md +420 -0
- package/agents/examples/boot-check.md +122 -0
- package/agents/examples/chat-channels.md +254 -0
- package/agents/examples/compliance-profiles-demo.md +307 -0
- package/agents/examples/cross-feature-events.md +317 -0
- package/agents/examples/crypto-shredding-hr.md +314 -0
- package/agents/examples/currencies-global.md +609 -0
- package/agents/examples/currencies-per-tenant.md +453 -0
- package/agents/examples/custom-fields-basic.md +315 -0
- package/agents/examples/custom-handlers.md +444 -0
- package/agents/examples/dashboard-stats.md +229 -0
- package/agents/examples/delivery-notifications.md +432 -0
- package/agents/examples/designer-demo.md +86 -0
- package/agents/examples/e2e-generator.md +211 -0
- package/agents/examples/embedded-entity-form.md +598 -0
- package/agents/examples/embedded.md +649 -0
- package/agents/examples/encrypted-tenant-config.md +414 -0
- package/agents/examples/error-contract.md +535 -0
- package/agents/examples/event-sourcing.md +1287 -0
- package/agents/examples/feature-toggles.md +274 -0
- package/agents/examples/field-access.md +271 -0
- package/agents/examples/files-post-processing.md +433 -0
- package/agents/examples/i18n.md +166 -0
- package/agents/examples/idempotency.md +179 -0
- package/agents/examples/inbound-mail-triage.md +315 -0
- package/agents/examples/lane-routing.md +246 -0
- package/agents/examples/ledger-banking.md +215 -0
- package/agents/examples/ledger-invoicing.md +236 -0
- package/agents/examples/legal-pages.md +393 -0
- package/agents/examples/lifecycle-hooks.md +368 -0
- package/agents/examples/managed-config.md +428 -0
- package/agents/examples/managed-pages.md +353 -0
- package/agents/examples/notes-history-basic.md +346 -0
- package/agents/examples/ownership.md +404 -0
- package/agents/examples/pdf-audit-export.md +306 -0
- package/agents/examples/pipeline-basics.md +792 -0
- package/agents/examples/public-share-token.md +351 -0
- package/agents/examples/rate-limiting.md +211 -0
- package/agents/examples/realtime-sse.md +187 -0
- package/agents/examples/record-detail-layout.md +297 -0
- package/agents/examples/reference-data.md +151 -0
- package/agents/examples/relations.md +280 -0
- package/agents/examples/screens-nav.md +353 -0
- package/agents/examples/search.md +233 -0
- package/agents/examples/secrets-demo.md +417 -0
- package/agents/examples/security-baseline.md +187 -0
- package/agents/examples/seed-migration.md +99 -0
- package/agents/examples/session-revocation.md +250 -0
- package/agents/examples/state-machine.md +920 -0
- package/agents/examples/tags-basic.md +401 -0
- package/agents/examples/tenant-isolation.md +173 -0
- package/agents/examples/tenant-settings.md +275 -0
- package/agents/examples/tier-admin.md +347 -0
- package/agents/examples/timezones.md +366 -0
- package/agents/examples/user-data-rights.md +418 -0
- package/agents/examples/user-profile.md +226 -0
- package/agents/examples/webhook-step.md +597 -0
- package/agents/examples/wizard-form.md +379 -0
- package/agents/examples/workflow-engine.md +1731 -0
- package/agents/examples/writeform-section.md +300 -0
- package/agents/examples.md +75 -0
- package/agents/features.md +768 -0
- package/agents/widgets.md +181 -0
- package/dist/api/auth-middleware.d.ts +2 -0
- package/dist/api/auth-middleware.js +7 -0
- package/dist/api/auth-routes.d.ts +0 -1
- package/dist/api/auth-routes.js +49 -46
- package/dist/api/extra-route.d.ts +3 -0
- package/dist/api/index.d.ts +2 -2
- package/dist/api/index.js +1 -1
- package/dist/api/routes.js +46 -17
- package/dist/api/server.d.ts +4 -3
- package/dist/api/server.js +39 -10
- package/dist/api/session-cookies.d.ts +1 -0
- package/dist/api/session-cookies.js +20 -5
- package/dist/api/sse-route.js +16 -1
- package/dist/bun-db/connection.js +3 -16
- package/dist/bun-db/index.d.ts +1 -1
- package/dist/bun-db/index.js +1 -1
- package/dist/bun-db/query.d.ts +15 -3
- package/dist/bun-db/query.js +76 -24
- package/dist/crypto/owner-transition-rekey.d.ts +1 -0
- package/dist/crypto/owner-transition-rekey.js +7 -3
- package/dist/crypto/pii-field-encryption.d.ts +1 -0
- package/dist/crypto/pii-field-encryption.js +9 -2
- package/dist/crypto/subject-resolver.d.ts +1 -0
- package/dist/crypto/subject-resolver.js +7 -0
- package/dist/db/api.d.ts +4 -3
- package/dist/db/apply-entity-event.js +26 -2
- package/dist/db/blind-index-cleanup.js +2 -1
- package/dist/db/bun-provider.js +9 -18
- package/dist/db/collect-table-metas.js +11 -7
- package/dist/db/dialect.d.ts +1 -1
- package/dist/db/dialect.js +2 -0
- package/dist/db/eagerload.js +4 -3
- package/dist/db/event-store-executor-context.d.ts +6 -6
- package/dist/db/event-store-executor-context.js +25 -22
- package/dist/db/event-store-executor-read.js +31 -16
- package/dist/db/event-store-executor-write.d.ts +1 -1
- package/dist/db/event-store-executor-write.js +84 -63
- package/dist/db/index.d.ts +2 -1
- package/dist/db/index.js +2 -1
- package/dist/db/lock-guard.d.ts +4 -0
- package/dist/db/lock-guard.js +14 -0
- package/dist/db/migrate-generator.js +1 -1
- package/dist/db/migrate-runner.d.ts +1 -0
- package/dist/db/migrate-runner.js +64 -38
- package/dist/db/queries/backfill-pii.js +63 -25
- package/dist/db/queries/event-consumer.d.ts +6 -6
- package/dist/db/queries/event-consumer.js +2 -1
- package/dist/db/queries/event-store-transfer.js +15 -4
- package/dist/db/queries/event-store.d.ts +1 -1
- package/dist/db/queries/event-store.js +11 -10
- package/dist/db/queries/pii-backfill-state.js +3 -2
- package/dist/db/queries/postgres-array-literal.d.ts +2 -0
- package/dist/db/queries/postgres-array-literal.js +6 -0
- package/dist/db/queries/projection-rebuild.d.ts +1 -1
- package/dist/db/queries/projection-rebuild.js +3 -2
- package/dist/db/queries/shadow-swap.d.ts +1 -1
- package/dist/db/queries/shadow-swap.js +12 -8
- package/dist/db/query.d.ts +1 -1
- package/dist/db/query.js +1 -1
- package/dist/db/retired-framework-tables.js +22 -4
- package/dist/db/table-builder.js +6 -2
- package/dist/db/tenant-db-runner.d.ts +4 -0
- package/dist/db/tenant-db-runner.js +17 -0
- package/dist/db/tenant-db.d.ts +4 -0
- package/dist/db/tenant-db.js +50 -5
- package/dist/derivatives/delete-stored-file.d.ts +4 -0
- package/dist/derivatives/delete-stored-file.js +9 -0
- package/dist/derivatives/derivatives-context.js +1 -1
- package/dist/derivatives/index.d.ts +1 -1
- package/dist/derivatives/index.js +1 -1
- package/dist/engine/boot-validator/access-declarations.js +2 -2
- package/dist/engine/boot-validator/api-ext.js +3 -3
- package/dist/engine/boot-validator/boot-check.js +1 -1
- package/dist/engine/boot-validator/config-deps.js +13 -13
- package/dist/engine/boot-validator/detail-screens.js +1 -1
- package/dist/engine/boot-validator/entity-handler.d.ts +1 -0
- package/dist/engine/boot-validator/entity-handler.js +87 -35
- package/dist/engine/boot-validator/entity-list-screens.js +2 -2
- package/dist/engine/boot-validator/handler-field-references.js +19 -9
- package/dist/engine/boot-validator/i18n-keys.js +2 -2
- package/dist/engine/boot-validator/nav.js +1 -1
- package/dist/engine/boot-validator/pii-retention.js +8 -8
- package/dist/engine/boot-validator/projection-list-screens.js +5 -5
- package/dist/engine/boot-validator/query-output-columns.js +7 -7
- package/dist/engine/boot-validator/query-refs.js +14 -2
- package/dist/engine/boot-validator/screens.d.ts +2 -1
- package/dist/engine/boot-validator/screens.js +153 -95
- package/dist/engine/boot-validator/url-prefill-fields.js +54 -35
- package/dist/engine/dedupe-features.js +31 -4
- package/dist/engine/define-handler.d.ts +2 -2
- package/dist/engine/define-handler.js +8 -1
- package/dist/engine/entity-handlers.js +14 -7
- package/dist/engine/feature-ast/extractors/handlers.js +2 -0
- package/dist/engine/feature-ast/pattern-change-schema.d.ts +2 -0
- package/dist/engine/feature-ast/pattern-change-schema.js +1 -0
- package/dist/engine/feature-ast/render.js +10 -2
- package/dist/engine/feature-entity-handlers.d.ts +2 -2
- package/dist/engine/feature-entity-handlers.js +12 -3
- package/dist/engine/handler-field-references.d.ts +21 -2
- package/dist/engine/handler-field-references.js +55 -17
- package/dist/engine/index.d.ts +3 -3
- package/dist/engine/index.js +1 -1
- package/dist/engine/schema-builder.js +2 -1
- package/dist/engine/screen-helpers.d.ts +1 -0
- package/dist/engine/screen-helpers.js +2 -2
- package/dist/engine/soft-delete-cleanup.js +10 -3
- package/dist/engine/steps/_read-source.js +5 -2
- package/dist/engine/steps/retry.js +2 -1
- package/dist/engine/steps/unsafe-projection-delete.js +2 -2
- package/dist/engine/steps/unsafe-projection-upsert.js +2 -2
- package/dist/engine/template-variables.d.ts +3 -0
- package/dist/engine/template-variables.js +3 -0
- package/dist/engine/tier-resolver-extension.d.ts +1 -0
- package/dist/engine/tier-resolver-extension.js +6 -0
- package/dist/engine/types/index.d.ts +1 -1
- package/dist/entrypoint/index.d.ts +2 -2
- package/dist/env/_zod-introspect.d.ts +1 -0
- package/dist/errors/classes.js +1 -0
- package/dist/errors/docs-url.js +6 -6
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/errors/kumiko-error.d.ts +2 -0
- package/dist/errors/kumiko-error.js +2 -0
- package/dist/errors/subject-erased.d.ts +1 -0
- package/dist/errors/subject-erased.js +6 -0
- package/dist/errors/write-error-info.js +2 -0
- package/dist/event-store/admin-api.js +3 -2
- package/dist/event-store/errors.d.ts +9 -0
- package/dist/event-store/errors.js +20 -0
- package/dist/event-store/event-store.d.ts +6 -0
- package/dist/event-store/event-store.js +28 -6
- package/dist/event-store/index.d.ts +1 -1
- package/dist/event-store/index.js +1 -1
- package/dist/event-store/tenant-db-append.js +2 -2
- package/dist/files/assert-file-refs-belong-to-tenant.d.ts +4 -5
- package/dist/files/assert-file-refs-belong-to-tenant.js +7 -15
- package/dist/files/bind-pending-file-refs.d.ts +2 -0
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/bind-referenced-files.write.d.ts +1 -1
- package/dist/files/bind-referenced-files.write.js +22 -13
- package/dist/files/create-file-ref-from-bytes.d.ts +6 -3
- package/dist/files/create-file-ref-from-bytes.js +67 -4
- package/dist/files/create-file-ref.d.ts +24 -1
- package/dist/files/create-file-ref.js +67 -25
- package/dist/files/dispose-removed-file-refs.js +21 -6
- package/dist/files/feature.d.ts +1 -0
- package/dist/files/feature.js +2 -1
- package/dist/files/file-handle.d.ts +1 -1
- package/dist/files/file-handle.js +12 -0
- package/dist/files/file-routes.js +10 -6
- package/dist/files/index.d.ts +3 -3
- package/dist/files/index.js +2 -2
- package/dist/files/load-save-file-refs.d.ts +10 -0
- package/dist/files/load-save-file-refs.js +47 -0
- package/dist/files/purge-expired-file-ref-bytes.d.ts +4 -1
- package/dist/files/purge-expired-file-ref-bytes.js +46 -21
- package/dist/files/resolve-content-type.js +40 -4
- package/dist/files/storage-tracking.js +5 -4
- package/dist/files/types.js +5 -8
- package/dist/files/zip-entry-names.d.ts +1 -0
- package/dist/files/zip-entry-names.js +46 -0
- package/dist/jobs/index.d.ts +1 -1
- package/dist/jobs/job-runner.d.ts +5 -1
- package/dist/jobs/job-runner.js +124 -37
- package/dist/observability/escape-hatch-report.d.ts +2 -1
- package/dist/observability/escape-hatch-report.js +48 -7
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +1 -1
- package/dist/observability/metrics-handle.d.ts +1 -0
- package/dist/observability/metrics-handle.js +7 -0
- package/dist/observability/standard-metrics.js +4 -4
- package/dist/pipeline/cascade-handler.js +5 -2
- package/dist/pipeline/dispatch-batch.js +79 -31
- package/dist/pipeline/dispatch-query.d.ts +2 -1
- package/dist/pipeline/dispatch-query.js +6 -6
- package/dist/pipeline/dispatch-shared.d.ts +7 -2
- package/dist/pipeline/dispatch-shared.js +136 -48
- package/dist/pipeline/dispatch-stream.js +10 -4
- package/dist/pipeline/dispatch-write.js +34 -22
- package/dist/pipeline/dispatcher.js +3 -1
- package/dist/pipeline/event-dispatcher.d.ts +2 -2
- package/dist/pipeline/event-dispatcher.js +11 -9
- package/dist/pipeline/event-retention.d.ts +1 -0
- package/dist/pipeline/event-retention.js +7 -1
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline/notify-scope.js +3 -1
- package/dist/pipeline/pre-transaction-reservations.d.ts +3 -1
- package/dist/pipeline/pre-transaction-reservations.js +29 -9
- package/dist/rate-limit/bucket.d.ts +3 -3
- package/dist/rate-limit/bucket.js +14 -4
- package/dist/redis/cache-sync-bus.js +9 -3
- package/dist/schema-cli.js +12 -1
- package/dist/search/meilisearch-adapter.js +36 -9
- package/dist/stack/db.d.ts +2 -2
- package/dist/stack/test-stack.js +2 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/dist/testing/multipart-helper.d.ts +6 -0
- package/dist/testing/multipart-helper.js +1 -1
- package/dist/testing/zip-fixture.d.ts +1 -0
- package/dist/testing/zip-fixture.js +33 -0
- package/dist/ui-types/card-column-roles.js +4 -2
- package/dist/ui-types/index.d.ts +1 -1
- package/dist/ui-types/index.js +1 -1
- package/dist/upgrade-cli.js +46 -13
- package/package.json +7 -5
- package/src/changes.json +512 -8
|
@@ -5,7 +5,7 @@ export { createEscapeHatchProcessDedup, createEscapeHatchReporter, createEscapeH
|
|
|
5
5
|
export { getFallbackMeter, getFallbackProvider, getFallbackTracer } from "./fallback.js";
|
|
6
6
|
export { generateSpanId, generateTraceId } from "./ids.js";
|
|
7
7
|
export { buildMetricName, validateLabelKey, validateMetricName, } from "./metric-validator.js";
|
|
8
|
-
export { createMetricsHandle, createNoopMetricsHandle, createSafeMetricsHandle, createUnboundMetricsHandle, } from "./metrics-handle.js";
|
|
8
|
+
export { createMetricsForFactory, createMetricsHandle, createNoopMetricsHandle, createSafeMetricsHandle, createUnboundMetricsHandle, } from "./metrics-handle.js";
|
|
9
9
|
export { prometheusMetricsEnvSchema, resolveObservabilityWiring, } from "./metrics-wiring.js";
|
|
10
10
|
export { createNoopProvider } from "./noop-provider.js";
|
|
11
11
|
export { createPrometheusMeter, serializeOpenMetrics, } from "./prometheus-meter.js";
|
|
@@ -3,3 +3,4 @@ export declare function createMetricsHandle(meter: Meter, featureName: string):
|
|
|
3
3
|
export declare function createSafeMetricsHandle(meter: Meter, featureName: string): MetricsHandle;
|
|
4
4
|
export declare function createUnboundMetricsHandle(meter: Meter): MetricsHandle;
|
|
5
5
|
export declare function createNoopMetricsHandle(): MetricsHandle;
|
|
6
|
+
export declare function createMetricsForFactory(meter: Meter | undefined): (featureName: string) => MetricsHandle;
|
|
@@ -100,3 +100,10 @@ export function createNoopMetricsHandle() {
|
|
|
100
100
|
set(_name, _value, _labels) { },
|
|
101
101
|
};
|
|
102
102
|
}
|
|
103
|
+
// Shared factory behind ctx.metricsFor for handler and job contexts, so both
|
|
104
|
+
// keep the same no-op-on-unregistered contract (framework#1844).
|
|
105
|
+
export function createMetricsForFactory(meter) {
|
|
106
|
+
return meter
|
|
107
|
+
? (featureName) => createSafeMetricsHandle(meter, featureName)
|
|
108
|
+
: () => createNoopMetricsHandle();
|
|
109
|
+
}
|
|
@@ -62,15 +62,15 @@ export const STANDARD_METRIC_DEFS = [
|
|
|
62
62
|
// Prometheus collapses per-instance shards into last-writer-wins and
|
|
63
63
|
// per-instance lag is invisible to alerting.
|
|
64
64
|
{
|
|
65
|
-
name: "
|
|
65
|
+
name: "kumiko_event_consumer_lag_events",
|
|
66
66
|
type: "gauge",
|
|
67
|
-
description: "Number of
|
|
67
|
+
description: "Number of events between the consumer's cursor and the events head.",
|
|
68
68
|
labels: ["consumer", "instance_id"],
|
|
69
69
|
},
|
|
70
70
|
{
|
|
71
|
-
name: "
|
|
71
|
+
name: "kumiko_event_consumer_pending_gaps",
|
|
72
72
|
type: "gauge",
|
|
73
|
-
description: "Number of
|
|
73
|
+
description: "Number of unresolved id ranges below the consumer's cursor (ids not yet visible or proven rolled back).",
|
|
74
74
|
labels: ["consumer", "instance_id"],
|
|
75
75
|
},
|
|
76
76
|
{
|
|
@@ -52,10 +52,13 @@ export function createCascadeDeleteHook(registry, tables) {
|
|
|
52
52
|
// block the parent delete here instead of changing with it. The error names
|
|
53
53
|
// the parent only: the caller may delete the parent without reading the child.
|
|
54
54
|
if (strategy === OnDeleteStrategies.cascade || strategy === OnDeleteStrategies.setNull) {
|
|
55
|
-
const
|
|
56
|
-
|
|
55
|
+
const childEntity = registry.getEntity(relation.target);
|
|
56
|
+
const childLock = childEntity?.lockedWhen;
|
|
57
|
+
if (childLock !== undefined && payload.lockGuardBypassed !== true) {
|
|
58
|
+
// A soft-deleted child is invisible to the caller and must not block.
|
|
57
59
|
const children = await selectMany(db, targetTable, {
|
|
58
60
|
[relation.foreignKey]: payload.id,
|
|
61
|
+
...(childEntity?.softDelete === true && { isDeleted: false }),
|
|
59
62
|
});
|
|
60
63
|
if (children.some((child) => isRecordLocked(childLock, child))) {
|
|
61
64
|
throw new RecordLockedError({ entityName, entityId: payload.id });
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { requestContext, runWithWriteOrigin } from "../api/request-context.js";
|
|
2
2
|
import { transaction } from "../db/query.js";
|
|
3
|
-
import { InternalError, toWriteErrorInfo, writeFailure } from "../errors/index.js";
|
|
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
6
|
import { resolveDbSource } from "./dispatch-shared.js";
|
|
7
|
-
import { executeNestedWrite } from "./dispatch-write.js";
|
|
7
|
+
import { executeNestedWrite, executeWrite } from "./dispatch-write.js";
|
|
8
8
|
import { BatchRollback, isLifecycleResult, wrapToKumiko, } from "./dispatcher-utils.js";
|
|
9
9
|
import { reserveBeforeTransaction, runAsCommand, } from "./pre-transaction-reservations.js";
|
|
10
10
|
import { effectiveWriteOrigin, isPersonalDataGated, rootWriteOrigin } from "./write-origin.js";
|
|
@@ -22,15 +22,44 @@ function toReservedScope(reservations, index, cmd) {
|
|
|
22
22
|
const reservation = reservations.reservedCommands.get(index);
|
|
23
23
|
return reservation && { type: cmd.type, reservation };
|
|
24
24
|
}
|
|
25
|
+
function isPerCommandHandler(ctx, type) {
|
|
26
|
+
return ctx.registry.getWriteHandler(type)?.transaction === "perCommand";
|
|
27
|
+
}
|
|
25
28
|
async function runReservedCommand(scope, cmd, index, tx) {
|
|
26
29
|
const { ctx, user, inheritedOrigin, reservations, origins, afterCommitHooks } = scope;
|
|
27
30
|
const origin = effectiveWriteOrigin(rootWriteOrigin(ctx.registry, cmd.type, user), inheritedOrigin);
|
|
28
31
|
origins.push(origin);
|
|
29
32
|
const hookStart = afterCommitHooks.length;
|
|
30
|
-
|
|
33
|
+
// Nested-write children would run inline with the in-transaction scope, so a perCommand command skips the expansion.
|
|
34
|
+
const execute = isPerCommandHandler(ctx, cmd.type) ? executeWrite : executeNestedWrite;
|
|
35
|
+
const res = await runAsCommand(toReservedScope(reservations, index, cmd), () => runWithWriteOrigin(origin, () => execute(ctx, cmd.type, cmd.payload, user, origin, tx, afterCommitHooks)));
|
|
31
36
|
rewrapHooksWithOrigin(afterCommitHooks, hookStart, origin);
|
|
32
37
|
return res;
|
|
33
38
|
}
|
|
39
|
+
// Sequential fallback without an outer transaction. A failure result and a throw both end the
|
|
40
|
+
// batch, so both give the reserved capacity back.
|
|
41
|
+
async function runCommandsWithoutTransaction(scope, commands, results) {
|
|
42
|
+
try {
|
|
43
|
+
for (let i = 0; i < commands.length; i++) {
|
|
44
|
+
const cmd = commands[i];
|
|
45
|
+
if (!cmd)
|
|
46
|
+
continue;
|
|
47
|
+
const res = await runReservedCommand(scope, cmd, i, undefined);
|
|
48
|
+
results.push(res);
|
|
49
|
+
if (!res.isSuccess) {
|
|
50
|
+
// No tx means no rollback — but we still drop afterCommit hooks,
|
|
51
|
+
// matching the semantic "failure = side-effects don't fire".
|
|
52
|
+
await scope.reservations.releaseAll();
|
|
53
|
+
return { index: i, error: res.error };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
await scope.reservations.releaseAll();
|
|
59
|
+
throw e;
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
34
63
|
// Core batch logic extracted so write() and command() can reuse it
|
|
35
64
|
// (a single write = batch of one, running in its own transaction).
|
|
36
65
|
export async function runBatch(ctx, commands, user, requestId, inheritedOrigin) {
|
|
@@ -43,6 +72,30 @@ export async function runBatch(ctx, commands, user, requestId, inheritedOrigin)
|
|
|
43
72
|
const { signal: _signal, ...withoutSignal } = current;
|
|
44
73
|
return requestContext.run(withoutSignal, () => runBatchBody(ctx, commands, user, requestId, inheritedOrigin));
|
|
45
74
|
}
|
|
75
|
+
function perCommandBatchRejection(ctx, commands) {
|
|
76
|
+
const index = commands.findIndex((cmd) => isPerCommandHandler(ctx, cmd.type));
|
|
77
|
+
const command = commands[index];
|
|
78
|
+
if (!command || commands.length < 2)
|
|
79
|
+
return undefined;
|
|
80
|
+
return {
|
|
81
|
+
isSuccess: false,
|
|
82
|
+
error: toWriteErrorInfo(new UnprocessableError("per_command_handler_in_batch", {
|
|
83
|
+
message: `handler "${command.type}" runs without a transaction (transaction: "perCommand") and cannot be part of a multi-command batch`,
|
|
84
|
+
})),
|
|
85
|
+
failedIndex: index,
|
|
86
|
+
results: [],
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
async function checkBatchIdempotency(ctx, user, requestId) {
|
|
90
|
+
if (!requestId || !ctx.idempotency)
|
|
91
|
+
return {};
|
|
92
|
+
const checked = await ctx.idempotency.check(user.tenantId, user.id, requestId);
|
|
93
|
+
if (checked.status !== "cached")
|
|
94
|
+
return { token: checked.token };
|
|
95
|
+
const parsed = parseJsonSafe(checked.result, null);
|
|
96
|
+
// corrupted cache entry — treat as miss, let the request re-run
|
|
97
|
+
return parsed ? { cached: parsed } : {};
|
|
98
|
+
}
|
|
46
99
|
async function runBatchBody(ctx, commands, user, requestId, inheritedOrigin) {
|
|
47
100
|
const { idempotency, lifecycle, appContext: context } = ctx;
|
|
48
101
|
if (commands.length === 0) {
|
|
@@ -53,19 +106,10 @@ async function runBatchBody(ctx, commands, user, requestId, inheritedOrigin) {
|
|
|
53
106
|
// idempotencyToken is only set when we actually acquired the lock — the
|
|
54
107
|
// corrupted-cache fallthrough below leaves it unset, so finalize() skips
|
|
55
108
|
// store() rather than writing over an entry it never owned.
|
|
56
|
-
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const parsed = parseJsonSafe(checked.result, null);
|
|
61
|
-
if (parsed)
|
|
62
|
-
return parsed;
|
|
63
|
-
// corrupted cache entry — treat as miss, let the request re-run
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
idempotencyToken = checked.token;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
109
|
+
const idempotencyCheck = await checkBatchIdempotency(ctx, user, requestId);
|
|
110
|
+
if (idempotencyCheck.cached)
|
|
111
|
+
return idempotencyCheck.cached;
|
|
112
|
+
const idempotencyToken = idempotencyCheck.token;
|
|
69
113
|
// Hooks already ran on the plaintext results; what is returned and cached
|
|
70
114
|
// for retries must not carry writeOnly values.
|
|
71
115
|
const maskForClient = (result) => maskBatchResultForClient(ctx.registry, commands, result);
|
|
@@ -148,6 +192,10 @@ async function runBatchBody(ctx, commands, user, requestId, inheritedOrigin) {
|
|
|
148
192
|
return;
|
|
149
193
|
await runWithWriteOrigin(strictest, flushBatchHooksInner);
|
|
150
194
|
};
|
|
195
|
+
const perCommandRejection = perCommandBatchRejection(ctx, commands);
|
|
196
|
+
if (perCommandRejection)
|
|
197
|
+
return releaseOrFinalize(perCommandRejection, false);
|
|
198
|
+
const runsWithoutTransaction = commands.some((cmd) => isPerCommandHandler(ctx, cmd.type));
|
|
151
199
|
// Reserve before the transaction opens so no extra connection is held while the handler tx is
|
|
152
200
|
// open; the release below runs only after that tx has ended without committing.
|
|
153
201
|
const reservations = await reserveBeforeTransaction(ctx, commands, user, inheritedOrigin);
|
|
@@ -162,25 +210,24 @@ async function runBatchBody(ctx, commands, user, requestId, inheritedOrigin) {
|
|
|
162
210
|
// batch() opens its own outer transaction — needs the top-level
|
|
163
211
|
// connection's `.begin()` (TransactionSql exposes only `.savepoint()`).
|
|
164
212
|
const db = resolveDbSource(ctx, undefined);
|
|
165
|
-
if (!db) {
|
|
166
|
-
// Without a DB connection
|
|
213
|
+
if (!db || runsWithoutTransaction) {
|
|
214
|
+
// Without a DB connection (or for a perCommand handler, whose writes open their own transactions)
|
|
215
|
+
// there is no transaction to open. Fall back to
|
|
167
216
|
// sequential execution — useful for unit tests that don't touch the DB.
|
|
168
217
|
// Each command runs independently; a failure stops the batch.
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
// matching the semantic "failure = side-effects don't fire".
|
|
178
|
-
await reservations.releaseAll();
|
|
179
|
-
return finalize({ isSuccess: false, error: res.error, failedIndex: i, results });
|
|
180
|
-
}
|
|
218
|
+
const failed = await runCommandsWithoutTransaction({ ctx, user, inheritedOrigin, reservations, origins, afterCommitHooks }, commands, results);
|
|
219
|
+
if (failed) {
|
|
220
|
+
return finalize({
|
|
221
|
+
isSuccess: false,
|
|
222
|
+
error: failed.error,
|
|
223
|
+
failedIndex: failed.index,
|
|
224
|
+
results,
|
|
225
|
+
});
|
|
181
226
|
}
|
|
182
227
|
await flushAfterCommit();
|
|
183
|
-
|
|
228
|
+
// The inner commands of a perCommand handler already fired their own batch hooks.
|
|
229
|
+
if (!runsWithoutTransaction)
|
|
230
|
+
await flushBatchHooks();
|
|
184
231
|
return finalize({ isSuccess: true, results });
|
|
185
232
|
}
|
|
186
233
|
let transactionCallbackCompleted = false;
|
|
@@ -201,6 +248,7 @@ async function runBatchBody(ctx, commands, user, requestId, inheritedOrigin) {
|
|
|
201
248
|
}
|
|
202
249
|
catch (e) {
|
|
203
250
|
// Rollback or failed COMMIT: the transaction is over, so the reserved capacity goes back.
|
|
251
|
+
// The release is outcome-safe: it only gives back a reservation row the handler tx did not delete.
|
|
204
252
|
await reservations.releaseAll();
|
|
205
253
|
if (e instanceof BatchRollback) {
|
|
206
254
|
// Thrown inside the callback, so the tx rolled back. A 4xx is
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DbTx } from "../db/connection.js";
|
|
2
2
|
import type { SessionUser } from "../engine/types/index.js";
|
|
3
3
|
import { type DispatchContext, type WriteOrigin } from "./dispatch-shared.js";
|
|
4
|
-
|
|
4
|
+
import type { AfterCommitHook } from "./dispatcher-utils.js";
|
|
5
|
+
export declare function executeQuery(ctx: DispatchContext, type: string, payload: unknown, user: SessionUser, origin: WriteOrigin, tx?: DbTx, outerNotifySink?: AfterCommitHook[]): Promise<unknown>;
|
|
@@ -10,10 +10,10 @@ import { runInMemberReadOnlyTransaction } from "./member-read-only-transaction.j
|
|
|
10
10
|
// Standalone query execution — used by the public dispatcher.query() and
|
|
11
11
|
// by ctx.query/ctx.queryAs inside handlers. Runs the handler, applies
|
|
12
12
|
// field-level read filters for the given user, logs the event.
|
|
13
|
-
export async function executeQuery(ctx, type, payload, user, origin, tx) {
|
|
14
|
-
return runHandlerInstrumented(ctx, type, "query", user, () => executeQueryInner(ctx, type, payload, user, origin, tx));
|
|
13
|
+
export async function executeQuery(ctx, type, payload, user, origin, tx, outerNotifySink) {
|
|
14
|
+
return runHandlerInstrumented(ctx, type, "query", user, () => executeQueryInner(ctx, type, payload, user, origin, tx, outerNotifySink));
|
|
15
15
|
}
|
|
16
|
-
async function executeQueryInner(ctx, type, payload, user, origin, tx) {
|
|
16
|
+
async function executeQueryInner(ctx, type, payload, user, origin, tx, outerNotifySink) {
|
|
17
17
|
const { registry } = ctx;
|
|
18
18
|
const handler = registry.getQueryHandler(type);
|
|
19
19
|
if (!handler)
|
|
@@ -54,11 +54,11 @@ async function executeQueryInner(ctx, type, payload, user, origin, tx) {
|
|
|
54
54
|
// A resolved member (ctx.queryAsMember) runs in a Postgres READ ONLY transaction, not just the ctx surface below.
|
|
55
55
|
return isMemberResolutionPrincipal(user)
|
|
56
56
|
? runInMemberReadOnlyTransaction(ctx, tx, (readOnlyTx) => runQueryHandler(ctx, type, handler, parsed.data, includeDeleted, user, origin, readOnlyTx))
|
|
57
|
-
: runQueryHandler(ctx, type, handler, parsed.data, includeDeleted, user, origin, tx);
|
|
57
|
+
: runQueryHandler(ctx, type, handler, parsed.data, includeDeleted, user, origin, tx, outerNotifySink);
|
|
58
58
|
}
|
|
59
|
-
async function runQueryHandler(ctx, type, handler, payload, includeDeleted, user, origin, tx) {
|
|
59
|
+
async function runQueryHandler(ctx, type, handler, payload, includeDeleted, user, origin, tx, outerNotifySink) {
|
|
60
60
|
const { registry } = ctx;
|
|
61
|
-
const handlerContext = await buildHandlerContext(ctx, type, user, origin, tx, undefined, includeDeleted);
|
|
61
|
+
const handlerContext = await buildHandlerContext(ctx, type, user, origin, tx, undefined, includeDeleted, undefined, outerNotifySink);
|
|
62
62
|
let result = await handler.handler({ type, payload, user }, handlerContext);
|
|
63
63
|
// postQuery-Hooks: fire BEFORE field-access-filter so hooks see raw data
|
|
64
64
|
// and can merge custom-fields/computed-counts/tags/etc. Each hook is
|
|
@@ -6,7 +6,7 @@ import type { defineTransitions } from "../engine/state-machine.js";
|
|
|
6
6
|
import type { EffectiveFeaturesResolver } from "../engine/tier-resolver-extension.js";
|
|
7
7
|
import type { TenantId } from "../engine/types/identifiers.js";
|
|
8
8
|
import type { AppContext, HandlerContext, JobRunnerRef, PayloadRateLimitOption, RateLimitDeclaration, Registry, SessionUser, WriteResult } from "../engine/types/index.js";
|
|
9
|
-
import { type WriteErrorInfo } from "../errors/index.js";
|
|
9
|
+
import { InternalError, type WriteErrorInfo } from "../errors/index.js";
|
|
10
10
|
import { type EscapeHatchReportWindow, type getFallbackMeter, getFallbackTracer } from "../observability/index.js";
|
|
11
11
|
import { type AfterCommitHook } from "./dispatcher-utils.js";
|
|
12
12
|
import type { IdempotencyGuard } from "./idempotency.js";
|
|
@@ -53,7 +53,12 @@ export declare const DEFAULT_MEMBER_READ_TIMEOUT_MS = 10000;
|
|
|
53
53
|
export declare function resolveDbSource(ctx: DispatchContext, tx: DbTx | undefined): DbConnection | DbTx | undefined;
|
|
54
54
|
export declare function isMemberResolutionPrincipal(user: SessionUser): boolean;
|
|
55
55
|
export declare function applyMemberResolutionReadOnly(handlerContext: HandlerContext): HandlerContext;
|
|
56
|
-
export declare function
|
|
56
|
+
export declare function perCommandNestedDenied(targetType: string): InternalError;
|
|
57
|
+
export declare function denyTransactionBoundSurfaces(handlerContext: HandlerContext): HandlerContext;
|
|
58
|
+
export declare function applyBeforeTransactionReadOnly(handlerContext: HandlerContext): HandlerContext;
|
|
59
|
+
export type HandlerTransactionScope = "inTransaction" | "withoutTransaction" | "postCommit";
|
|
60
|
+
export declare function buildHandlerContext(ctx: DispatchContext, type: string, user: SessionUser, origin: WriteOrigin, tx?: DbTx, afterCommitHooks?: AfterCommitHook[], includeDeleted?: boolean, transactionScope?: HandlerTransactionScope, outerNotifySink?: AfterCommitHook[], readOnlyForTenant?: boolean): Promise<HandlerContext>;
|
|
61
|
+
export declare function createHandlerBoundDigest(digest: (value: string) => string, handlerName: string): (value: string) => string;
|
|
57
62
|
export declare function runHandlerInstrumented<T>(ctx: DispatchContext, type: string, operation: "query" | "write" | "stream", user: SessionUser, inner: () => Promise<T>): Promise<T>;
|
|
58
63
|
export declare function runStreamInstrumented<T>(ctx: DispatchContext, type: string, user: SessionUser, inner: () => AsyncGenerator<T>): AsyncGenerator<T>;
|
|
59
64
|
export declare function checkFeatureEnabled(ctx: DispatchContext, qualifiedHandler: string, tenantId: TenantId): Promise<import("../errors/index.js").FeatureDisabledError | undefined>;
|