@cosmicdrift/kumiko-framework 0.304.0 → 0.306.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 (92) hide show
  1. package/package.json +4 -4
  2. package/src/api/__tests__/extra-route-rejection.test.ts +38 -0
  3. package/src/api/__tests__/extra-routes.integration.test.ts +30 -0
  4. package/src/api/__tests__/server-boot-guards.test.ts +1 -0
  5. package/src/api/__tests__/server-error-logging.test.ts +104 -0
  6. package/src/api/api-constants.ts +13 -0
  7. package/src/api/extra-route.ts +33 -4
  8. package/src/api/index.ts +1 -0
  9. package/src/api/request-context.ts +5 -4
  10. package/src/api/routes.ts +26 -1
  11. package/src/api/server.ts +8 -2
  12. package/src/bun-db/__tests__/closed-connection-retry.integration.test.ts +159 -0
  13. package/src/bun-db/__tests__/select-many-retry.integration.test.ts +138 -0
  14. package/src/bun-db/query.ts +42 -18
  15. package/src/changes.json +108 -0
  16. package/src/db/__tests__/pg-error.test.ts +14 -0
  17. package/src/db/__tests__/system-db-view-export.test.ts +107 -0
  18. package/src/db/__tests__/tenant-db-no-raw.test.ts +10 -0
  19. package/src/db/__tests__/with-systemdb-unsafe-raw-grant.test.ts +71 -0
  20. package/src/db/event-store-executor-write.ts +7 -0
  21. package/src/db/index.ts +1 -1
  22. package/src/db/pg-error.ts +13 -0
  23. package/src/db/queries/__tests__/{unsafe-read-retrying.test.ts → unsafe-read-retrying.integration.test.ts} +28 -28
  24. package/src/db/tenant-db.ts +140 -16
  25. package/src/engine/__tests__/boot-validator-gdpr-storage.test.ts +6 -1
  26. package/src/engine/__tests__/boot-validator-s0-integration.test.ts +2 -2
  27. package/src/engine/__tests__/boot-validator.test.ts +1 -1
  28. package/src/engine/__tests__/tier-resolver-extension.test.ts +1 -1
  29. package/src/engine/boot-validator/access-declarations.ts +5 -66
  30. package/src/engine/extension-names.ts +55 -25
  31. package/src/engine/extensions/storage-provider.ts +14 -41
  32. package/src/engine/extensions/tenant-data.ts +4 -0
  33. package/src/engine/extensions/tenant-resource.ts +40 -0
  34. package/src/engine/extensions/user-data.ts +8 -7
  35. package/src/engine/feature-ast/__tests__/handler-header-roundtrip.test.ts +669 -0
  36. package/src/engine/feature-ast/__tests__/parse.test.ts +5 -3
  37. package/src/engine/feature-ast/__tests__/patch-update.test.ts +718 -0
  38. package/src/engine/feature-ast/__tests__/pattern-change-schema.test.ts +819 -0
  39. package/src/engine/feature-ast/entity-field-types.ts +41 -0
  40. package/src/engine/feature-ast/extractors/handlers.ts +217 -84
  41. package/src/engine/feature-ast/extractors/hooks.ts +72 -15
  42. package/src/engine/feature-ast/extractors/round2.ts +21 -0
  43. package/src/engine/feature-ast/extractors/shared.ts +9 -0
  44. package/src/engine/feature-ast/index.ts +11 -1
  45. package/src/engine/feature-ast/patch.ts +338 -5
  46. package/src/engine/feature-ast/patcher.ts +2 -2
  47. package/src/engine/feature-ast/pattern-change-schema.ts +1411 -0
  48. package/src/engine/feature-ast/patterns.ts +22 -15
  49. package/src/engine/feature-ast/render.ts +1 -0
  50. package/src/engine/feature-ui-extensions.ts +8 -7
  51. package/src/engine/index.ts +23 -5
  52. package/src/engine/personal-data-fields.ts +66 -0
  53. package/src/engine/registry-validate.ts +15 -0
  54. package/src/engine/registry.ts +2 -0
  55. package/src/engine/types/extension-options-map.ts +1 -0
  56. package/src/engine/types/index.ts +8 -0
  57. package/src/env/__tests__/dry-run.test.ts +43 -3
  58. package/src/env/dry-run.ts +28 -15
  59. package/src/errors/__tests__/write-failures.test.ts +47 -4
  60. package/src/errors/i18n/de.yaml +12 -0
  61. package/src/errors/i18n/en.yaml +12 -0
  62. package/src/errors/reasons.ts +4 -0
  63. package/src/errors/write-error-info.ts +12 -3
  64. package/src/jobs/__tests__/job-backoff.integration.test.ts +155 -0
  65. package/src/jobs/__tests__/job-retention.integration.test.ts +316 -0
  66. package/src/jobs/__tests__/job-retry-enqueue-paths.integration.test.ts +309 -0
  67. package/src/jobs/__tests__/jobs.integration.test.ts +38 -3
  68. package/src/jobs/job-runner.ts +170 -19
  69. package/src/pipeline/__tests__/ctx-bridge.integration.test.ts +113 -26
  70. package/src/pipeline/__tests__/dispatcher.test.ts +5 -0
  71. package/src/pipeline/__tests__/hook-systemdb-escape-hatch.integration.test.ts +246 -0
  72. package/src/pipeline/__tests__/idempotency-transient-failure.integration.test.ts +198 -0
  73. package/src/pipeline/__tests__/public-intake-runtime-gate.integration.test.ts +425 -0
  74. package/src/pipeline/__tests__/redis-pipeline.integration.test.ts +59 -0
  75. package/src/pipeline/active-membership.ts +5 -1
  76. package/src/pipeline/dispatch-batch.ts +59 -13
  77. package/src/pipeline/dispatch-query.ts +16 -5
  78. package/src/pipeline/dispatch-shared.ts +12 -5
  79. package/src/pipeline/dispatch-stream.ts +7 -2
  80. package/src/pipeline/dispatch-write.ts +22 -5
  81. package/src/pipeline/dispatcher.ts +9 -2
  82. package/src/pipeline/idempotency.ts +16 -0
  83. package/src/pipeline/member-reader.ts +3 -1
  84. package/src/pipeline/system-identity-switch.ts +22 -4
  85. package/src/pipeline/write-origin.ts +107 -0
  86. package/src/rate-limit/__tests__/middleware.integration.test.ts +40 -0
  87. package/src/rate-limit/middleware.ts +3 -0
  88. package/src/stack/__tests__/setup-test-stack-metrics.integration.test.ts +79 -0
  89. package/src/stack/test-stack.ts +5 -0
  90. package/src/testing/closed-connection-error.ts +62 -0
  91. package/src/testing/index.ts +1 -0
  92. package/src/bun-db/__tests__/select-many-retry.test.ts +0 -79
