@cosmicdrift/kumiko-framework 0.387.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/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 +1 -0
- package/dist/api/client-error-route.js +8 -2
- package/dist/api/index.d.ts +3 -1
- package/dist/api/index.js +3 -1
- package/dist/api/request-context.d.ts +1 -1
- package/dist/api/request-context.js +1 -1
- 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 -1251
- package/dist/api/session-cookies.js +4 -4
- package/dist/api/sse-route.js +16 -12
- package/dist/crypto/owner-transition-rekey.d.ts +5 -1
- package/dist/crypto/owner-transition-rekey.js +1 -1
- 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 +21 -0
- package/dist/db/aggregate-sql.js +155 -0
- package/dist/db/closed-connection.d.ts +9 -0
- package/dist/db/closed-connection.js +95 -0
- 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 +99 -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.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 +48 -701
- 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 +395 -499
- 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/migrate-generator.js +71 -86
- package/dist/db/migrate-runner.js +4 -1
- package/dist/db/query.d.ts +2 -15
- package/dist/db/query.js +8 -761
- package/dist/db/reference-data.js +6 -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.js +39 -9
- 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/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/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/pii-retention.js +233 -155
- 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 +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 +12 -9
- 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.js +56 -96
- package/dist/engine/define-feature.js +9 -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/round2.js +25 -6
- 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 +1 -0
- 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 +9 -0
- package/dist/engine/feature-ast/patterns.js +43 -51
- package/dist/engine/feature-ast/render.js +48 -87
- package/dist/engine/feature-entity-handlers.js +3 -0
- package/dist/engine/form-derivations.js +18 -7
- package/dist/engine/handler-field-references.js +1 -1
- 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-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 +51 -14
- package/dist/engine/validate-projection-allowlist.js +2 -1
- package/dist/entrypoint/index.d.ts +4 -1
- package/dist/entrypoint/index.js +7 -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.js +10 -4
- package/dist/files/bind-pending-file-refs.js +3 -3
- package/dist/files/create-file-ref-from-bytes.js +4 -1
- package/dist/files/file-handle.js +2 -3
- package/dist/files/file-routes.js +11 -8
- package/dist/files/resolve-content-type.js +43 -0
- package/dist/files/upload-ledger.js +7 -0
- package/dist/i18n/required-surface-keys.js +182 -206
- 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 +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 +47 -657
- package/dist/observability/escape-hatch-report.d.ts +3 -0
- package/dist/observability/escape-hatch-report.js +12 -1
- package/dist/pipeline/cascade-handler.js +165 -138
- package/dist/pipeline/dispatch-query.js +10 -3
- package/dist/pipeline/dispatch-shared.d.ts +13 -3
- package/dist/pipeline/dispatch-shared.js +58 -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 +3 -5
- 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/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/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/upgrade-cli.js +149 -121
- package/package.json +4 -4
- package/src/changes.json +310 -0
- 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
|
@@ -92,6 +92,13 @@ function publishConfigWrite(ctx, type, user, result) {
|
|
|
92
92
|
const message = scope === ConfigScopes.system ? { scope: "all", key } : { tenantId: user.tenantId, key };
|
|
93
93
|
ctx.cacheSync?.publish(CACHE_SYNC_TOPICS.tenantConfig, message);
|
|
94
94
|
}
|
|
95
|
+
async function settleSaveFileRefs(handlerContext, result, user) {
|
|
96
|
+
const fileRefArgs = { registry: handlerContext.registry, result, user, db: handlerContext.db };
|
|
97
|
+
const fileRefRows = await loadSaveFileRefs(fileRefArgs);
|
|
98
|
+
assertFileRefsBelongToTenant({ ...fileRefArgs, fileRefRows });
|
|
99
|
+
await bindPendingFileRefs({ ...fileRefArgs, fileRefRows });
|
|
100
|
+
await disposeRemovedFileRefs(fileRefArgs);
|
|
101
|
+
}
|
|
95
102
|
// Runs lifecycle hooks for a handler result. inTransaction hooks fire NOW
|
|
96
103
|
// (they see the tx via ctx.db when batch/write opens a transaction).
|
|
97
104
|
// afterCommit hooks are queued into `afterCommitHooks` for the caller to
|
|
@@ -115,14 +122,12 @@ async function runLifecycle(ctx, type, data, handlerContext, user, origin, after
|
|
|
115
122
|
// keeps projection state consistent with what the hooks observe.
|
|
116
123
|
await runProjections(result, handlerContext.registry, runner);
|
|
117
124
|
if (result.kind === "save") {
|
|
118
|
-
|
|
119
|
-
const fileRefRows = await loadSaveFileRefs(fileRefArgs);
|
|
120
|
-
assertFileRefsBelongToTenant({ ...fileRefArgs, fileRefRows });
|
|
121
|
-
await bindPendingFileRefs({ ...fileRefArgs, fileRefRows });
|
|
122
|
-
await disposeRemovedFileRefs(fileRefArgs);
|
|
125
|
+
await settleSaveFileRefs(handlerContext, result, user);
|
|
123
126
|
await lifecycle.runPostSave(type, result, handlerContext, HookPhases.inTransaction);
|
|
124
127
|
afterCommitHooks.push(async () => {
|
|
125
|
-
const afterCommitContext = await buildHandlerContext(ctx, type, user, origin,
|
|
128
|
+
const afterCommitContext = await buildHandlerContext(ctx, type, user, origin, {
|
|
129
|
+
transactionScope: "postCommit",
|
|
130
|
+
});
|
|
126
131
|
await lifecycle.runPostSave(type, result, afterCommitContext, HookPhases.afterCommit);
|
|
127
132
|
});
|
|
128
133
|
}
|
|
@@ -130,7 +135,9 @@ async function runLifecycle(ctx, type, data, handlerContext, user, origin, after
|
|
|
130
135
|
await lifecycle.runPreDelete(type, result, handlerContext);
|
|
131
136
|
await lifecycle.runPostDelete(type, result, handlerContext, HookPhases.inTransaction);
|
|
132
137
|
afterCommitHooks.push(async () => {
|
|
133
|
-
const afterCommitContext = await buildHandlerContext(ctx, type, user, origin,
|
|
138
|
+
const afterCommitContext = await buildHandlerContext(ctx, type, user, origin, {
|
|
139
|
+
transactionScope: "postCommit",
|
|
140
|
+
});
|
|
134
141
|
await lifecycle.runPostDelete(type, result, afterCommitContext, HookPhases.afterCommit);
|
|
135
142
|
});
|
|
136
143
|
}
|
|
@@ -190,25 +197,7 @@ async function completeParentRow(ctx, parentEntityName, parentEntity, parentRow,
|
|
|
190
197
|
});
|
|
191
198
|
return stored ? { ...stored, ...parentRow } : parentRow;
|
|
192
199
|
}
|
|
193
|
-
|
|
194
|
-
//
|
|
195
|
-
// When a parent `:create` handler's payload carries values under keys
|
|
196
|
-
// declared as `hasMany` relations with `nestedWrite: true`, those values
|
|
197
|
-
// are expanded into child writes: parent first (so its new id exists),
|
|
198
|
-
// then each nested entry as a separate `<target>:create` write with the
|
|
199
|
-
// foreign key set by the framework — never taken from the client. All of
|
|
200
|
-
// this runs inside the caller's transaction, so a child failure rolls the
|
|
201
|
-
// parent (and any earlier children) back together.
|
|
202
|
-
//
|
|
203
|
-
// This wrapper is what runBatch calls, not executeWrite. Single writes
|
|
204
|
-
// (`dispatcher.write`) flow through runBatch as batch-of-one, so they get
|
|
205
|
-
// nested-expansion too for free. A batch with N heterogeneous commands
|
|
206
|
-
// can each independently carry nested-children — all still one TX.
|
|
207
|
-
export async function executeNestedWrite(ctx, type, payload, user, origin, tx, afterCommitHooks) {
|
|
208
|
-
const { registry } = ctx;
|
|
209
|
-
const nested = extractNestedSpecs(type, payload, registry);
|
|
210
|
-
if (!nested)
|
|
211
|
-
return executeWrite(ctx, type, payload, user, origin, tx, afterCommitHooks);
|
|
200
|
+
function collectNestedForeignKeyIssues(nested) {
|
|
212
201
|
// Pre-flight client-shape checks. Merge non-array issues (collected up
|
|
213
202
|
// front by extractNestedSpecs) with fk-injection issues into one error
|
|
214
203
|
// so the client sees every problem in a single round-trip.
|
|
@@ -230,39 +219,33 @@ export async function executeNestedWrite(ctx, type, payload, user, origin, tx, a
|
|
|
230
219
|
}
|
|
231
220
|
}
|
|
232
221
|
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const
|
|
260
|
-
if (typeof parentId !== "string") {
|
|
261
|
-
return writeFailure(new InternalError({
|
|
262
|
-
message: `nested-write: parent handler "${type}" returned no string "id" — cannot attach children`,
|
|
263
|
-
}));
|
|
264
|
-
}
|
|
265
|
-
// A custom create handler may return an existing foreign row; verify it before attaching children.
|
|
222
|
+
return issues;
|
|
223
|
+
}
|
|
224
|
+
// Handlers built on the CRUD executor return a SaveContext wrapper —
|
|
225
|
+
// `{ kind: "save", id, data: <row>, changes, previous, event, ... }`.
|
|
226
|
+
// The wrapper is load-bearing for batch-level hooks downstream (see
|
|
227
|
+
// flushBatchHooks), so we mutate in place: nested children land on the
|
|
228
|
+
// inner `data` (which mirrors the entity shape the client expects) while
|
|
229
|
+
// the wrapper keeps its SaveContext semantics intact for the lifecycle
|
|
230
|
+
// pipeline. For handlers that return a bare row (no wrapper), children
|
|
231
|
+
// land directly on that object.
|
|
232
|
+
//
|
|
233
|
+
// Hook-ordering note: per-entity postSave hooks already ran inside the
|
|
234
|
+
// parent's executeWrite call — they never saw `tasks`, which is
|
|
235
|
+
// the right semantic (postSave gets the entity's own columns, not
|
|
236
|
+
// synthetic relation keys). A future postSaveBatch subscriber that
|
|
237
|
+
// enumerates columns generically WOULD see `tasks`; no such subscriber
|
|
238
|
+
// exists today. If you add one that iterates `Object.keys(save.data)`,
|
|
239
|
+
// filter by `entity.fields` membership to stay correct.
|
|
240
|
+
// handler-Result.data is generic across all entity handlers; nested-
|
|
241
|
+
// write inspects the shape structurally.
|
|
242
|
+
function unwrapSaveRow(data) {
|
|
243
|
+
const wrapper = data; // @cast-boundary engine-payload
|
|
244
|
+
return (wrapper["data"] ?? wrapper); // @cast-boundary engine-payload
|
|
245
|
+
}
|
|
246
|
+
// A custom create handler may return an existing foreign row; verify it before attaching children.
|
|
247
|
+
async function rejectUnverifiedParent(ctx, type, parentRow, parentId, user, tx) {
|
|
248
|
+
const { registry } = ctx;
|
|
266
249
|
const parentEntityName = registry.getHandlerEntity(type);
|
|
267
250
|
const isCallerScoped = !registry.isHandlerSystemScoped(type);
|
|
268
251
|
const isParentForeign = isCallerScoped &&
|
|
@@ -275,45 +258,90 @@ export async function executeNestedWrite(ctx, type, payload, user, origin, tx, a
|
|
|
275
258
|
details: { reason: FrameworkReasons.fieldAccessDenied, handler: type },
|
|
276
259
|
}));
|
|
277
260
|
}
|
|
278
|
-
if (parentEntityName)
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
261
|
+
if (!parentEntityName)
|
|
262
|
+
return undefined;
|
|
263
|
+
const parentEntity = registry.getEntity(parentEntityName);
|
|
264
|
+
if (!parentEntity)
|
|
265
|
+
return undefined;
|
|
266
|
+
const ownershipRow = await completeParentRow(ctx, parentEntityName, parentEntity, parentRow, user, tx);
|
|
267
|
+
const deniedField = checkWriteFieldOwnership(parentEntity, ownershipRow, user);
|
|
268
|
+
if (!deniedField)
|
|
269
|
+
return undefined;
|
|
270
|
+
return writeFailure(new AccessDeniedError({
|
|
271
|
+
message: `nested-write: parent row ownership check failed on field "${deniedField}" of "${type}" — refusing to attach children to a parent row the caller does not own`,
|
|
272
|
+
i18nKey: "errors.access.fieldDenied",
|
|
273
|
+
details: {
|
|
274
|
+
reason: FrameworkReasons.fieldAccessDenied,
|
|
275
|
+
field: deniedField,
|
|
276
|
+
handler: type,
|
|
277
|
+
},
|
|
278
|
+
}));
|
|
279
|
+
}
|
|
280
|
+
async function writeNestedChildren(ctx, spec, parentId, user, origin, tx, afterCommitHooks) {
|
|
281
|
+
const { registry } = ctx;
|
|
282
|
+
const subRows = [];
|
|
283
|
+
for (let i = 0; i < spec.items.length; i++) {
|
|
284
|
+
const rawItem = spec.items[i];
|
|
285
|
+
const itemObj = (rawItem ?? {}); // @cast-boundary engine-payload
|
|
286
|
+
const subPayload = { ...itemObj, [spec.foreignKey]: parentId };
|
|
287
|
+
const subResult = await executeWrite(ctx, spec.subType, subPayload, user, origin, tx, afterCommitHooks);
|
|
288
|
+
if (!subResult.isSuccess) {
|
|
289
|
+
return {
|
|
290
|
+
kind: "failed",
|
|
291
|
+
failure: {
|
|
305
292
|
isSuccess: false,
|
|
306
293
|
error: prefixValidationPath(subResult.error, `${spec.key}.${i}`),
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
const subWrapper = subResult.data; // @cast-boundary engine-payload
|
|
310
|
-
const subRow = (subWrapper["data"] ?? subWrapper); // @cast-boundary engine-payload
|
|
311
|
-
const subEntityName = registry.getHandlerEntity(spec.subType);
|
|
312
|
-
const subEntity = subEntityName ? registry.getEntity(subEntityName) : undefined;
|
|
313
|
-
// parentRow is returned to the client; the child's own SaveContext keeps the plaintext.
|
|
314
|
-
subRows.push(subEntity ? maskWriteOnlyFields(subEntity, subRow) : subRow);
|
|
294
|
+
},
|
|
295
|
+
};
|
|
315
296
|
}
|
|
316
|
-
|
|
297
|
+
const subRow = unwrapSaveRow(subResult.data);
|
|
298
|
+
const subEntityName = registry.getHandlerEntity(spec.subType);
|
|
299
|
+
const subEntity = subEntityName ? registry.getEntity(subEntityName) : undefined;
|
|
300
|
+
// parentRow is returned to the client; the child's own SaveContext keeps the plaintext.
|
|
301
|
+
subRows.push(subEntity ? maskWriteOnlyFields(subEntity, subRow) : subRow);
|
|
302
|
+
}
|
|
303
|
+
return { kind: "written", rows: subRows };
|
|
304
|
+
}
|
|
305
|
+
// Nested-write orchestration (v1: depth=1, create-only, hasMany-only).
|
|
306
|
+
//
|
|
307
|
+
// When a parent `:create` handler's payload carries values under keys
|
|
308
|
+
// declared as `hasMany` relations with `nestedWrite: true`, those values
|
|
309
|
+
// are expanded into child writes: parent first (so its new id exists),
|
|
310
|
+
// then each nested entry as a separate `<target>:create` write with the
|
|
311
|
+
// foreign key set by the framework — never taken from the client. All of
|
|
312
|
+
// this runs inside the caller's transaction, so a child failure rolls the
|
|
313
|
+
// parent (and any earlier children) back together.
|
|
314
|
+
//
|
|
315
|
+
// This wrapper is what runBatch calls, not executeWrite. Single writes
|
|
316
|
+
// (`dispatcher.write`) flow through runBatch as batch-of-one, so they get
|
|
317
|
+
// nested-expansion too for free. A batch with N heterogeneous commands
|
|
318
|
+
// can each independently carry nested-children — all still one TX.
|
|
319
|
+
export async function executeNestedWrite(ctx, type, payload, user, origin, tx, afterCommitHooks) {
|
|
320
|
+
const nested = extractNestedSpecs(type, payload, ctx.registry);
|
|
321
|
+
if (!nested)
|
|
322
|
+
return executeWrite(ctx, type, payload, user, origin, tx, afterCommitHooks);
|
|
323
|
+
const issues = collectNestedForeignKeyIssues(nested);
|
|
324
|
+
if (issues.length > 0) {
|
|
325
|
+
return writeFailure(new ValidationError({ fields: issues }));
|
|
326
|
+
}
|
|
327
|
+
const parentResult = await executeWrite(ctx, type, nested.cleanPayload, user, origin, tx, afterCommitHooks);
|
|
328
|
+
if (!parentResult.isSuccess)
|
|
329
|
+
return parentResult;
|
|
330
|
+
const parentRow = unwrapSaveRow(parentResult.data);
|
|
331
|
+
const parentId = parentRow["id"];
|
|
332
|
+
if (typeof parentId !== "string") {
|
|
333
|
+
return writeFailure(new InternalError({
|
|
334
|
+
message: `nested-write: parent handler "${type}" returned no string "id" — cannot attach children`,
|
|
335
|
+
}));
|
|
336
|
+
}
|
|
337
|
+
const parentRejection = await rejectUnverifiedParent(ctx, type, parentRow, parentId, user, tx);
|
|
338
|
+
if (parentRejection)
|
|
339
|
+
return parentRejection;
|
|
340
|
+
for (const spec of nested.specs) {
|
|
341
|
+
const children = await writeNestedChildren(ctx, spec, parentId, user, origin, tx, afterCommitHooks);
|
|
342
|
+
if (children.kind === "failed")
|
|
343
|
+
return children.failure;
|
|
344
|
+
parentRow[spec.key] = children.rows;
|
|
317
345
|
}
|
|
318
346
|
return parentResult;
|
|
319
347
|
}
|
|
@@ -333,96 +361,130 @@ async function confirmReservation(reservation, tx, user) {
|
|
|
333
361
|
}
|
|
334
362
|
return undefined;
|
|
335
363
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
364
|
+
function failedStep(failure) {
|
|
365
|
+
return { kind: "failed", failure };
|
|
366
|
+
}
|
|
367
|
+
function resolveWriteEntry(ctx, type, user, tx) {
|
|
368
|
+
const handler = ctx.registry.getWriteHandler(type);
|
|
339
369
|
if (!handler)
|
|
340
|
-
return writeFailure(new NotFoundError("handler", type));
|
|
370
|
+
return failedStep(writeFailure(new NotFoundError("handler", type)));
|
|
341
371
|
// Defense in depth — covers runBatch and a spread-copied user that kept
|
|
342
372
|
// `origin`, beyond buildHandlerContext's own ctx.write replacement.
|
|
343
373
|
if (isMemberResolutionPrincipal(user)) {
|
|
344
|
-
return writeFailure(memberResolutionReadOnlyDenied());
|
|
374
|
+
return failedStep(writeFailure(memberResolutionReadOnlyDenied()));
|
|
345
375
|
}
|
|
346
376
|
const isPerCommand = handler.transaction === "perCommand";
|
|
347
377
|
if (isPerCommand && tx !== undefined) {
|
|
348
|
-
return writeFailure(perCommandNestedDenied(type));
|
|
378
|
+
return failedStep(writeFailure(perCommandNestedDenied(type)));
|
|
349
379
|
}
|
|
380
|
+
return { kind: "run", handler, isPerCommand };
|
|
381
|
+
}
|
|
382
|
+
async function runWriteGates(ctx, handler, type, payload, user) {
|
|
350
383
|
// A command the dispatcher ran pre-transaction phases for was already billed against the rate
|
|
351
384
|
// limits; the gates only run again against the in-transaction state.
|
|
352
385
|
const needsPreTransactionPhase = hasPreTransactionPhase(handler);
|
|
353
386
|
const reservation = needsPreTransactionPhase ? consumeReservation(type) : undefined;
|
|
354
387
|
const gates = await runPreHandlerGates(ctx, handler, type, payload, user, reservation ? "recheck" : "charge");
|
|
355
388
|
if (!gates.isSuccess)
|
|
356
|
-
return gates;
|
|
357
|
-
const parsedPayload = gates.payload;
|
|
389
|
+
return failedStep(gates);
|
|
390
|
+
const parsedPayload = reservation ? reservation.parsedPayload : gates.payload;
|
|
358
391
|
// A handler with a pre-transaction phase cannot run that phase from inside an open transaction;
|
|
359
392
|
// running it without would skip the reservation or the prepared data, so a nested ctx.write of
|
|
360
393
|
// such a handler is rejected instead.
|
|
361
394
|
if (needsPreTransactionPhase && !reservation) {
|
|
362
|
-
return writeFailure(new InternalError({
|
|
395
|
+
return failedStep(writeFailure(new InternalError({
|
|
363
396
|
message: `handler "${type}" has a pre-transaction phase (reserveBeforeTransaction/beforeTransaction) and cannot run as a nested write unless the top-level command ran it`,
|
|
364
|
-
}));
|
|
397
|
+
})));
|
|
365
398
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
const transitionGuardDb = handlerContext.systemDb
|
|
399
|
+
return { kind: "run", parsedPayload, reservation };
|
|
400
|
+
}
|
|
401
|
+
// Reads via the guard's own db handle — for r.systemScope() handlers
|
|
402
|
+
// handlerContext.db is fail-closed, so this reaches for
|
|
403
|
+
// systemDb.acknowledgeCrossTenant like any other framework-internal
|
|
404
|
+
// mechanism that touches a system-scoped handler's data.
|
|
405
|
+
function resolveTransitionGuardDb(handlerContext, type) {
|
|
406
|
+
return handlerContext.systemDb
|
|
375
407
|
? handlerContext.systemDb.acknowledgeCrossTenant(`auto transition guard for r.systemScope() write handler (${type})`)
|
|
376
408
|
: handlerContext.db;
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
continue;
|
|
403
|
-
// Skip guard for soft-deleted rows — they shouldn't be transitioning
|
|
404
|
-
// at all; a handler that wants to move a deleted row should use
|
|
405
|
-
// unsafeSkipTransitionGuard or restore first.
|
|
406
|
-
const rowAsRow = row; // @cast-boundary engine-payload
|
|
407
|
-
const isDeleted = rowAsRow["isDeleted"] ?? rowAsRow["is_deleted"];
|
|
408
|
-
if (entity.softDelete && isDeleted === true) {
|
|
409
|
-
continue;
|
|
410
|
-
}
|
|
411
|
-
const currentValue = row[fieldName] ??
|
|
412
|
-
row[toSnakeCase(fieldName)]; // @cast-boundary engine-bridge
|
|
413
|
-
guardTransition(getTransitions(ctx, { entityName, fieldName, map: transitionMap }), currentValue, newValue);
|
|
414
|
-
}
|
|
415
|
-
}
|
|
409
|
+
}
|
|
410
|
+
// Auto transition guard: if entity has transitions and handler doesn't skip it.
|
|
411
|
+
async function guardAutoTransitions(ctx, handler, type, parsedPayload, transitionGuardDb, tx) {
|
|
412
|
+
const entityName = ctx.registry.getHandlerEntity(type);
|
|
413
|
+
// skip: handler has no entity or opted out of the transition guard
|
|
414
|
+
if (!entityName || handler.unsafeSkipTransitionGuard)
|
|
415
|
+
return;
|
|
416
|
+
const entity = ctx.registry.getEntity(entityName);
|
|
417
|
+
// skip: entity declares no transitions, or there is no db to read the current value from
|
|
418
|
+
if (!entity?.transitions || !transitionGuardDb)
|
|
419
|
+
return;
|
|
420
|
+
const parsedData = parsedPayload; // @cast-boundary engine-payload
|
|
421
|
+
const changes = parsedData["changes"] ?? parsedData; // @cast-boundary engine-payload
|
|
422
|
+
const id = parsedData["id"] ?? undefined; // @cast-boundary engine-payload
|
|
423
|
+
for (const [fieldName, transitionMap] of Object.entries(entity.transitions)) {
|
|
424
|
+
await guardFieldTransition(ctx, {
|
|
425
|
+
entityName,
|
|
426
|
+
entity,
|
|
427
|
+
fieldName,
|
|
428
|
+
transitionMap,
|
|
429
|
+
changes,
|
|
430
|
+
id,
|
|
431
|
+
transitionGuardDb,
|
|
432
|
+
tx,
|
|
433
|
+
});
|
|
416
434
|
}
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
//
|
|
422
|
-
|
|
435
|
+
}
|
|
436
|
+
async function guardFieldTransition(ctx, args) {
|
|
437
|
+
const { entityName, entity, fieldName, transitionMap, changes, id, transitionGuardDb, tx } = args;
|
|
438
|
+
const newValue = changes[fieldName]; // @cast-boundary engine-bridge
|
|
439
|
+
// skip: field is not part of this write
|
|
440
|
+
if (!newValue || !id)
|
|
441
|
+
return;
|
|
442
|
+
const table = getTable(ctx, entityName);
|
|
443
|
+
// skip: entity has no table to read the current value from
|
|
444
|
+
if (!table)
|
|
445
|
+
return;
|
|
446
|
+
// SELECT FOR UPDATE inside the surrounding transaction — locks the
|
|
447
|
+
// row so a concurrent handler can't mutate `status` between our
|
|
448
|
+
// guard check and the handler's UPDATE. Without this lock the guard
|
|
449
|
+
// can false-pass; optimistic locking would catch it later, but with
|
|
450
|
+
// a less specific error. Falls back to a plain SELECT if no tx is
|
|
451
|
+
// active (tests without a DB connection).
|
|
452
|
+
const tableName = asEntityTableMeta(table)?.tableName ?? "";
|
|
453
|
+
const rows = tx
|
|
454
|
+
? await selectRowForUpdateById(tenantDbRunner(transitionGuardDb), tableName, id)
|
|
455
|
+
: await selectMany(transitionGuardDb, table, { id });
|
|
456
|
+
const row = rows[0];
|
|
457
|
+
// skip: no stored row to transition from
|
|
458
|
+
if (!row)
|
|
459
|
+
return;
|
|
460
|
+
// Skip guard for soft-deleted rows — they shouldn't be transitioning
|
|
461
|
+
// at all; a handler that wants to move a deleted row should use
|
|
462
|
+
// unsafeSkipTransitionGuard or restore first.
|
|
463
|
+
const rowAsRow = row; // @cast-boundary engine-payload
|
|
464
|
+
const isDeleted = rowAsRow["isDeleted"] ?? rowAsRow["is_deleted"];
|
|
465
|
+
// skip: soft-deleted row
|
|
466
|
+
if (entity.softDelete && isDeleted === true) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const currentValue = row[fieldName] ??
|
|
470
|
+
row[toSnakeCase(fieldName)]; // @cast-boundary engine-bridge
|
|
471
|
+
guardTransition(getTransitions(ctx, { entityName, fieldName, map: transitionMap }), currentValue, newValue);
|
|
472
|
+
}
|
|
473
|
+
// The handler itself plus the lifecycle pipeline run under the same
|
|
474
|
+
// try-wrapper: any KumikoError bubbles up as a typed WriteErrorInfo, any
|
|
475
|
+
// other throw gets wrapped in InternalError so the Prod contract holds
|
|
476
|
+
// ("unexpected throw → 500 with sanitized body"). We intentionally do NOT
|
|
477
|
+
// catch further out (runBatch still sees these as exceptions via
|
|
478
|
+
// writeFailure, not via a rethrow) so batches roll back naturally.
|
|
479
|
+
async function invokeWriteHandler(handler, type, gated, user, handlerContext) {
|
|
423
480
|
let result;
|
|
424
481
|
try {
|
|
425
|
-
const event = {
|
|
482
|
+
const event = {
|
|
483
|
+
type,
|
|
484
|
+
payload: gated.parsedPayload,
|
|
485
|
+
user,
|
|
486
|
+
prepared: gated.reservation?.prepared,
|
|
487
|
+
};
|
|
426
488
|
result = await handler.handler(event, handlerContext);
|
|
427
489
|
}
|
|
428
490
|
catch (e) {
|
|
@@ -441,41 +503,86 @@ async function executeWriteInner(ctx, type, payload, user, origin, tx, afterComm
|
|
|
441
503
|
`Use defineWriteHandler() or wrap the return as { isSuccess: true as const, data: ... }.`,
|
|
442
504
|
}));
|
|
443
505
|
}
|
|
444
|
-
|
|
445
|
-
|
|
506
|
+
return result;
|
|
507
|
+
}
|
|
508
|
+
async function runWriteLifecycle(ctx, args) {
|
|
509
|
+
try {
|
|
510
|
+
const runner = resolveDbSource(ctx, args.tx);
|
|
511
|
+
await runLifecycle(ctx, args.type, args.result.data, args.handlerContext, args.user, args.origin, args.afterCommitHooks, runner);
|
|
512
|
+
}
|
|
513
|
+
catch (e) {
|
|
514
|
+
return writeFailure(wrapToKumiko(e));
|
|
515
|
+
}
|
|
516
|
+
return undefined;
|
|
517
|
+
}
|
|
518
|
+
function scheduleWriteAfterCommit(ctx, type, user, parsedPayload, result, afterCommitHooks) {
|
|
519
|
+
const { jobRunner } = ctx;
|
|
520
|
+
// jobRunner has external side-effects (BullMQ enqueue) — must NOT
|
|
521
|
+
// fire for rolled-back writes. Defer to afterCommit.
|
|
522
|
+
if (jobRunner) {
|
|
523
|
+
const eventData = (parsedPayload ?? {}); // @cast-boundary engine-payload
|
|
524
|
+
afterCommitHooks.push(() => jobRunner.handleEvent(type, eventData, user));
|
|
525
|
+
}
|
|
526
|
+
invalidateTenantTimezoneCache(ctx, type, user, result);
|
|
527
|
+
// Again after commit: a query landing between the drop above and the
|
|
528
|
+
// commit would read the pre-write row (tx not yet visible) and
|
|
529
|
+
// repopulate the cache with the stale value for a full TTL.
|
|
530
|
+
afterCommitHooks.push(async () => {
|
|
531
|
+
invalidateTenantTimezoneCache(ctx, type, user, result);
|
|
532
|
+
publishConfigWrite(ctx, type, user, result);
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
async function settleSuccessfulWrite(ctx, args) {
|
|
536
|
+
if (args.reservation) {
|
|
537
|
+
const confirmFailure = await confirmReservation(args.reservation, args.tx, args.user);
|
|
446
538
|
if (confirmFailure)
|
|
447
539
|
return confirmFailure;
|
|
448
540
|
}
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
await runLifecycle(ctx, type, result.data, handlerContext, user, origin, afterCommitHooks, runner);
|
|
455
|
-
}
|
|
456
|
-
catch (e) {
|
|
457
|
-
return writeFailure(wrapToKumiko(e));
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
// jobRunner has external side-effects (BullMQ enqueue) — must NOT
|
|
461
|
-
// fire for rolled-back writes. Defer to afterCommit.
|
|
462
|
-
if (jobRunner) {
|
|
463
|
-
const eventData = (parsedPayload ?? {}); // @cast-boundary engine-payload
|
|
464
|
-
afterCommitHooks.push(() => jobRunner.handleEvent(type, eventData, user));
|
|
465
|
-
}
|
|
466
|
-
invalidateTenantTimezoneCache(ctx, type, user, result);
|
|
467
|
-
// Again after commit: a query landing between the drop above and the
|
|
468
|
-
// commit would read the pre-write row (tx not yet visible) and
|
|
469
|
-
// repopulate the cache with the stale value for a full TTL.
|
|
470
|
-
afterCommitHooks.push(async () => {
|
|
471
|
-
invalidateTenantTimezoneCache(ctx, type, user, result);
|
|
472
|
-
publishConfigWrite(ctx, type, user, result);
|
|
473
|
-
});
|
|
541
|
+
// A perCommand handler's writes ran as their own commands; its result carries no SaveContext of its own to project.
|
|
542
|
+
if (!args.isPerCommand) {
|
|
543
|
+
const lifecycleFailure = await runWriteLifecycle(ctx, args);
|
|
544
|
+
if (lifecycleFailure)
|
|
545
|
+
return lifecycleFailure;
|
|
474
546
|
}
|
|
547
|
+
scheduleWriteAfterCommit(ctx, args.type, args.user, args.parsedPayload, args.result, args.afterCommitHooks);
|
|
548
|
+
return undefined;
|
|
549
|
+
}
|
|
550
|
+
async function executeWriteInner(ctx, type, payload, user, origin, tx, afterCommitHooks) {
|
|
551
|
+
const entry = resolveWriteEntry(ctx, type, user, tx);
|
|
552
|
+
if (entry.kind === "failed")
|
|
553
|
+
return entry.failure;
|
|
554
|
+
const { handler, isPerCommand } = entry;
|
|
555
|
+
const gated = await runWriteGates(ctx, handler, type, payload, user);
|
|
556
|
+
if (gated.kind === "failed")
|
|
557
|
+
return gated.failure;
|
|
558
|
+
// Without a database there is no transaction either, but ordinary handlers keep the in-transaction nested-write semantics.
|
|
559
|
+
const handlerContext = await buildHandlerContext(ctx, type, user, origin, {
|
|
560
|
+
tx,
|
|
561
|
+
afterCommitHooks,
|
|
562
|
+
transactionScope: isPerCommand ? "withoutTransaction" : "inTransaction",
|
|
563
|
+
});
|
|
564
|
+
const transitionGuardDb = resolveTransitionGuardDb(handlerContext, type);
|
|
565
|
+
await guardAutoTransitions(ctx, handler, type, gated.parsedPayload, transitionGuardDb, tx);
|
|
566
|
+
const result = await invokeWriteHandler(handler, type, gated, user, handlerContext);
|
|
567
|
+
if (!result.isSuccess)
|
|
568
|
+
return result;
|
|
569
|
+
const settleFailure = await settleSuccessfulWrite(ctx, {
|
|
570
|
+
type,
|
|
571
|
+
user,
|
|
572
|
+
origin,
|
|
573
|
+
tx,
|
|
574
|
+
afterCommitHooks,
|
|
575
|
+
parsedPayload: gated.parsedPayload,
|
|
576
|
+
reservation: gated.reservation,
|
|
577
|
+
isPerCommand,
|
|
578
|
+
handlerContext,
|
|
579
|
+
result,
|
|
580
|
+
});
|
|
581
|
+
if (settleFailure)
|
|
582
|
+
return settleFailure;
|
|
475
583
|
// Response-guard: block Secret<> leaks in write responses (SaveContext
|
|
476
584
|
// data / previous / changes). Feature code that fed a plaintext through
|
|
477
585
|
// to the return payload fails here instead of hitting the client.
|
|
478
|
-
|
|
479
|
-
assertNoSecretLeak(result.data);
|
|
586
|
+
assertNoSecretLeak(result.data);
|
|
480
587
|
return result;
|
|
481
588
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { advanceConsumerPastEventReturning, removePendingGapReturning, selectConsumerForUpdate, selectSnapshotXmax, updateConsumerStatusReturning, } from "../db/queries/event-consumer.js";
|
|
2
2
|
import { selectNextEventIdAfter, selectSmallestVisibleIdInRanges, } from "../db/queries/event-store.js";
|
|
3
|
-
import { coerceRow,
|
|
3
|
+
import { coerceRow, runInNewTransaction, runInSavepointIfSupported, runnerCanBeginTransaction, selectMany, } from "../db/query.js";
|
|
4
|
+
import { extractTableInfo } from "../db/table-info.js";
|
|
4
5
|
import { getEventsHighWaterMark } from "../event-store/index.js";
|
|
5
6
|
import { eventConsumerStateTable, SHARED_INSTANCE_SENTINEL, } from "./event-consumer-state.js";
|
|
6
7
|
import { mergeContiguousGapRanges, rangeContainsId, splitRangeExcludingIds, toIdRanges, } from "./pending-gap-ranges.js";
|
|
@@ -3,7 +3,8 @@ import { Temporal } from "@cosmicdrift/kumiko-types/temporal";
|
|
|
3
3
|
import { requestContext } from "../api/request-context.js";
|
|
4
4
|
import { insertConsumerIfAbsent, markConsumerProcessing, rearmDeadConsumer, selectConsumerForUpdate, selectConsumerForUpdateSkipLocked, selectProvablyIdleConsumerPairs, updateConsumerDeliveryOutcome, } from "../db/queries/event-consumer.js";
|
|
5
5
|
import { selectEventsHeadId, selectPendingAndNewEventRows, } from "../db/queries/event-store.js";
|
|
6
|
-
import { coerceRow
|
|
6
|
+
import { coerceRow } from "../db/query.js";
|
|
7
|
+
import { extractTableInfo } from "../db/table-info.js";
|
|
7
8
|
import { qnScope } from "../engine/qualified-name.js";
|
|
8
9
|
import { eventsTable, toStoredEvent as rowToStoredEvent, } from "../event-store/index.js";
|
|
9
10
|
import { emitDispatcherError, emitEventConsumerLag, emitEventConsumerPendingGaps, getFallbackMeter, } from "../observability/index.js";
|
|
@@ -30,10 +31,6 @@ const UNPARSEABLE_STORED_WRITE_ORIGIN = {
|
|
|
30
31
|
export async function acquireConsumerState(tx, name, instanceId, rearmCooldownMs, maxRearmCount) {
|
|
31
32
|
const rawState = await selectConsumerForUpdateSkipLocked(tx, name, instanceId);
|
|
32
33
|
if (!rawState) {
|
|
33
|
-
return { state: null, skip: "not_registered" };
|
|
34
|
-
}
|
|
35
|
-
const state = coerceRow(rawState, extractTableInfo(eventConsumerStateTable));
|
|
36
|
-
if (!state) {
|
|
37
34
|
// Either the row never existed (no pre-reg, no ensureRegistered) or
|
|
38
35
|
// another instance currently holds the lock with SKIP LOCKED filtering
|
|
39
36
|
// us out. We can't distinguish here in a single query, so return
|
|
@@ -42,6 +39,7 @@ export async function acquireConsumerState(tx, name, instanceId, rearmCooldownMs
|
|
|
42
39
|
// this path is never taken.
|
|
43
40
|
return { state: null, skip: "not_registered" };
|
|
44
41
|
}
|
|
42
|
+
const state = coerceRow(rawState, extractTableInfo(eventConsumerStateTable));
|
|
45
43
|
if (state.status === ConsumerStatuses.disabled)
|
|
46
44
|
return { state: null, skip: "disabled" };
|
|
47
45
|
if (state.status === ConsumerStatuses.dead) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { TenantId } from "../engine/types/identifiers.ts";
|
|
2
|
+
import type { MemberReader, SessionUser } from "../engine/types/index.ts";
|
|
3
|
+
import type { HandlerContextScope } from "./handler-context-scope.ts";
|
|
4
|
+
export declare function buildAuthFacet(scope: HandlerContextScope): {
|
|
5
|
+
resolveAuthClaims: (claimsUser: SessionUser) => Promise<Record<string, unknown>>;
|
|
6
|
+
resolveActiveMembership: (userId: string, tenantId: TenantId) => Promise<import("@cosmicdrift/kumiko-types/handlers").ActiveMembershipResult>;
|
|
7
|
+
queryAsMember: MemberReader;
|
|
8
|
+
hasFeature: (featureName: string) => Promise<boolean>;
|
|
9
|
+
};
|