@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.
Files changed (38) hide show
  1. package/package.json +3 -3
  2. package/src/api/__tests__/http-route-rate-limit.integration.test.ts +71 -0
  3. package/src/api/server.ts +1 -1
  4. package/src/db/__tests__/migrate-generator.test.ts +17 -0
  5. package/src/db/__tests__/money.test.ts +41 -5
  6. package/src/db/event-store-executor-read.ts +1 -1
  7. package/src/db/index.ts +1 -1
  8. package/src/db/migrate-generator.ts +13 -3
  9. package/src/db/money.ts +34 -4
  10. package/src/derivatives/__tests__/derivatives-context.integration.test.ts +73 -6
  11. package/src/derivatives/__tests__/derivatives-context.test.ts +9 -2
  12. package/src/derivatives/__tests__/variant-key.test.ts +2 -2
  13. package/src/derivatives/__tests__/variant-route.integration.test.ts +28 -0
  14. package/src/derivatives/derivatives-context.ts +7 -7
  15. package/src/derivatives/variant-key.ts +1 -1
  16. package/src/engine/__tests__/boot-validator.test.ts +140 -0
  17. package/src/engine/__tests__/embedded-derived.test.ts +35 -0
  18. package/src/engine/__tests__/engine.test.ts +20 -0
  19. package/src/engine/__tests__/schema-builder.test.ts +93 -0
  20. package/src/engine/boot-validator/entity-handler.ts +64 -5
  21. package/src/engine/boot-validator/screens.ts +57 -36
  22. package/src/engine/embedded-derived.ts +9 -1
  23. package/src/engine/schema-builder.ts +33 -23
  24. package/src/entrypoint/__tests__/split-deploy.integration.test.ts +37 -6
  25. package/src/errors/zod-bridge.ts +4 -9
  26. package/src/event-store/__tests__/perf.integration.test.ts +2 -11
  27. package/src/files/file-routes.ts +20 -6
  28. package/src/files/storage-tracking.ts +2 -1
  29. package/src/jobs/job-runner.ts +18 -9
  30. package/src/logging/__tests__/fallback-logger.test.ts +43 -0
  31. package/src/logging/utils.ts +14 -1
  32. package/src/observability/__tests__/metric-validator.test.ts +10 -2
  33. package/src/observability/__tests__/metrics-handle.test.ts +30 -0
  34. package/src/observability/metric-validator.ts +4 -3
  35. package/src/observability/metrics-handle.ts +24 -12
  36. package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +69 -5
  37. package/src/pipeline/dispatch-shared.ts +12 -9
  38. 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
- await crud.create(
129
- { label: `${event.payload.label}-outside-tx` },
130
- event.user,
131
- ctx.dbOutsideTransaction,
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
- // Propagate the request's AbortSignal so every TenantDb query
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
- const db = dbSource ? buildTenantScopedDb(dbSource) : undefined;
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 ? buildTenantScopedDb(outsideTxSource) : undefined;
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,
@@ -38,6 +38,7 @@ export {
38
38
  export type {
39
39
  BooleanFieldDef,
40
40
  DateFieldDef,
41
+ EmbeddedDerivedCellDef,
41
42
  EntityDefinition,
42
43
  FieldDefinition,
43
44
  FileFieldDef,