@@ -110,6 +110,10 @@ export type TestStackOptions = {
110
110
  * MUST go through here (real HTTP via `stack.http`/`stack.app.fetch`),
111
111
  * never `createTestDispatcher`. */
112
112
  extraRoutes?: import("../api/server").ServerOptions["extraRoutes"];
113
+ /** Forwarded to buildServer like runProdApp. Without a PrometheusMeter-backed
114
+ * `observability` the route answers 503, so spread `resolveObservabilityWiring(token)`
115
+ * rather than setting `metrics` alone. */
116
+ metrics?: import("../api/server").ServerOptions["metrics"];
113
117
  /** Inject a MasterKeyProvider for secrets-backed tests. Lands typed in
114
118
  * AppContext — set/delete/get + rotation job pick it up. Omit for
115
119
  * suites that don't touch secrets. */
@@ -410,6 +414,7 @@ export async function setupTestStack(options: TestStackOptions): Promise<TestSta
410
414
  eventDedup,
411
415
  sseBroker,
412
416
  ...(options.extraRoutes && { extraRoutes: options.extraRoutes }),
417
+ ...(options.metrics && { metrics: options.metrics }),
413
418
  // Tests drive the dispatcher via stack.eventDispatcher.runOnce() for
414
419
  // deterministic drains — no timer-induced flakiness. pollIntervalMs
415
420
  // stays short anyway in case a test opts into `.start()`. pgClient
@@ -0,0 +1,62 @@
1
+ // Captures a real closed-connection error instead of a hand-built fake —
2
+ // the production matcher checks driver-specific codes a fake can't reproduce.
3
+
4
+ import postgres from "postgres";
5
+ import { isClosedConnectionError } from "../bun-db/query";
6
+
7
+ export function testDatabaseUrl(): string {
8
+ return (
9
+ process.env["TEST_DATABASE_URL"] ??
10
+ process.env["DATABASE_URL"] ??
11
+ "postgresql://kumiko:kumiko@localhost:15432/kumiko_test"
12
+ );
13
+ }
14
+
15
+ const MAX_WARMUP_ROUNDS = 10;
16
+ const READS_PER_ROUND = 5;
17
+
18
+ type AdminClient = { unsafe(sql: string, params?: readonly unknown[]): Promise<unknown> };
19
+
20
+ // A per-round admin client hits a postgres-js reconnect timing bug (the first
21
+ // retry after terminate hangs); a single long-lived admin client avoids it.
22
+ export async function terminateBackendsByApplicationName(
23
+ admin: AdminClient,
24
+ applicationName: string,
25
+ ): Promise<void> {
26
+ await admin.unsafe(
27
+ "select pg_terminate_backend(pid) from pg_stat_activity where application_name = $1",
28
+ [applicationName],
29
+ );
30
+ }
31
+
32
+ // Terminates a throwaway pool's backends and races reads right after — the
33
+ // dead-connection window is only a few ms wide, so this retries rounds until one lands.
34
+ export async function captureClosedConnectionError(
35
+ url: string = testDatabaseUrl(),
36
+ ): Promise<unknown> {
37
+ const admin = postgres(url, { max: 1 });
38
+ try {
39
+ for (let round = 0; round < MAX_WARMUP_ROUNDS; round++) {
40
+ const applicationName = `kumiko-closed-conn-test-${crypto.randomUUID()}`;
41
+ const pool = postgres(url, { max: 1, connection: { application_name: applicationName } });
42
+ try {
43
+ await pool.unsafe("select 1");
44
+ await terminateBackendsByApplicationName(admin, applicationName);
45
+ for (let read = 0; read < READS_PER_ROUND; read++) {
46
+ try {
47
+ await pool.unsafe("select 1");
48
+ } catch (err) {
49
+ if (isClosedConnectionError(err)) return err;
50
+ }
51
+ }
52
+ } finally {
53
+ await pool.end({ timeout: 0 });
54
+ }
55
+ }
56
+ throw new Error(
57
+ `captureClosedConnectionError: no closed-connection error observed after ${MAX_WARMUP_ROUNDS} rounds.`,
58
+ );
59
+ } finally {
60
+ await admin.end({ timeout: 0 });
61
+ }
62
+ }
@@ -15,6 +15,7 @@ export { resetEntityFieldEncryptionCacheForTests } from "../db/entity-field-encr
15
15
  export { rolesOf } from "./access-assertions";
16
16
  export { expectError, expectSuccess } from "./assertions";
17
17
  export { withBootValidatorFixture } from "./boot-validator-fixture";
18
+ export { captureClosedConnectionError } from "./closed-connection-error";
18
19
  export { type ClearableTable, clearTables, resetTestTables } from "./db-cleanup";
19
20
  export {
20
21
  type E2EGeneratorOptions,
@@ -1,79 +0,0 @@
1
- // #1163: Bun.SQL can hand out a closed connection under load (AbortError
2
- // "The connection was closed."). Pure reads retry exactly once on a fresh
3
- // pool checkout; tx handles, non-matching errors, and genuine user aborts
4
- // must NOT retry.
5
-
6
- import { describe, expect, test } from "bun:test";
7
- import { buildEntityTable } from "../../db/table-builder";
8
- import { selectMany } from "../query";
9
-
10
- function closedConnectionError(): Error {
11
- return Object.assign(new Error("The connection was closed."), { name: "AbortError" });
12
- }
13
-
14
- type FakeClient = {
15
- unsafe: (sql: string, params?: readonly unknown[]) => Promise<readonly unknown[]>;
16
- begin?: () => never;
17
- calls: number;
18
- };
19
-
20
- function fakeClient(failures: Error[], opts: { tx?: boolean } = {}): FakeClient {
21
- const remaining = [...failures];
22
- const client: FakeClient = {
23
- calls: 0,
24
- unsafe: async () => {
25
- client.calls++;
26
- const err = remaining.shift();
27
- if (err) throw err;
28
- return [{ id: "r1", title: "ok", tenant_id: "t1", inserted_at: null, updated_at: null }];
29
- },
30
- };
31
- // A top-level pool client has begin(); a transaction handle does not.
32
- if (!opts.tx)
33
- client.begin = () => {
34
- throw new Error("not used in test");
35
- };
36
- return client;
37
- }
38
-
39
- const table = buildEntityTable("note", {
40
- fields: { title: { type: "text", required: true } },
41
- });
42
-
43
- describe("selectMany — closed-connection retry (#1163)", () => {
44
- test("retries once on AbortError 'connection was closed' and returns rows", async () => {
45
- const db = fakeClient([closedConnectionError()]);
46
- const rows = await selectMany(db, table);
47
- expect(rows).toHaveLength(1);
48
- expect(rows[0]?.title).toBe("ok");
49
- expect(db.calls).toBe(2);
50
- });
51
-
52
- test("gives up after the single retry when the connection stays closed", async () => {
53
- const db = fakeClient([closedConnectionError(), closedConnectionError()]);
54
- await expect(selectMany(db, table)).rejects.toThrow("connection was closed");
55
- expect(db.calls).toBe(2);
56
- });
57
-
58
- test("never retries on a transaction handle (no begin)", async () => {
59
- const db = fakeClient([closedConnectionError()], { tx: true });
60
- await expect(selectMany(db, table)).rejects.toThrow("connection was closed");
61
- expect(db.calls).toBe(1);
62
- });
63
-
64
- test("does not retry a genuine user abort (different message)", async () => {
65
- const userAbort = Object.assign(new Error("The operation was aborted."), {
66
- name: "AbortError",
67
- });
68
- const db = fakeClient([userAbort]);
69
- await expect(selectMany(db, table)).rejects.toThrow("operation was aborted");
70
- expect(db.calls).toBe(1);
71
- });
72
-
73
- test("does not retry generic query errors", async () => {
74
- const syntax = Object.assign(new Error("syntax error at or near"), { name: "PostgresError" });
75
- const db = fakeClient([syntax]);
76
- await expect(selectMany(db, table)).rejects.toThrow("syntax error");
77
- expect(db.calls).toBe(1);
78
- });
79
- });