@cosmicdrift/kumiko-framework 0.193.1 → 0.195.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/package.json +3 -3
- package/src/api/__tests__/http-route-rate-limit.integration.test.ts +71 -0
- package/src/api/server.ts +1 -1
- package/src/db/__tests__/migrate-generator.test.ts +17 -0
- package/src/db/__tests__/money.test.ts +41 -5
- package/src/db/event-store-executor-read.ts +1 -1
- package/src/db/index.ts +1 -1
- package/src/db/migrate-generator.ts +13 -3
- package/src/db/money.ts +34 -4
- package/src/derivatives/__tests__/derivatives-context.integration.test.ts +73 -6
- package/src/derivatives/__tests__/derivatives-context.test.ts +9 -2
- package/src/derivatives/__tests__/variant-key.test.ts +2 -2
- package/src/derivatives/__tests__/variant-route.integration.test.ts +28 -0
- package/src/derivatives/derivatives-context.ts +7 -7
- package/src/derivatives/variant-key.ts +1 -1
- package/src/engine/__tests__/boot-validator.test.ts +140 -0
- package/src/engine/__tests__/embedded-derived.test.ts +35 -0
- package/src/engine/__tests__/engine.test.ts +20 -0
- package/src/engine/__tests__/schema-builder.test.ts +93 -0
- package/src/engine/boot-validator/entity-handler.ts +64 -5
- package/src/engine/boot-validator/screens.ts +57 -36
- package/src/engine/embedded-derived.ts +9 -1
- package/src/engine/schema-builder.ts +33 -23
- package/src/entrypoint/__tests__/split-deploy.integration.test.ts +37 -6
- package/src/errors/zod-bridge.ts +4 -9
- package/src/event-store/__tests__/perf.integration.test.ts +2 -11
- package/src/files/file-routes.ts +20 -6
- package/src/files/storage-tracking.ts +2 -1
- package/src/jobs/job-runner.ts +18 -9
- package/src/logging/__tests__/fallback-logger.test.ts +43 -0
- package/src/logging/utils.ts +14 -1
- package/src/observability/__tests__/metric-validator.test.ts +10 -2
- package/src/observability/__tests__/metrics-handle.test.ts +30 -0
- package/src/observability/metric-validator.ts +4 -3
- package/src/observability/metrics-handle.ts +24 -12
- package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +69 -5
- package/src/pipeline/dispatch-shared.ts +12 -9
- package/src/ui-types/index.ts +1 -0
|
@@ -125,11 +125,11 @@ const bridgeFeature = defineFeature("ctxbridge", (r) => {
|
|
|
125
125
|
async (event, ctx) => {
|
|
126
126
|
const crud = createEventStoreExecutor(bagTable, bagEntity, { entityName: "bag" });
|
|
127
127
|
await crud.create({ label: `${event.payload.label}-inside-tx` }, event.user, ctx.db);
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
);
|
|
128
|
+
const outsideTx = ctx.dbOutsideTransaction;
|
|
129
|
+
if (!outsideTx) {
|
|
130
|
+
throw new Error("bag:create-outside-tx-then-fail requires ctx.dbOutsideTransaction");
|
|
131
|
+
}
|
|
132
|
+
await crud.create({ label: `${event.payload.label}-outside-tx` }, event.user, outsideTx);
|
|
133
133
|
return writeFailure(new UnprocessableError("intentional_failure"));
|
|
134
134
|
},
|
|
135
135
|
{ access: { roles: ["Admin"] } },
|
|
@@ -149,6 +149,39 @@ const bridgeFeature = defineFeature("ctxbridge", (r) => {
|
|
|
149
149
|
{ access: { roles: ["Admin"] } },
|
|
150
150
|
);
|
|
151
151
|
|
|
152
|
+
// Records whether ctx.db threw (should, once the request signal is
|
|
153
|
+
// aborted) and whether the ctx.dbOutsideTransaction insert still landed
|
|
154
|
+
// (should — durability writes must survive a client disconnect).
|
|
155
|
+
//
|
|
156
|
+
// The throwing side reads via ctx.db.selectMany, not the event-store
|
|
157
|
+
// executor's create() — the latter writes through db.raw (bypassing
|
|
158
|
+
// TenantDb's withDbSpan/signal check entirely), so it wouldn't exercise
|
|
159
|
+
// the signal wiring this test is meant to prove. insertOne isn't an
|
|
160
|
+
// option either: bagTable is executor-managed (WritableTable rejects its
|
|
161
|
+
// EXECUTOR_ONLY brand) — direct writes would drift it past its event
|
|
162
|
+
// stream. selectMany has no such restriction (reads keep the plain
|
|
163
|
+
// SchemaTable param) and still goes through the same signal check.
|
|
164
|
+
r.writeHandler(
|
|
165
|
+
"bag:create-signal-probe",
|
|
166
|
+
z.object({ label: z.string() }),
|
|
167
|
+
async (event, ctx) => {
|
|
168
|
+
const crud = createEventStoreExecutor(bagTable, bagEntity, { entityName: "bag" });
|
|
169
|
+
let dbThrewAbortError = false;
|
|
170
|
+
try {
|
|
171
|
+
await ctx.db?.selectMany(bagTable, {});
|
|
172
|
+
} catch (err) {
|
|
173
|
+
dbThrewAbortError = err instanceof Error && err.name === "AbortError";
|
|
174
|
+
}
|
|
175
|
+
const outsideTx = ctx.dbOutsideTransaction;
|
|
176
|
+
if (!outsideTx) {
|
|
177
|
+
throw new Error("bag:create-signal-probe requires ctx.dbOutsideTransaction");
|
|
178
|
+
}
|
|
179
|
+
await crud.create({ label: `${event.payload.label}-outside-tx` }, event.user, outsideTx);
|
|
180
|
+
return { isSuccess: true as const, data: { dbThrewAbortError } };
|
|
181
|
+
},
|
|
182
|
+
{ access: { roles: ["Admin"] } },
|
|
183
|
+
);
|
|
184
|
+
|
|
152
185
|
// afterCommit hook on bag — fires once per outer commit.
|
|
153
186
|
r.hook("postSave", { allOf: bag }, async (result) => {
|
|
154
187
|
afterCommitLog.push(`bag:${result.data["label"]}`);
|
|
@@ -266,4 +299,35 @@ describe("ctx.dbOutsideTransaction", () => {
|
|
|
266
299
|
const labels = (bags as Array<Record<string, unknown>>).map((row) => row["label"]);
|
|
267
300
|
expect(labels).toEqual(["probe-outside-tx"]);
|
|
268
301
|
});
|
|
302
|
+
|
|
303
|
+
test("an already-aborted request signal fails ctx.db but not ctx.dbOutsideTransaction", async () => {
|
|
304
|
+
const controller = new AbortController();
|
|
305
|
+
controller.abort();
|
|
306
|
+
const token = await stack.jwt.sign(admin);
|
|
307
|
+
|
|
308
|
+
const res = await stack.app.request(
|
|
309
|
+
new Request("http://test.local/api/write", {
|
|
310
|
+
method: "POST",
|
|
311
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
|
|
312
|
+
body: JSON.stringify({
|
|
313
|
+
type: "ctxbridge:write:bag:create-signal-probe",
|
|
314
|
+
payload: { label: "signal-probe" },
|
|
315
|
+
}),
|
|
316
|
+
signal: controller.signal,
|
|
317
|
+
}),
|
|
318
|
+
);
|
|
319
|
+
|
|
320
|
+
const body = (await res.json()) as {
|
|
321
|
+
isSuccess: boolean;
|
|
322
|
+
data?: { dbThrewAbortError: boolean };
|
|
323
|
+
};
|
|
324
|
+
expect(body.isSuccess).toBe(true);
|
|
325
|
+
expect(body.data?.dbThrewAbortError).toBe(true);
|
|
326
|
+
|
|
327
|
+
// Only the outside-tx insert landed — ctx.db's insert threw before it
|
|
328
|
+
// could write, and there was nothing to roll back for it.
|
|
329
|
+
const bags = await selectMany(stack.db, bagTable);
|
|
330
|
+
const labels = (bags as Array<Record<string, unknown>>).map((row) => row["label"]);
|
|
331
|
+
expect(labels).toEqual(["signal-probe-outside-tx"]);
|
|
332
|
+
});
|
|
269
333
|
});
|
|
@@ -170,24 +170,27 @@ export async function buildHandlerContext(
|
|
|
170
170
|
// but at this point we're the root of the pipeline — cast is safe.
|
|
171
171
|
const dbSource = resolveDbSource(ctx, tx);
|
|
172
172
|
const reqCtx = requestContext.get();
|
|
173
|
-
const buildTenantScopedDb = (source: DbConnection | DbTx) =>
|
|
173
|
+
const buildTenantScopedDb = (source: DbConnection | DbTx, signal: AbortSignal | undefined) =>
|
|
174
174
|
createTenantDb(
|
|
175
175
|
source,
|
|
176
176
|
user.tenantId,
|
|
177
177
|
isSystem ? "system" : "tenant",
|
|
178
178
|
context.tracer,
|
|
179
179
|
context.meter,
|
|
180
|
-
|
|
181
|
-
// throws when the client has disconnected — handlers with many
|
|
182
|
-
// sequential queries skip the rest of the chain instead of
|
|
183
|
-
// burning DB-CPU for results no one reads.
|
|
184
|
-
reqCtx?.signal,
|
|
180
|
+
signal,
|
|
185
181
|
);
|
|
186
|
-
|
|
182
|
+
// Propagate the request's AbortSignal so every TenantDb query throws when
|
|
183
|
+
// the client has disconnected — handlers with many sequential queries skip
|
|
184
|
+
// the rest of the chain instead of burning DB-CPU for results no one reads.
|
|
185
|
+
const db = dbSource ? buildTenantScopedDb(dbSource, reqCtx?.signal) : undefined;
|
|
187
186
|
// Unbound pool, tenant-scoped like `db` but never tx-bound — writes
|
|
188
|
-
// through it survive a rollback of the handler's own transaction.
|
|
187
|
+
// through it survive a rollback of the handler's own transaction. No
|
|
188
|
+
// AbortSignal here: a client disconnect must not abort a durability write
|
|
189
|
+
// that is meant to outlive the request.
|
|
189
190
|
const outsideTxSource = resolveDbSource(ctx, undefined);
|
|
190
|
-
const dbOutsideTransaction = outsideTxSource
|
|
191
|
+
const dbOutsideTransaction = outsideTxSource
|
|
192
|
+
? buildTenantScopedDb(outsideTxSource, undefined)
|
|
193
|
+
: undefined;
|
|
191
194
|
const log = context.log?.child({
|
|
192
195
|
handler: type,
|
|
193
196
|
tenantId: user.tenantId,
|