@pattern-stack/codegen 0.8.0 → 0.9.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 (118) hide show
  1. package/CHANGELOG.md +70 -0
  2. package/dist/runtime/subsystems/auth/controllers/auth.controller.d.ts +1 -0
  3. package/dist/runtime/subsystems/auth/index.d.ts +2 -0
  4. package/dist/runtime/subsystems/auth/index.js +55 -0
  5. package/dist/runtime/subsystems/auth/index.js.map +1 -1
  6. package/dist/runtime/subsystems/auth/middleware/requester-context.d.ts +81 -0
  7. package/dist/runtime/subsystems/auth/middleware/requester-context.js +60 -0
  8. package/dist/runtime/subsystems/auth/middleware/requester-context.js.map +1 -0
  9. package/dist/runtime/subsystems/auth/protocols/user-context.d.ts +18 -0
  10. package/dist/runtime/subsystems/bridge/bridge-delivery-handler.js.map +1 -1
  11. package/dist/runtime/subsystems/bridge/bridge-outbox-drain-hook.js.map +1 -1
  12. package/dist/runtime/subsystems/bridge/bridge.module.d.ts +3 -0
  13. package/dist/runtime/subsystems/bridge/bridge.module.js +930 -275
  14. package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
  15. package/dist/runtime/subsystems/bridge/event-flow.service.js.map +1 -1
  16. package/dist/runtime/subsystems/bridge/index.d.ts +3 -0
  17. package/dist/runtime/subsystems/bridge/index.js +837 -182
  18. package/dist/runtime/subsystems/bridge/index.js.map +1 -1
  19. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.d.ts +3 -1
  20. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js +92 -1
  21. package/dist/runtime/subsystems/events/event-bus.drizzle-backend.js.map +1 -1
  22. package/dist/runtime/subsystems/events/event-bus.memory-backend.d.ts +3 -1
  23. package/dist/runtime/subsystems/events/event-bus.memory-backend.js +99 -0
  24. package/dist/runtime/subsystems/events/event-bus.memory-backend.js.map +1 -1
  25. package/dist/runtime/subsystems/events/event-bus.redis-backend.js.map +1 -1
  26. package/dist/runtime/subsystems/events/event-keyset-cursor.d.ts +32 -0
  27. package/dist/runtime/subsystems/events/event-keyset-cursor.js +38 -0
  28. package/dist/runtime/subsystems/events/event-keyset-cursor.js.map +1 -0
  29. package/dist/runtime/subsystems/events/event-read.protocol.d.ts +94 -0
  30. package/dist/runtime/subsystems/events/event-read.protocol.js +9 -0
  31. package/dist/runtime/subsystems/events/event-read.protocol.js.map +1 -0
  32. package/dist/runtime/subsystems/events/events.module.js +177 -3
  33. package/dist/runtime/subsystems/events/events.module.js.map +1 -1
  34. package/dist/runtime/subsystems/events/events.tokens.d.ts +16 -1
  35. package/dist/runtime/subsystems/events/events.tokens.js +2 -0
  36. package/dist/runtime/subsystems/events/events.tokens.js.map +1 -1
  37. package/dist/runtime/subsystems/events/generated/bus.js.map +1 -1
  38. package/dist/runtime/subsystems/events/generated/index.js.map +1 -1
  39. package/dist/runtime/subsystems/events/index.d.ts +2 -1
  40. package/dist/runtime/subsystems/events/index.js +178 -3
  41. package/dist/runtime/subsystems/events/index.js.map +1 -1
  42. package/dist/runtime/subsystems/index.d.ts +2 -0
  43. package/dist/runtime/subsystems/index.js +1198 -264
  44. package/dist/runtime/subsystems/index.js.map +1 -1
  45. package/dist/runtime/subsystems/jobs/bullmq.config.d.ts +98 -0
  46. package/dist/runtime/subsystems/jobs/bullmq.config.js +143 -0
  47. package/dist/runtime/subsystems/jobs/bullmq.config.js.map +1 -0
  48. package/dist/runtime/subsystems/jobs/index.d.ts +6 -2
  49. package/dist/runtime/subsystems/jobs/index.js +861 -201
  50. package/dist/runtime/subsystems/jobs/index.js.map +1 -1
  51. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.d.ts +107 -0
  52. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js +922 -0
  53. package/dist/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.js.map +1 -0
  54. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.d.ts +52 -0
  55. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.js +57 -0
  56. package/dist/runtime/subsystems/jobs/job-run-keyset-cursor.js.map +1 -0
  57. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.d.ts +2 -1
  58. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js +81 -1
  59. package/dist/runtime/subsystems/jobs/job-run-service.drizzle-backend.js.map +1 -1
  60. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.d.ts +2 -1
  61. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js +81 -0
  62. package/dist/runtime/subsystems/jobs/job-run-service.memory-backend.js.map +1 -1
  63. package/dist/runtime/subsystems/jobs/job-run-service.protocol.d.ts +74 -1
  64. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.d.ts +48 -0
  65. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js +374 -0
  66. package/dist/runtime/subsystems/jobs/job-worker.bullmq-backend.js.map +1 -0
  67. package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +42 -4
  68. package/dist/runtime/subsystems/jobs/job-worker.module.js +832 -178
  69. package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
  70. package/dist/runtime/subsystems/jobs/jobs-domain.module.d.ts +10 -1
  71. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +519 -20
  72. package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
  73. package/dist/runtime/subsystems/jobs/pool-config.loader.d.ts +9 -1
  74. package/dist/runtime/subsystems/jobs/pool-config.loader.js +4 -0
  75. package/dist/runtime/subsystems/jobs/pool-config.loader.js.map +1 -1
  76. package/dist/runtime/subsystems/observability/index.d.ts +4 -3
  77. package/dist/runtime/subsystems/observability/index.js +109 -2
  78. package/dist/runtime/subsystems/observability/index.js.map +1 -1
  79. package/dist/runtime/subsystems/observability/observability.module.js +109 -2
  80. package/dist/runtime/subsystems/observability/observability.module.js.map +1 -1
  81. package/dist/runtime/subsystems/observability/observability.protocol.d.ts +63 -2
  82. package/dist/runtime/subsystems/observability/observability.service.d.ts +21 -3
  83. package/dist/runtime/subsystems/observability/observability.service.js +109 -2
  84. package/dist/runtime/subsystems/observability/observability.service.js.map +1 -1
  85. package/dist/runtime/subsystems/observability/reporters/bridge-metrics.reporter.d.ts +1 -0
  86. package/dist/runtime/subsystems/observability/reporters/index.d.ts +1 -0
  87. package/dist/src/cli/index.js +43 -7
  88. package/dist/src/cli/index.js.map +1 -1
  89. package/package.json +1 -1
  90. package/runtime/subsystems/auth/index.ts +8 -0
  91. package/runtime/subsystems/auth/middleware/requester-context.ts +141 -0
  92. package/runtime/subsystems/auth/protocols/user-context.ts +17 -0
  93. package/runtime/subsystems/bridge/bridge.module.ts +5 -0
  94. package/runtime/subsystems/events/event-bus.drizzle-backend.ts +109 -3
  95. package/runtime/subsystems/events/event-bus.memory-backend.ts +103 -1
  96. package/runtime/subsystems/events/event-keyset-cursor.ts +59 -0
  97. package/runtime/subsystems/events/event-read.protocol.ts +97 -0
  98. package/runtime/subsystems/events/events.module.ts +18 -2
  99. package/runtime/subsystems/events/events.tokens.ts +16 -0
  100. package/runtime/subsystems/events/index.ts +7 -0
  101. package/runtime/subsystems/jobs/bullmq.config.ts +125 -0
  102. package/runtime/subsystems/jobs/index.ts +22 -0
  103. package/runtime/subsystems/jobs/job-orchestrator.bullmq-backend.ts +381 -0
  104. package/runtime/subsystems/jobs/job-run-keyset-cursor.ts +88 -0
  105. package/runtime/subsystems/jobs/job-run-service.drizzle-backend.ts +59 -1
  106. package/runtime/subsystems/jobs/job-run-service.memory-backend.ts +53 -0
  107. package/runtime/subsystems/jobs/job-run-service.protocol.ts +77 -0
  108. package/runtime/subsystems/jobs/job-worker.bullmq-backend.ts +311 -0
  109. package/runtime/subsystems/jobs/job-worker.module.ts +124 -10
  110. package/runtime/subsystems/jobs/jobs-domain.module.ts +40 -21
  111. package/runtime/subsystems/jobs/pool-config.loader.ts +11 -0
  112. package/runtime/subsystems/observability/index.ts +8 -0
  113. package/runtime/subsystems/observability/observability.protocol.ts +76 -0
  114. package/runtime/subsystems/observability/observability.service.ts +148 -1
  115. package/templates/entity/new/clean-lite-ps/prompt-extension.js +14 -12
  116. package/templates/relationship/new/prompt.js +8 -5
  117. package/templates/subsystem/jobs/worker.ejs.t +30 -7
  118. package/templates/subsystem/sync/sync-audit.schema.ejs.t +12 -16
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Keyset (seek) cursor codec for `IEventReadPort.listEvents` (OBS-LIST-1).
3
+ *
4
+ * The list is ordered `occurred_at DESC, id DESC`. The cursor encodes the
5
+ * `(occurredAt, id)` of the last row on the previous page so the next page
6
+ * seeks with `WHERE (occurred_at, id) < (cursorOccurredAt, cursorId)`.
7
+ *
8
+ * The cursor is opaque to consumers: a base64url-encoded JSON tuple. Shape
9
+ * is an implementation detail — never parse it outside this module.
10
+ *
11
+ * Mirrors the jobs keyset codec; kept separate because the events subsystem
12
+ * must not depend on `runtime/subsystems/jobs/`.
13
+ */
14
+ interface EventKeyset {
15
+ occurredAt: Date;
16
+ id: string;
17
+ }
18
+ /** Default page size when `limit` is omitted. */
19
+ declare const DEFAULT_EVENT_LIST_LIMIT = 50;
20
+ /** Hard upper bound on page size. */
21
+ declare const MAX_EVENT_LIST_LIMIT = 200;
22
+ /** Clamp a caller-supplied `limit` into `[1, MAX_EVENT_LIST_LIMIT]`. */
23
+ declare function clampEventLimit(limit: number | undefined): number;
24
+ declare function encodeEventCursor(keyset: EventKeyset): string;
25
+ /**
26
+ * Decode an opaque cursor back into its `(occurredAt, id)` keyset. Returns
27
+ * `null` for malformed input so user-supplied garbage is treated as "start
28
+ * from the beginning" rather than throwing.
29
+ */
30
+ declare function decodeEventCursor(cursor: string): EventKeyset | null;
31
+
32
+ export { DEFAULT_EVENT_LIST_LIMIT, type EventKeyset, MAX_EVENT_LIST_LIMIT, clampEventLimit, decodeEventCursor, encodeEventCursor };
@@ -0,0 +1,38 @@
1
+ // runtime/subsystems/events/event-keyset-cursor.ts
2
+ var DEFAULT_EVENT_LIST_LIMIT = 50;
3
+ var MAX_EVENT_LIST_LIMIT = 200;
4
+ function clampEventLimit(limit) {
5
+ if (typeof limit !== "number" || !Number.isFinite(limit)) {
6
+ return DEFAULT_EVENT_LIST_LIMIT;
7
+ }
8
+ const floored = Math.floor(limit);
9
+ if (floored < 1) return 1;
10
+ if (floored > MAX_EVENT_LIST_LIMIT) return MAX_EVENT_LIST_LIMIT;
11
+ return floored;
12
+ }
13
+ function encodeEventCursor(keyset) {
14
+ const tuple = [keyset.occurredAt.toISOString(), keyset.id];
15
+ return Buffer.from(JSON.stringify(tuple), "utf8").toString("base64url");
16
+ }
17
+ function decodeEventCursor(cursor) {
18
+ try {
19
+ const json = Buffer.from(cursor, "base64url").toString("utf8");
20
+ const parsed = JSON.parse(json);
21
+ if (!Array.isArray(parsed) || parsed.length !== 2) return null;
22
+ const [iso, id] = parsed;
23
+ if (typeof iso !== "string" || typeof id !== "string") return null;
24
+ const occurredAt = new Date(iso);
25
+ if (Number.isNaN(occurredAt.getTime())) return null;
26
+ return { occurredAt, id };
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+ export {
32
+ DEFAULT_EVENT_LIST_LIMIT,
33
+ MAX_EVENT_LIST_LIMIT,
34
+ clampEventLimit,
35
+ decodeEventCursor,
36
+ encodeEventCursor
37
+ };
38
+ //# sourceMappingURL=event-keyset-cursor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../runtime/subsystems/events/event-keyset-cursor.ts"],"sourcesContent":["/**\n * Keyset (seek) cursor codec for `IEventReadPort.listEvents` (OBS-LIST-1).\n *\n * The list is ordered `occurred_at DESC, id DESC`. The cursor encodes the\n * `(occurredAt, id)` of the last row on the previous page so the next page\n * seeks with `WHERE (occurred_at, id) < (cursorOccurredAt, cursorId)`.\n *\n * The cursor is opaque to consumers: a base64url-encoded JSON tuple. Shape\n * is an implementation detail — never parse it outside this module.\n *\n * Mirrors the jobs keyset codec; kept separate because the events subsystem\n * must not depend on `runtime/subsystems/jobs/`.\n */\n\nexport interface EventKeyset {\n occurredAt: Date;\n id: string;\n}\n\n/** Default page size when `limit` is omitted. */\nexport const DEFAULT_EVENT_LIST_LIMIT = 50;\n/** Hard upper bound on page size. */\nexport const MAX_EVENT_LIST_LIMIT = 200;\n\n/** Clamp a caller-supplied `limit` into `[1, MAX_EVENT_LIST_LIMIT]`. */\nexport function clampEventLimit(limit: number | undefined): number {\n if (typeof limit !== 'number' || !Number.isFinite(limit)) {\n return DEFAULT_EVENT_LIST_LIMIT;\n }\n const floored = Math.floor(limit);\n if (floored < 1) return 1;\n if (floored > MAX_EVENT_LIST_LIMIT) return MAX_EVENT_LIST_LIMIT;\n return floored;\n}\n\nexport function encodeEventCursor(keyset: EventKeyset): string {\n const tuple = [keyset.occurredAt.toISOString(), keyset.id];\n return Buffer.from(JSON.stringify(tuple), 'utf8').toString('base64url');\n}\n\n/**\n * Decode an opaque cursor back into its `(occurredAt, id)` keyset. Returns\n * `null` for malformed input so user-supplied garbage is treated as \"start\n * from the beginning\" rather than throwing.\n */\nexport function decodeEventCursor(cursor: string): EventKeyset | null {\n try {\n const json = Buffer.from(cursor, 'base64url').toString('utf8');\n const parsed = JSON.parse(json) as unknown;\n if (!Array.isArray(parsed) || parsed.length !== 2) return null;\n const [iso, id] = parsed;\n if (typeof iso !== 'string' || typeof id !== 'string') return null;\n const occurredAt = new Date(iso);\n if (Number.isNaN(occurredAt.getTime())) return null;\n return { occurredAt, id };\n } catch {\n return null;\n }\n}\n"],"mappings":";AAoBO,IAAM,2BAA2B;AAEjC,IAAM,uBAAuB;AAG7B,SAAS,gBAAgB,OAAmC;AACjE,MAAI,OAAO,UAAU,YAAY,CAAC,OAAO,SAAS,KAAK,GAAG;AACxD,WAAO;AAAA,EACT;AACA,QAAM,UAAU,KAAK,MAAM,KAAK;AAChC,MAAI,UAAU,EAAG,QAAO;AACxB,MAAI,UAAU,qBAAsB,QAAO;AAC3C,SAAO;AACT;AAEO,SAAS,kBAAkB,QAA6B;AAC7D,QAAM,QAAQ,CAAC,OAAO,WAAW,YAAY,GAAG,OAAO,EAAE;AACzD,SAAO,OAAO,KAAK,KAAK,UAAU,KAAK,GAAG,MAAM,EAAE,SAAS,WAAW;AACxE;AAOO,SAAS,kBAAkB,QAAoC;AACpE,MAAI;AACF,UAAM,OAAO,OAAO,KAAK,QAAQ,WAAW,EAAE,SAAS,MAAM;AAC7D,UAAM,SAAS,KAAK,MAAM,IAAI;AAC9B,QAAI,CAAC,MAAM,QAAQ,MAAM,KAAK,OAAO,WAAW,EAAG,QAAO;AAC1D,UAAM,CAAC,KAAK,EAAE,IAAI;AAClB,QAAI,OAAO,QAAQ,YAAY,OAAO,OAAO,SAAU,QAAO;AAC9D,UAAM,aAAa,IAAI,KAAK,GAAG;AAC/B,QAAI,OAAO,MAAM,WAAW,QAAQ,CAAC,EAAG,QAAO;AAC/C,WAAO,EAAE,YAAY,GAAG;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":[]}
@@ -0,0 +1,94 @@
1
+ import { DomainEvent } from './event-bus.protocol.js';
2
+ import '../../types/drizzle.js';
3
+ import 'drizzle-orm/node-postgres';
4
+
5
+ /**
6
+ * IEventReadPort — read-side port over `domain_events` (OBS-LIST-1).
7
+ *
8
+ * The publish/subscribe `IEventBus` (EVENT_BUS) is a *write + dispatch*
9
+ * port; it deliberately does not expose tabular reads beyond `findById`.
10
+ * The observability combiner needs a paginated, filterable list of
11
+ * `domain_events` for its events viewer, so we add a dedicated read port
12
+ * rather than overloading `IEventBus`.
13
+ *
14
+ * Keeping reads on a separate port means:
15
+ * - the combiner can compose it `@Optional()` independently of EVENT_BUS;
16
+ * - the Redis backend (which retains no history) simply does not provide
17
+ * it — there is no "list" semantics to fake;
18
+ * - the write/dispatch surface stays minimal.
19
+ *
20
+ * Both `DrizzleEventBus` and `MemoryEventBus` implement this port (they
21
+ * already hold the rows / in-memory log); `EventsModule.forRoot` binds the
22
+ * `EVENT_READ_PORT` token to the same instance for drizzle/memory backends.
23
+ */
24
+
25
+ /**
26
+ * Filter + keyset-pagination input for `IEventReadPort.listEvents`.
27
+ *
28
+ * Ordered `occurred_at DESC, id DESC`. `rootRunId` filters on the JSON
29
+ * `metadata->>'rootRunId'` — the correlation id stamped by the jobs/bridge
30
+ * machinery so an event can be traced back to the run tree that emitted it.
31
+ */
32
+ interface ListEventsQuery {
33
+ /** Filter on `metadata->>'rootRunId'` (correlation id). */
34
+ rootRunId?: string;
35
+ /** Filter on the first-class `pool` column. */
36
+ poolId?: string;
37
+ /** Filter on the first-class `direction` column (inbound|change|outbound). */
38
+ direction?: string;
39
+ /** Lower bound on `occurred_at` (inclusive). */
40
+ since?: Date;
41
+ /** Opaque keyset cursor from a previous page's `nextCursor`. */
42
+ cursor?: string;
43
+ /** Page size. Backend clamps to a sane default + max. */
44
+ limit?: number;
45
+ /**
46
+ * Multi-tenancy filter on the first-class `tenant_id` column. Only
47
+ * meaningful when the consumer publishes tenant-scoped events
48
+ * (`events.multi_tenant: true`); otherwise leave undefined.
49
+ * - `string` — filter `tenant_id = :tenantId`.
50
+ * - `null` — filter `tenant_id IS NULL`.
51
+ * - `undefined` — no tenant filter.
52
+ */
53
+ tenantId?: string | null;
54
+ }
55
+ /**
56
+ * Summary row for the events list. A narrow projection over `domain_events`
57
+ * carrying what the viewer + correlation timeline need. `rootRunId` is
58
+ * surfaced (lifted out of `metadata`) so the timeline can stitch without a
59
+ * second metadata dig.
60
+ */
61
+ interface EventSummary {
62
+ id: string;
63
+ type: string;
64
+ aggregateId: string;
65
+ aggregateType: string;
66
+ status: string;
67
+ pool: string | null;
68
+ direction: string | null;
69
+ tier: string;
70
+ rootRunId: string | null;
71
+ tenantId: string | null;
72
+ occurredAt: Date;
73
+ processedAt: Date | null;
74
+ }
75
+ /**
76
+ * One page of `listEvents` results. `nextCursor` is `null` when there are
77
+ * no more rows.
78
+ */
79
+ interface EventPage {
80
+ items: EventSummary[];
81
+ nextCursor: string | null;
82
+ }
83
+ interface IEventReadPort {
84
+ /**
85
+ * Paginated, filterable list of `domain_events` (OBS-LIST-1). Newest
86
+ * first (`occurred_at` desc, `id` desc keyset tie-break). Returns an
87
+ * `EventPage` with an opaque `nextCursor` for keyset pagination.
88
+ */
89
+ listEvents(query?: ListEventsQuery): Promise<EventPage>;
90
+ }
91
+ /** A `DomainEvent` whose metadata may carry a `rootRunId` correlation id. */
92
+ declare function rootRunIdOf(event: DomainEvent): string | null;
93
+
94
+ export { type EventPage, type EventSummary, type IEventReadPort, type ListEventsQuery, rootRunIdOf };
@@ -0,0 +1,9 @@
1
+ // runtime/subsystems/events/event-read.protocol.ts
2
+ function rootRunIdOf(event) {
3
+ const v = event.metadata?.["rootRunId"];
4
+ return typeof v === "string" ? v : null;
5
+ }
6
+ export {
7
+ rootRunIdOf
8
+ };
9
+ //# sourceMappingURL=event-read.protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../runtime/subsystems/events/event-read.protocol.ts"],"sourcesContent":["/**\n * IEventReadPort — read-side port over `domain_events` (OBS-LIST-1).\n *\n * The publish/subscribe `IEventBus` (EVENT_BUS) is a *write + dispatch*\n * port; it deliberately does not expose tabular reads beyond `findById`.\n * The observability combiner needs a paginated, filterable list of\n * `domain_events` for its events viewer, so we add a dedicated read port\n * rather than overloading `IEventBus`.\n *\n * Keeping reads on a separate port means:\n * - the combiner can compose it `@Optional()` independently of EVENT_BUS;\n * - the Redis backend (which retains no history) simply does not provide\n * it — there is no \"list\" semantics to fake;\n * - the write/dispatch surface stays minimal.\n *\n * Both `DrizzleEventBus` and `MemoryEventBus` implement this port (they\n * already hold the rows / in-memory log); `EventsModule.forRoot` binds the\n * `EVENT_READ_PORT` token to the same instance for drizzle/memory backends.\n */\n\nimport type { DomainEvent } from './event-bus.protocol';\n\n/**\n * Filter + keyset-pagination input for `IEventReadPort.listEvents`.\n *\n * Ordered `occurred_at DESC, id DESC`. `rootRunId` filters on the JSON\n * `metadata->>'rootRunId'` — the correlation id stamped by the jobs/bridge\n * machinery so an event can be traced back to the run tree that emitted it.\n */\nexport interface ListEventsQuery {\n /** Filter on `metadata->>'rootRunId'` (correlation id). */\n rootRunId?: string;\n /** Filter on the first-class `pool` column. */\n poolId?: string;\n /** Filter on the first-class `direction` column (inbound|change|outbound). */\n direction?: string;\n /** Lower bound on `occurred_at` (inclusive). */\n since?: Date;\n /** Opaque keyset cursor from a previous page's `nextCursor`. */\n cursor?: string;\n /** Page size. Backend clamps to a sane default + max. */\n limit?: number;\n /**\n * Multi-tenancy filter on the first-class `tenant_id` column. Only\n * meaningful when the consumer publishes tenant-scoped events\n * (`events.multi_tenant: true`); otherwise leave undefined.\n * - `string` — filter `tenant_id = :tenantId`.\n * - `null` — filter `tenant_id IS NULL`.\n * - `undefined` — no tenant filter.\n */\n tenantId?: string | null;\n}\n\n/**\n * Summary row for the events list. A narrow projection over `domain_events`\n * carrying what the viewer + correlation timeline need. `rootRunId` is\n * surfaced (lifted out of `metadata`) so the timeline can stitch without a\n * second metadata dig.\n */\nexport interface EventSummary {\n id: string;\n type: string;\n aggregateId: string;\n aggregateType: string;\n status: string;\n pool: string | null;\n direction: string | null;\n tier: string;\n rootRunId: string | null;\n tenantId: string | null;\n occurredAt: Date;\n processedAt: Date | null;\n}\n\n/**\n * One page of `listEvents` results. `nextCursor` is `null` when there are\n * no more rows.\n */\nexport interface EventPage {\n items: EventSummary[];\n nextCursor: string | null;\n}\n\nexport interface IEventReadPort {\n /**\n * Paginated, filterable list of `domain_events` (OBS-LIST-1). Newest\n * first (`occurred_at` desc, `id` desc keyset tie-break). Returns an\n * `EventPage` with an opaque `nextCursor` for keyset pagination.\n */\n listEvents(query?: ListEventsQuery): Promise<EventPage>;\n}\n\n/** A `DomainEvent` whose metadata may carry a `rootRunId` correlation id. */\nexport function rootRunIdOf(event: DomainEvent): string | null {\n const v = event.metadata?.['rootRunId'];\n return typeof v === 'string' ? v : null;\n}\n"],"mappings":";AA6FO,SAAS,YAAY,OAAmC;AAC7D,QAAM,IAAI,MAAM,WAAW,WAAW;AACtC,SAAO,OAAO,MAAM,WAAW,IAAI;AACrC;","names":[]}
@@ -15,6 +15,7 @@ import { Module } from "@nestjs/common";
15
15
 
16
16
  // runtime/subsystems/events/events.tokens.ts
17
17
  var EVENT_BUS = "EVENT_BUS";
18
+ var EVENT_READ_PORT = "EVENT_READ_PORT";
18
19
  var TYPED_EVENT_BUS = "TYPED_EVENT_BUS";
19
20
  var EVENTS_MULTI_TENANT = "EVENTS_MULTI_TENANT";
20
21
  var REDIS_URL = /* @__PURE__ */ Symbol("REDIS_URL");
@@ -25,7 +26,38 @@ var DRIZZLE = "DRIZZLE";
25
26
 
26
27
  // runtime/subsystems/events/event-bus.drizzle-backend.ts
27
28
  import { Injectable, Inject, Logger, Optional } from "@nestjs/common";
28
- import { eq, and, inArray, asc } from "drizzle-orm";
29
+ import { eq, and, inArray, asc, desc, gte, lt, or, sql as sql2 } from "drizzle-orm";
30
+
31
+ // runtime/subsystems/events/event-keyset-cursor.ts
32
+ var DEFAULT_EVENT_LIST_LIMIT = 50;
33
+ var MAX_EVENT_LIST_LIMIT = 200;
34
+ function clampEventLimit(limit) {
35
+ if (typeof limit !== "number" || !Number.isFinite(limit)) {
36
+ return DEFAULT_EVENT_LIST_LIMIT;
37
+ }
38
+ const floored = Math.floor(limit);
39
+ if (floored < 1) return 1;
40
+ if (floored > MAX_EVENT_LIST_LIMIT) return MAX_EVENT_LIST_LIMIT;
41
+ return floored;
42
+ }
43
+ function encodeEventCursor(keyset) {
44
+ const tuple = [keyset.occurredAt.toISOString(), keyset.id];
45
+ return Buffer.from(JSON.stringify(tuple), "utf8").toString("base64url");
46
+ }
47
+ function decodeEventCursor(cursor) {
48
+ try {
49
+ const json = Buffer.from(cursor, "base64url").toString("utf8");
50
+ const parsed = JSON.parse(json);
51
+ if (!Array.isArray(parsed) || parsed.length !== 2) return null;
52
+ const [iso, id] = parsed;
53
+ if (typeof iso !== "string" || typeof id !== "string") return null;
54
+ const occurredAt = new Date(iso);
55
+ if (Number.isNaN(occurredAt.getTime())) return null;
56
+ return { occurredAt, id };
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
29
61
 
30
62
  // runtime/subsystems/events/domain-events.schema.ts
31
63
  import {
@@ -129,6 +161,24 @@ function toInsertValues(event) {
129
161
  tenantId
130
162
  };
131
163
  }
164
+ function toEventSummary(r) {
165
+ const metadata = r.metadata ?? void 0;
166
+ const rootRunId = metadata?.["rootRunId"];
167
+ return {
168
+ id: r.id,
169
+ type: r.type,
170
+ aggregateId: r.aggregateId,
171
+ aggregateType: r.aggregateType,
172
+ status: r.status,
173
+ pool: r.pool,
174
+ direction: r.direction,
175
+ tier: r.tier,
176
+ rootRunId: typeof rootRunId === "string" ? rootRunId : null,
177
+ tenantId: r.tenantId,
178
+ occurredAt: r.occurredAt instanceof Date ? r.occurredAt : new Date(r.occurredAt),
179
+ processedAt: r.processedAt == null ? null : r.processedAt instanceof Date ? r.processedAt : new Date(r.processedAt)
180
+ };
181
+ }
132
182
  var DrizzleEventBus = class {
133
183
  constructor(db, opts, bridgeHook = null) {
134
184
  this.db = db;
@@ -194,6 +244,48 @@ var DrizzleEventBus = class {
194
244
  };
195
245
  }
196
246
  // ============================================================================
247
+ // IEventReadPort (OBS-LIST-1)
248
+ // ============================================================================
249
+ async listEvents(query = {}) {
250
+ const limit = clampEventLimit(query.limit);
251
+ const conditions = [];
252
+ if (query.poolId) conditions.push(eq(domainEvents.pool, query.poolId));
253
+ if (query.direction)
254
+ conditions.push(eq(domainEvents.direction, query.direction));
255
+ if (query.since) conditions.push(gte(domainEvents.occurredAt, query.since));
256
+ if (query.rootRunId) {
257
+ conditions.push(
258
+ sql2`${domainEvents.metadata}->>'rootRunId' = ${query.rootRunId}`
259
+ );
260
+ }
261
+ if (query.tenantId !== void 0) {
262
+ conditions.push(
263
+ query.tenantId === null ? sql2`${domainEvents.tenantId} is null` : eq(domainEvents.tenantId, query.tenantId)
264
+ );
265
+ }
266
+ if (query.cursor) {
267
+ const keyset = decodeEventCursor(query.cursor);
268
+ if (keyset) {
269
+ conditions.push(
270
+ or(
271
+ lt(domainEvents.occurredAt, keyset.occurredAt),
272
+ and(
273
+ eq(domainEvents.occurredAt, keyset.occurredAt),
274
+ lt(domainEvents.id, keyset.id)
275
+ )
276
+ )
277
+ );
278
+ }
279
+ }
280
+ const rows = await this.db.select().from(domainEvents).where(conditions.length > 0 ? and(...conditions) : void 0).orderBy(desc(domainEvents.occurredAt), desc(domainEvents.id)).limit(limit + 1);
281
+ const hasMore = rows.length > limit;
282
+ const page = hasMore ? rows.slice(0, limit) : rows;
283
+ const items = page.map(toEventSummary);
284
+ const last = page[page.length - 1];
285
+ const nextCursor = hasMore && last ? encodeEventCursor({ occurredAt: last.occurredAt, id: last.id }) : null;
286
+ return { items, nextCursor };
287
+ }
288
+ // ============================================================================
197
289
  // Polling
198
290
  // ============================================================================
199
291
  /**
@@ -318,6 +410,27 @@ DrizzleEventBus = __decorateClass([
318
410
 
319
411
  // runtime/subsystems/events/event-bus.memory-backend.ts
320
412
  import { Inject as Inject2, Injectable as Injectable2, Logger as Logger2, Optional as Optional2 } from "@nestjs/common";
413
+ function toEventSummary2(event) {
414
+ const metadata = event.metadata;
415
+ const str = (key) => {
416
+ const v = metadata?.[key];
417
+ return typeof v === "string" ? v : null;
418
+ };
419
+ return {
420
+ id: event.id,
421
+ type: event.type,
422
+ aggregateId: event.aggregateId,
423
+ aggregateType: event.aggregateType,
424
+ status: "processed",
425
+ pool: str("pool"),
426
+ direction: str("direction"),
427
+ tier: str("tier") ?? "domain",
428
+ rootRunId: str("rootRunId"),
429
+ tenantId: str("tenantId"),
430
+ occurredAt: event.occurredAt,
431
+ processedAt: event.occurredAt
432
+ };
433
+ }
321
434
  var MemoryEventBus = class {
322
435
  logger = new Logger2(MemoryEventBus.name);
323
436
  /** All events published since construction (or last clear). */
@@ -353,6 +466,53 @@ var MemoryEventBus = class {
353
466
  set.delete(h);
354
467
  };
355
468
  }
469
+ // ============================================================================
470
+ // IEventReadPort (OBS-LIST-1)
471
+ // ============================================================================
472
+ async listEvents(query = {}) {
473
+ const limit = clampEventLimit(query.limit);
474
+ const keyset = query.cursor ? decodeEventCursor(query.cursor) : null;
475
+ const str = (e, key) => {
476
+ const v = e.metadata?.[key];
477
+ return typeof v === "string" ? v : null;
478
+ };
479
+ const matched = this.publishedEvents.filter((e) => {
480
+ if (query.poolId && str(e, "pool") !== query.poolId) return false;
481
+ if (query.direction && str(e, "direction") !== query.direction)
482
+ return false;
483
+ if (query.rootRunId && str(e, "rootRunId") !== query.rootRunId)
484
+ return false;
485
+ if (query.since && e.occurredAt.getTime() < query.since.getTime())
486
+ return false;
487
+ if (query.tenantId !== void 0) {
488
+ const t = str(e, "tenantId");
489
+ if (query.tenantId === null) {
490
+ if (t !== null) return false;
491
+ } else if (t !== query.tenantId) {
492
+ return false;
493
+ }
494
+ }
495
+ return true;
496
+ });
497
+ matched.sort((a, b) => {
498
+ const dt = b.occurredAt.getTime() - a.occurredAt.getTime();
499
+ if (dt !== 0) return dt;
500
+ return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;
501
+ });
502
+ const seeked = keyset ? matched.filter((e) => {
503
+ const ct = e.occurredAt.getTime();
504
+ const kt = keyset.occurredAt.getTime();
505
+ if (ct < kt) return true;
506
+ if (ct > kt) return false;
507
+ return e.id < keyset.id;
508
+ }) : matched;
509
+ const hasMore = seeked.length > limit;
510
+ const page = hasMore ? seeked.slice(0, limit) : seeked;
511
+ const items = page.map(toEventSummary2);
512
+ const last = page[page.length - 1];
513
+ const nextCursor = hasMore && last ? encodeEventCursor({ occurredAt: last.occurredAt, id: last.id }) : null;
514
+ return { items, nextCursor };
515
+ }
356
516
  /** Remove all published events and subscriptions. Useful in beforeEach. */
357
517
  clear() {
358
518
  this.publishedEvents.length = 0;
@@ -930,10 +1090,20 @@ var EventsModule = class {
930
1090
  REDIS_URL
931
1091
  ]
932
1092
  },
1093
+ {
1094
+ // Read port (OBS-LIST-1). Drizzle + memory backends implement
1095
+ // IEventReadPort on the EVENT_BUS instance; the redis backend
1096
+ // retains no history, so EVENT_READ_PORT resolves to `null` and
1097
+ // optional consumers (the observability combiner) degrade to
1098
+ // empty results.
1099
+ provide: EVENT_READ_PORT,
1100
+ useFactory: (options, bus) => options.backend === "redis" ? null : bus,
1101
+ inject: [EVENTS_MODULE_OPTIONS, EVENT_BUS]
1102
+ },
933
1103
  TypedEventBus,
934
1104
  { provide: TYPED_EVENT_BUS, useExisting: TypedEventBus }
935
1105
  ],
936
- exports: [EVENT_BUS, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT]
1106
+ exports: [EVENT_BUS, EVENT_READ_PORT, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT]
937
1107
  };
938
1108
  }
939
1109
  static forRoot(options = { backend: "drizzle" }) {
@@ -961,9 +1131,13 @@ var EventsModule = class {
961
1131
  providers: [
962
1132
  { provide: EVENTS_MODULE_OPTIONS, useValue: options },
963
1133
  provider,
1134
+ // Read port (OBS-LIST-1): drizzle + memory backends implement
1135
+ // IEventReadPort on the same instance as EVENT_BUS. The redis
1136
+ // backend retains no history and does not provide this token.
1137
+ { provide: EVENT_READ_PORT, useExisting: EVENT_BUS },
964
1138
  ...buildTypedBusProviders(multiTenant)
965
1139
  ],
966
- exports: [EVENT_BUS, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT]
1140
+ exports: [EVENT_BUS, EVENT_READ_PORT, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT]
967
1141
  };
968
1142
  }
969
1143
  };