@pattern-stack/codegen 0.4.0 → 0.4.2

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 (136) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/src/cli/index.js +1616 -1070
  3. package/dist/src/cli/index.js.map +1 -1
  4. package/package.json +3 -1
  5. package/runtime/analytics/index.ts +31 -0
  6. package/runtime/analytics/metrics.ts +85 -0
  7. package/runtime/analytics/packs/crm-entity-measures.ts +20 -0
  8. package/runtime/analytics/packs/index.ts +5 -0
  9. package/runtime/analytics/packs/monetary-measures.ts +20 -0
  10. package/runtime/analytics/specs.ts +54 -0
  11. package/runtime/analytics/types.ts +105 -0
  12. package/runtime/base-classes/activity-entity-repository.ts +50 -0
  13. package/runtime/base-classes/activity-entity-service.ts +48 -0
  14. package/runtime/base-classes/base-read-use-cases.ts +88 -0
  15. package/runtime/base-classes/base-repository.ts +289 -0
  16. package/runtime/base-classes/base-service.ts +183 -0
  17. package/runtime/base-classes/index.ts +38 -0
  18. package/runtime/base-classes/knowledge-entity-repository.ts +12 -0
  19. package/runtime/base-classes/knowledge-entity-service.ts +14 -0
  20. package/runtime/base-classes/lifecycle-events.ts +152 -0
  21. package/runtime/base-classes/metadata-entity-repository.ts +80 -0
  22. package/runtime/base-classes/metadata-entity-service.ts +48 -0
  23. package/runtime/base-classes/synced-entity-repository.ts +57 -0
  24. package/runtime/base-classes/synced-entity-service.ts +50 -0
  25. package/runtime/base-classes/with-analytics.ts +22 -0
  26. package/runtime/constants/tokens.ts +29 -0
  27. package/runtime/eav-helpers.ts +74 -0
  28. package/runtime/pipes/zod-validation.pipe.ts +64 -0
  29. package/runtime/shared/openapi/error-response.dto.ts +24 -0
  30. package/runtime/shared/openapi/errors.ts +39 -0
  31. package/runtime/shared/openapi/index.ts +20 -0
  32. package/runtime/shared/openapi/registry.tokens.ts +13 -0
  33. package/runtime/shared/openapi/registry.ts +151 -0
  34. package/runtime/subsystems/analytics/analytics-query.protocol.ts +37 -0
  35. package/runtime/subsystems/analytics/analytics.module.ts +64 -0
  36. package/runtime/subsystems/analytics/analytics.tokens.ts +24 -0
  37. package/runtime/subsystems/analytics/cube-backend.ts +75 -0
  38. package/runtime/subsystems/analytics/index.ts +15 -0
  39. package/runtime/subsystems/analytics/noop-backend.ts +27 -0
  40. package/runtime/subsystems/auth/auth.module.ts +91 -0
  41. package/runtime/subsystems/auth/auth.tokens.ts +27 -0
  42. package/runtime/subsystems/auth/backends/encryption-key/env.ts +76 -0
  43. package/runtime/subsystems/auth/backends/oauth-state-store/in-memory.ts +42 -0
  44. package/runtime/subsystems/auth/index.ts +77 -0
  45. package/runtime/subsystems/auth/protocols/auth-strategy.ts +46 -0
  46. package/runtime/subsystems/auth/protocols/encryption-key.ts +21 -0
  47. package/runtime/subsystems/auth/protocols/integration-store.ts +66 -0
  48. package/runtime/subsystems/auth/protocols/oauth-state-store.ts +16 -0
  49. package/runtime/subsystems/auth/runtime/integration-broken.error.ts +21 -0
  50. package/runtime/subsystems/auth/runtime/oauth2-refresh.strategy.ts +189 -0
  51. package/runtime/subsystems/auth/runtime/session-expired.error.ts +39 -0
  52. package/runtime/subsystems/auth/runtime/with-auth-retry.ts +50 -0
  53. package/runtime/subsystems/bridge/assert-tenant-id.ts +57 -0
  54. package/runtime/subsystems/bridge/bridge-delivery-handler.ts +220 -0
  55. package/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.ts +149 -0
  56. package/runtime/subsystems/bridge/bridge-delivery.memory-backend.ts +140 -0
  57. package/runtime/subsystems/bridge/bridge-delivery.schema.ts +142 -0
  58. package/runtime/subsystems/bridge/bridge-errors.ts +112 -0
  59. package/runtime/subsystems/bridge/bridge-outbox-drain-hook.ts +175 -0
  60. package/runtime/subsystems/bridge/bridge.module.ts +160 -0
  61. package/runtime/subsystems/bridge/bridge.protocol.ts +351 -0
  62. package/runtime/subsystems/bridge/bridge.tokens.ts +68 -0
  63. package/runtime/subsystems/bridge/event-flow.service.ts +175 -0
  64. package/runtime/subsystems/bridge/generated/.gitkeep +0 -0
  65. package/runtime/subsystems/bridge/generated/registry.ts +6 -0
  66. package/runtime/subsystems/bridge/index.ts +84 -0
  67. package/runtime/subsystems/bridge/reserved-pools.ts +36 -0
  68. package/runtime/subsystems/cache/cache.drizzle-backend.ts +150 -0
  69. package/runtime/subsystems/cache/cache.memory-backend.ts +116 -0
  70. package/runtime/subsystems/cache/cache.module.ts +115 -0
  71. package/runtime/subsystems/cache/cache.protocol.ts +45 -0
  72. package/runtime/subsystems/cache/cache.schema.ts +27 -0
  73. package/runtime/subsystems/cache/cache.tokens.ts +17 -0
  74. package/runtime/subsystems/cache/index.ts +22 -0
  75. package/runtime/subsystems/events/domain-events.schema.ts +77 -0
  76. package/runtime/subsystems/events/event-bus.drizzle-backend.ts +327 -0
  77. package/runtime/subsystems/events/event-bus.memory-backend.ts +142 -0
  78. package/runtime/subsystems/events/event-bus.protocol.ts +86 -0
  79. package/runtime/subsystems/events/event-bus.redis-backend.ts +304 -0
  80. package/runtime/subsystems/events/events-errors.ts +30 -0
  81. package/runtime/subsystems/events/events.module.ts +230 -0
  82. package/runtime/subsystems/events/events.tokens.ts +62 -0
  83. package/runtime/subsystems/events/generated/bus.ts +103 -0
  84. package/runtime/subsystems/events/generated/index.ts +7 -0
  85. package/runtime/subsystems/events/generated/registry.ts +84 -0
  86. package/runtime/subsystems/events/generated/schemas.ts +59 -0
  87. package/runtime/subsystems/events/generated/types.ts +94 -0
  88. package/runtime/subsystems/events/index.ts +21 -0
  89. package/runtime/subsystems/index.ts +63 -0
  90. package/runtime/subsystems/jobs/generated/job-orchestration.schema.multi-tenant.ts +217 -0
  91. package/runtime/subsystems/jobs/generated/job-orchestration.schema.single-tenant.ts +217 -0
  92. package/runtime/subsystems/jobs/generated/scope-entity-type.ts +10 -0
  93. package/runtime/subsystems/jobs/index.ts +120 -0
  94. package/runtime/subsystems/jobs/job-handler.base.ts +206 -0
  95. package/runtime/subsystems/jobs/job-orchestration.schema.ts +217 -0
  96. package/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.ts +536 -0
  97. package/runtime/subsystems/jobs/job-orchestrator.memory-backend.ts +850 -0
  98. package/runtime/subsystems/jobs/job-orchestrator.protocol.ts +179 -0
  99. package/runtime/subsystems/jobs/job-run-service.drizzle-backend.ts +171 -0
  100. package/runtime/subsystems/jobs/job-run-service.memory-backend.ts +165 -0
  101. package/runtime/subsystems/jobs/job-run-service.protocol.ts +79 -0
  102. package/runtime/subsystems/jobs/job-step-service.drizzle-backend.ts +66 -0
  103. package/runtime/subsystems/jobs/job-step-service.memory-backend.ts +119 -0
  104. package/runtime/subsystems/jobs/job-step-service.protocol.ts +53 -0
  105. package/runtime/subsystems/jobs/job-worker.module.ts +302 -0
  106. package/runtime/subsystems/jobs/job-worker.ts +615 -0
  107. package/runtime/subsystems/jobs/jobs-domain.module.ts +119 -0
  108. package/runtime/subsystems/jobs/jobs-domain.tokens.ts +30 -0
  109. package/runtime/subsystems/jobs/jobs-errors.ts +150 -0
  110. package/runtime/subsystems/jobs/memory-job-store.ts +35 -0
  111. package/runtime/subsystems/jobs/pool-config.loader.ts +218 -0
  112. package/runtime/subsystems/storage/index.ts +18 -0
  113. package/runtime/subsystems/storage/storage.local-backend.ts +113 -0
  114. package/runtime/subsystems/storage/storage.memory-backend.ts +78 -0
  115. package/runtime/subsystems/storage/storage.module.ts +60 -0
  116. package/runtime/subsystems/storage/storage.protocol.ts +78 -0
  117. package/runtime/subsystems/storage/storage.tokens.ts +9 -0
  118. package/runtime/subsystems/storage/storage.utils.ts +20 -0
  119. package/runtime/subsystems/sync/deep-equal.differ.ts +198 -0
  120. package/runtime/subsystems/sync/execute-sync.use-case.ts +334 -0
  121. package/runtime/subsystems/sync/index.ts +98 -0
  122. package/runtime/subsystems/sync/sync-audit.schema.ts +300 -0
  123. package/runtime/subsystems/sync/sync-change-source.protocol.ts +99 -0
  124. package/runtime/subsystems/sync/sync-cursor-store.drizzle-backend.ts +104 -0
  125. package/runtime/subsystems/sync/sync-cursor-store.memory-backend.ts +64 -0
  126. package/runtime/subsystems/sync/sync-cursor-store.protocol.ts +53 -0
  127. package/runtime/subsystems/sync/sync-errors.ts +54 -0
  128. package/runtime/subsystems/sync/sync-field-diff.protocol.ts +61 -0
  129. package/runtime/subsystems/sync/sync-loopback.protocol.ts +33 -0
  130. package/runtime/subsystems/sync/sync-run-recorder.drizzle-backend.ts +123 -0
  131. package/runtime/subsystems/sync/sync-run-recorder.memory-backend.ts +143 -0
  132. package/runtime/subsystems/sync/sync-run-recorder.protocol.ts +86 -0
  133. package/runtime/subsystems/sync/sync-sink.protocol.ts +55 -0
  134. package/runtime/subsystems/sync/sync.module.ts +156 -0
  135. package/runtime/subsystems/sync/sync.tokens.ts +57 -0
  136. package/runtime/types/drizzle.ts +23 -0
@@ -0,0 +1,149 @@
1
+ /**
2
+ * DrizzleBridgeDeliveryRepo — Postgres implementation of `IJobBridge`
3
+ * (BRIDGE-4, ADR-023 Phase 2).
4
+ *
5
+ * Behavioral twin of `MemoryBridgeDeliveryRepo` (BRIDGE-3). The key
6
+ * difference is `insertDelivery`: where the memory backend throws
7
+ * `UniqueConstraintError` on a duplicate `(event_id, trigger_id)`, the
8
+ * Drizzle backend uses `INSERT … ON CONFLICT (event_id, trigger_id) DO
9
+ * NOTHING` and surfaces the dedup as a silent no-op. This matches the
10
+ * BRIDGE-4 spec recommendation — a thrown error inside the per-event tx
11
+ * would abort sibling triggers, which is exactly the failure mode the
12
+ * facade's Case B pre-write was designed to prevent.
13
+ *
14
+ * Tests that need to assert "the constraint fired" use `findDelivery` to
15
+ * confirm the existing row is the facade-eager pre-write (or the prior
16
+ * drain attempt's row), not the one this call tried to insert. The
17
+ * `UniqueConstraintError` branch from BRIDGE-3 is the memory-backend
18
+ * fidelity path; the Drizzle backend models the same idempotency through
19
+ * SQL semantics rather than a thrown error.
20
+ *
21
+ * The other four methods (`findDelivery`, `findDeliveryById`,
22
+ * `mark{Delivered,Skipped,Failed}`) are straightforward
23
+ * `SELECT … LIMIT 1` / `UPDATE … WHERE id = ?` queries.
24
+ */
25
+ import { Inject, Injectable, Optional } from '@nestjs/common';
26
+ import { eq, and } from 'drizzle-orm';
27
+
28
+ import { DRIZZLE } from '../../constants/tokens';
29
+ import type { DrizzleClient } from '../../types/drizzle';
30
+ import type { DrizzleTransaction } from '../events/event-bus.protocol';
31
+
32
+ import { bridgeDelivery } from './bridge-delivery.schema';
33
+ import type { BridgeDeliveryRecord } from './bridge-delivery.schema';
34
+ import type {
35
+ BridgeDeliveryInsert,
36
+ IJobBridge,
37
+ } from './bridge.protocol';
38
+ import { assertTenantId } from './assert-tenant-id';
39
+ import { BRIDGE_MULTI_TENANT } from './bridge.tokens';
40
+
41
+ @Injectable()
42
+ export class DrizzleBridgeDeliveryRepo implements IJobBridge {
43
+ constructor(
44
+ @Inject(DRIZZLE) private readonly db: DrizzleClient,
45
+ /**
46
+ * Site (c) of the three ADR-023 §Multi-tenancy enforcement sites.
47
+ * `@Optional()` so unit tests / non-bridge mounts that don't provide
48
+ * the token still construct the repo cleanly; defaults to `false`,
49
+ * which makes `assertTenantId` a no-op.
50
+ */
51
+ @Optional()
52
+ @Inject(BRIDGE_MULTI_TENANT)
53
+ private readonly multiTenant: boolean = false,
54
+ ) {}
55
+
56
+ async insertDelivery(
57
+ row: BridgeDeliveryInsert,
58
+ tx?: DrizzleTransaction,
59
+ ): Promise<void> {
60
+ // Multi-tenancy gate — last-line defense. Even if callers skipped
61
+ // sites (a) `EventFlowService.publishAndStart` and (b)
62
+ // `BridgeDeliveryHandler.run`, a direct repo call still fails fast
63
+ // BEFORE any SQL is issued.
64
+ assertTenantId(
65
+ 'DrizzleBridgeDeliveryRepo.insertDelivery',
66
+ this.multiTenant,
67
+ row.tenantId,
68
+ );
69
+ const client = (tx ?? this.db) as DrizzleClient;
70
+ // ON CONFLICT DO NOTHING — surfaces dedup as silent no-op so the
71
+ // per-event tx stays atomic across sibling triggers. RETURNING is
72
+ // omitted here: the public IJobBridge contract is `Promise<void>`,
73
+ // and the drain hook (BRIDGE-4) uses its own
74
+ // `tx.insert(...).onConflictDoNothing().returning({id})` pattern
75
+ // when it needs the rowcount discriminator. See class-level JSDoc.
76
+ await client
77
+ .insert(bridgeDelivery)
78
+ .values(row)
79
+ .onConflictDoNothing({
80
+ target: [bridgeDelivery.eventId, bridgeDelivery.triggerId],
81
+ });
82
+ }
83
+
84
+ async findDelivery(
85
+ eventId: string,
86
+ triggerId: string,
87
+ ): Promise<BridgeDeliveryRecord | null> {
88
+ const rows = await this.db
89
+ .select()
90
+ .from(bridgeDelivery)
91
+ .where(
92
+ and(
93
+ eq(bridgeDelivery.eventId, eventId),
94
+ eq(bridgeDelivery.triggerId, triggerId),
95
+ ),
96
+ )
97
+ .limit(1);
98
+ return (rows[0] as BridgeDeliveryRecord | undefined) ?? null;
99
+ }
100
+
101
+ async findDeliveryById(id: string): Promise<BridgeDeliveryRecord | null> {
102
+ const rows = await this.db
103
+ .select()
104
+ .from(bridgeDelivery)
105
+ .where(eq(bridgeDelivery.id, id))
106
+ .limit(1);
107
+ return (rows[0] as BridgeDeliveryRecord | undefined) ?? null;
108
+ }
109
+
110
+ async markDelivered(
111
+ id: string,
112
+ userRunId: string,
113
+ tx?: DrizzleTransaction,
114
+ ): Promise<void> {
115
+ const client = (tx ?? this.db) as DrizzleClient;
116
+ await client
117
+ .update(bridgeDelivery)
118
+ .set({
119
+ status: 'delivered',
120
+ userRunId,
121
+ deliveredAt: new Date(),
122
+ })
123
+ .where(eq(bridgeDelivery.id, id));
124
+ }
125
+
126
+ async markSkipped(
127
+ id: string,
128
+ reason: string,
129
+ tx?: DrizzleTransaction,
130
+ ): Promise<void> {
131
+ const client = (tx ?? this.db) as DrizzleClient;
132
+ await client
133
+ .update(bridgeDelivery)
134
+ .set({ status: 'skipped', skipReason: reason })
135
+ .where(eq(bridgeDelivery.id, id));
136
+ }
137
+
138
+ async markFailed(
139
+ id: string,
140
+ error: Record<string, unknown>,
141
+ tx?: DrizzleTransaction,
142
+ ): Promise<void> {
143
+ const client = (tx ?? this.db) as DrizzleClient;
144
+ await client
145
+ .update(bridgeDelivery)
146
+ .set({ status: 'failed', error })
147
+ .where(eq(bridgeDelivery.id, id));
148
+ }
149
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * MemoryBridgeDeliveryRepo — in-memory `IJobBridge` (BRIDGE-3, ADR-023 Phase 2).
3
+ *
4
+ * Behavioral twin of the Drizzle backend (BRIDGE-4) for use in
5
+ * `just test-unit`. No Docker, no Postgres, fully synchronous. Backs a
6
+ * `Map<"${eventId}::${triggerId}", BridgeDeliveryRecord>` so the UNIQUE
7
+ * `(event_id, trigger_id)` constraint can be simulated cheaply.
8
+ *
9
+ * Precedent: `MemoryEventBus` (EVT-5), `MemoryJobOrchestrator`
10
+ * (jobs subsystem). Same shape — a class implementing the protocol plus
11
+ * test-only helpers (`getDeliveriesForEvent`, `getByStatus`, `clear`) that
12
+ * BRIDGE-5's framework-handler tests and BRIDGE-7's facade tests will
13
+ * exercise.
14
+ *
15
+ * The synthetic `UniqueConstraintError` carries a `constraint` field equal
16
+ * to the Drizzle constraint name (`uq_bridge_delivery_event_trigger`, set
17
+ * in BRIDGE-1's schema) so consumers — including BRIDGE-4's
18
+ * `INSERT … ON CONFLICT (event_id, trigger_id) DO NOTHING` path and
19
+ * BRIDGE-7's Case B dedup tests — can branch on the same discriminator
20
+ * regardless of which backend is wired up. ADR-023 explicitly relies on
21
+ * this constraint as the dedup mechanism in two places (replay; facade-
22
+ * vs-drain Case B); a typed error makes both call sites checkable.
23
+ */
24
+ import { randomUUID } from 'node:crypto';
25
+
26
+ import type {
27
+ BridgeDeliveryInsert,
28
+ IJobBridge,
29
+ } from './bridge.protocol';
30
+ import type { BridgeDeliveryRecord } from './bridge-delivery.schema';
31
+ import { UniqueConstraintError } from './bridge-errors';
32
+
33
+ const BRIDGE_DELIVERY_UNIQUE_CONSTRAINT =
34
+ 'uq_bridge_delivery_event_trigger' as const;
35
+
36
+ function key(eventId: string, triggerId: string): string {
37
+ return `${eventId}::${triggerId}`;
38
+ }
39
+
40
+ export class MemoryBridgeDeliveryRepo implements IJobBridge {
41
+ private readonly deliveries = new Map<string, BridgeDeliveryRecord>();
42
+
43
+ async insertDelivery(row: BridgeDeliveryInsert): Promise<void> {
44
+ const k = key(row.eventId, row.triggerId);
45
+ if (this.deliveries.has(k)) {
46
+ throw new UniqueConstraintError(
47
+ BRIDGE_DELIVERY_UNIQUE_CONSTRAINT,
48
+ row.eventId,
49
+ row.triggerId,
50
+ );
51
+ }
52
+ // Materialize a full BridgeDeliveryRecord — fill in DB defaults that
53
+ // the insert payload allowed to be omitted.
54
+ const record: BridgeDeliveryRecord = {
55
+ id: row.id ?? randomUUID(),
56
+ eventId: row.eventId,
57
+ triggerId: row.triggerId,
58
+ wrapperRunId: row.wrapperRunId ?? null,
59
+ userRunId: row.userRunId ?? null,
60
+ status: row.status ?? 'pending',
61
+ skipReason: row.skipReason ?? null,
62
+ error: (row.error as Record<string, unknown> | null | undefined) ?? null,
63
+ tenantId: row.tenantId ?? null,
64
+ attemptedAt:
65
+ row.attemptedAt instanceof Date ? row.attemptedAt : new Date(),
66
+ deliveredAt:
67
+ row.deliveredAt instanceof Date ? row.deliveredAt : null,
68
+ };
69
+ this.deliveries.set(k, record);
70
+ }
71
+
72
+ async findDelivery(
73
+ eventId: string,
74
+ triggerId: string,
75
+ ): Promise<BridgeDeliveryRecord | null> {
76
+ return this.deliveries.get(key(eventId, triggerId)) ?? null;
77
+ }
78
+
79
+ async findDeliveryById(id: string): Promise<BridgeDeliveryRecord | null> {
80
+ for (const record of this.deliveries.values()) {
81
+ if (record.id === id) return record;
82
+ }
83
+ return null;
84
+ }
85
+
86
+ async markDelivered(id: string, userRunId: string): Promise<void> {
87
+ const record = this.findById(id);
88
+ record.status = 'delivered';
89
+ record.userRunId = userRunId;
90
+ record.deliveredAt = new Date();
91
+ }
92
+
93
+ async markSkipped(id: string, reason: string): Promise<void> {
94
+ const record = this.findById(id);
95
+ record.status = 'skipped';
96
+ record.skipReason = reason;
97
+ }
98
+
99
+ async markFailed(
100
+ id: string,
101
+ error: Record<string, unknown>,
102
+ ): Promise<void> {
103
+ const record = this.findById(id);
104
+ record.status = 'failed';
105
+ record.error = error;
106
+ }
107
+
108
+ // ─── Test helpers ────────────────────────────────────────────────────────
109
+
110
+ /** All deliveries for a given event (any status, declaration order). */
111
+ getDeliveriesForEvent(eventId: string): BridgeDeliveryRecord[] {
112
+ return [...this.deliveries.values()].filter((r) => r.eventId === eventId);
113
+ }
114
+
115
+ /** All deliveries currently in the given status. */
116
+ getByStatus(
117
+ status: BridgeDeliveryRecord['status'],
118
+ ): BridgeDeliveryRecord[] {
119
+ return [...this.deliveries.values()].filter((r) => r.status === status);
120
+ }
121
+
122
+ /** Reset the store. Tests use this in `beforeEach`. */
123
+ clear(): void {
124
+ this.deliveries.clear();
125
+ }
126
+
127
+ // ─── Internals ───────────────────────────────────────────────────────────
128
+
129
+ private findById(id: string): BridgeDeliveryRecord {
130
+ for (const record of this.deliveries.values()) {
131
+ if (record.id === id) return record;
132
+ }
133
+ throw new Error(
134
+ `MemoryBridgeDeliveryRepo: no delivery with id '${id}' (transition ` +
135
+ `methods may not be called for unknown rows; the framework handler ` +
136
+ `should always findDelivery first or operate on a row it just ` +
137
+ `inserted).`,
138
+ );
139
+ }
140
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Drizzle schema for the `bridge_delivery` ledger (ADR-023 Phase 2, BRIDGE-1).
3
+ *
4
+ * The `bridge_delivery` table is the idempotency ledger for the event-to-job
5
+ * bridge. Every (event, trigger) pair the bridge is asked to spawn produces
6
+ * exactly one row; the `UNIQUE (event_id, trigger_id)` constraint guarantees
7
+ * that:
8
+ *
9
+ * 1. Outbox replays of an event do not double-spawn user job runs — the
10
+ * drain attempts to insert the duplicate, the constraint trips, and the
11
+ * drain skips that trigger.
12
+ * 2. The `IEventFlow.publishAndStart` facade can pre-write a
13
+ * `(status='delivered')` row before the drain runs (Case B from ADR-023
14
+ * §`publishAndStart` + existing `triggers:` collision); the drain then
15
+ * hits UNIQUE on that trigger and skips it while still spawning any
16
+ * other triggers for the same event normally.
17
+ *
18
+ * Status values:
19
+ * - `pending` — wrapper run exists; user job not yet started.
20
+ * - `delivered` — user job started; `user_run_id` populated.
21
+ * - `skipped` — intentional no-op (`when:` returned false, or
22
+ * facade-eager path pre-empted the bridge spawn).
23
+ * - `failed` — wrapper exhausted retry policy; no auto-retry past that
24
+ * (mirrors events outbox stance — ops eyes only).
25
+ *
26
+ * `wrapper_run_id` is **nullable**: the facade-eager path (Case B) pre-writes
27
+ * `bridge_delivery` with no wrapper. The bridge-drain path always populates
28
+ * it.
29
+ *
30
+ * `tenant_id` is emitted **unconditionally and nullable** (per JOB-8
31
+ * 2026-04-20 reversal); enforcement is service-layer (BRIDGE-8) gated on the
32
+ * `BRIDGE_MULTI_TENANT` DI token, not a DB constraint.
33
+ *
34
+ * Indexes:
35
+ * - `bridge_delivery_event_idx` — lookup all deliveries for an event.
36
+ * - `bridge_delivery_status_idx` — partial; ops dashboards filter by
37
+ * `pending | failed`.
38
+ * - `bridge_delivery_user_run_idx` — partial; reverse lookup from a
39
+ * spawned user run back to its delivery row.
40
+ *
41
+ * No service logic, no DI wiring — this is the schema foundation. Backends
42
+ * (memory + drizzle) and the framework handler land in BRIDGE-3 / BRIDGE-4 /
43
+ * BRIDGE-5.
44
+ */
45
+ import {
46
+ index,
47
+ jsonb,
48
+ pgEnum,
49
+ pgTable,
50
+ text,
51
+ timestamp,
52
+ unique,
53
+ uuid,
54
+ } from 'drizzle-orm/pg-core';
55
+ import { sql } from 'drizzle-orm';
56
+ import type { InferSelectModel } from 'drizzle-orm';
57
+
58
+ import { domainEvents } from '../events/domain-events.schema';
59
+ import { jobRuns } from '../jobs/job-orchestration.schema';
60
+
61
+ // ─── Enum ───────────────────────────────────────────────────────────────────
62
+
63
+ export const bridgeDeliveryStatusEnum = pgEnum('bridge_delivery_status', [
64
+ 'pending',
65
+ 'delivered',
66
+ 'skipped',
67
+ 'failed',
68
+ ]);
69
+
70
+ // ─── Table ──────────────────────────────────────────────────────────────────
71
+
72
+ export const bridgeDelivery = pgTable(
73
+ 'bridge_delivery',
74
+ {
75
+ id: uuid('id').primaryKey().defaultRandom(),
76
+ /** FK to the source event in the outbox. */
77
+ eventId: uuid('event_id')
78
+ .notNull()
79
+ .references(() => domainEvents.id),
80
+ /**
81
+ * Stable codegen-emitted identifier for the (job, trigger) pair, of the
82
+ * form `<job_type>#<triggerIndex>` (BRIDGE-6). Forms the second half of
83
+ * the UNIQUE idempotency key.
84
+ */
85
+ triggerId: text('trigger_id').notNull(),
86
+ /**
87
+ * Wrapper `job_run.id` (the framework `@framework/bridge_delivery` run
88
+ * that drove this delivery). Nullable: the facade-eager path
89
+ * (`publishAndStart` Case B) pre-writes a delivered row with no wrapper.
90
+ */
91
+ wrapperRunId: uuid('wrapper_run_id').references(() => jobRuns.id),
92
+ /**
93
+ * Spawned user `job_run.id`. Null until status is `delivered`; remains
94
+ * null for `skipped` and `failed` deliveries.
95
+ */
96
+ userRunId: uuid('user_run_id').references(() => jobRuns.id),
97
+ status: bridgeDeliveryStatusEnum('status').notNull().default('pending'),
98
+ /** Populated when status=`skipped` (e.g. `'when_returned_false'`, `'trigger_unregistered'`). */
99
+ skipReason: text('skip_reason'),
100
+ /** Populated when status=`failed`. Mirrors `job_run.error` shape. */
101
+ error: jsonb('error').$type<Record<string, unknown>>(),
102
+ /**
103
+ * Emitted unconditionally and nullable (JOB-8 / SYNC-6 precedent).
104
+ * Enforcement gated on `BRIDGE_MULTI_TENANT` at the service layer
105
+ * (BRIDGE-8); no DB constraint.
106
+ */
107
+ tenantId: text('tenant_id'),
108
+ attemptedAt: timestamp('attempted_at', { withTimezone: true })
109
+ .notNull()
110
+ .defaultNow(),
111
+ deliveredAt: timestamp('delivered_at', { withTimezone: true }),
112
+ },
113
+ (t) => ({
114
+ /**
115
+ * Idempotency ledger. Outbox replays and facade-vs-drain collisions both
116
+ * dedup through this constraint.
117
+ */
118
+ uqBridgeDeliveryEventTrigger: unique('uq_bridge_delivery_event_trigger').on(
119
+ t.eventId,
120
+ t.triggerId,
121
+ ),
122
+ /** Lookup all deliveries for an event (fanout report, debugging). */
123
+ idxBridgeDeliveryEvent: index('idx_bridge_delivery_event').on(t.eventId),
124
+ /**
125
+ * Ops dashboard filter — only the actionable states. Partial index keeps
126
+ * it small at scale (the bulk of rows will be `delivered`).
127
+ */
128
+ idxBridgeDeliveryStatus: index('idx_bridge_delivery_status')
129
+ .on(t.status)
130
+ .where(sql`${t.status} IN ('pending','failed')`),
131
+ /**
132
+ * Reverse lookup from a spawned user run back to its delivery row.
133
+ * Partial — most rows in the bridge ledger but only successful
134
+ * deliveries have a `user_run_id`.
135
+ */
136
+ idxBridgeDeliveryUserRun: index('idx_bridge_delivery_user_run')
137
+ .on(t.userRunId)
138
+ .where(sql`${t.userRunId} IS NOT NULL`),
139
+ }),
140
+ );
141
+
142
+ export type BridgeDeliveryRecord = InferSelectModel<typeof bridgeDelivery>;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Typed errors for the bridge subsystem (ADR-023 Phase 2, BRIDGE-2).
3
+ *
4
+ * All thrown by the three enforcement sites named in ADR-023 §Multi-tenancy:
5
+ * - `EventFlowService.publishAndStart` entry (BRIDGE-7)
6
+ * - `BridgeDeliveryHandler.handle` entry (BRIDGE-5)
7
+ * - `DrizzleBridgeDeliveryRepo.insertDelivery` pre-write (BRIDGE-4)
8
+ *
9
+ * Same shape as `runtime/subsystems/jobs/jobs-errors.ts` and
10
+ * `runtime/subsystems/events/events-errors.ts` so consumers can catch them
11
+ * with the same exception-filter pattern across all three subsystems.
12
+ */
13
+
14
+ /**
15
+ * Thrown when `BridgeModule` was configured with `multiTenant: true` but
16
+ * the caller did not pass a `tenantId` at one of the three enforcement
17
+ * sites listed above.
18
+ *
19
+ * **Strict enforcement rationale (mirrors JOB-8 / SYNC-6 stance, locked
20
+ * 2026-04-18 for jobs; same rationale applies here).** Cross-tenant data
21
+ * leakage is the worst class of bug a multi-tenant system can ship;
22
+ * surfacing the misuse loudly at the call site (rather than silently
23
+ * defaulting to `null` or to "the last tenant seen") prevents both
24
+ * accidental global writes and sneaky reads that return a union of tenants.
25
+ *
26
+ * - `undefined` `tenantId` → throw this error.
27
+ * - Explicit `null` `tenantId` → passes; opts the call into cross-tenant
28
+ * work (e.g. a system housekeeping event with no owning tenant). The
29
+ * `bridge_delivery` row is persisted with `tenant_id = NULL`.
30
+ *
31
+ * The `callSite` constructor argument names which of the three enforcement
32
+ * sites threw — review reports and ops dashboards rely on a stable site
33
+ * name, so use the canonical strings: `'EventFlowService.publishAndStart'`,
34
+ * `'BridgeDeliveryHandler.handle'`,
35
+ * `'DrizzleBridgeDeliveryRepo.insertDelivery'`.
36
+ */
37
+ export class MissingTenantIdError extends Error {
38
+ override readonly name = 'MissingTenantIdError';
39
+ constructor(public readonly callSite: string) {
40
+ super(
41
+ `MissingTenantIdError: BridgeModule was configured with ` +
42
+ `multiTenant=true but ${callSite} was called without tenantId ` +
43
+ `(undefined). Pass an explicit tenantId, or pass null for ` +
44
+ `cross-tenant work.`,
45
+ );
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Synthetic error thrown by `MemoryBridgeDeliveryRepo.insertDelivery` when
51
+ * a duplicate `(event_id, trigger_id)` insert hits the simulated UNIQUE
52
+ * constraint (BRIDGE-3).
53
+ *
54
+ * Carries a `constraint` field equal to the Drizzle constraint name
55
+ * declared in BRIDGE-1's schema (`uq_bridge_delivery_event_trigger`) so
56
+ * call sites can branch on the same discriminator regardless of which
57
+ * backend is wired up. This matters because ADR-023 explicitly leans on
58
+ * the constraint as the dedup mechanism in two places — outbox replay
59
+ * and `publishAndStart` Case B — and BRIDGE-4 / BRIDGE-7 will share a
60
+ * type-check path with BRIDGE-3-driven tests.
61
+ *
62
+ * The Drizzle backend (BRIDGE-4) does NOT throw this error: it uses
63
+ * `INSERT … ON CONFLICT (event_id, trigger_id) DO NOTHING RETURNING id`
64
+ * per the BRIDGE-4 spec recommendation, so collisions surface as an empty
65
+ * result set rather than an exception. The error exists so the memory
66
+ * backend can faithfully model the "duplicate raises" behaviour for tests
67
+ * that want to assert the constraint actually fires.
68
+ */
69
+ /**
70
+ * Thrown by `BridgeModule.onModuleInit` when `JobWorkerModule` is wired
71
+ * alongside the bridge but its active `pools` list does not include one
72
+ * or more of the three reserved bridge pools (`events_inbound`,
73
+ * `events_change`, `events_outbound`).
74
+ *
75
+ * Without a worker polling those pools, the wrapper `job_run` rows the
76
+ * outbox drain inserts (BRIDGE-4) sit `pending` forever — a silent
77
+ * footgun where `eventFlow.publish(...)` returns success but no user
78
+ * job ever spawns. The boot-time check converts that into a fail-fast.
79
+ *
80
+ * Operators can either (a) add `...BRIDGE_RESERVED_POOLS` to their
81
+ * `JobWorkerModule.forRoot({ pools })` configuration so the same
82
+ * process polls the reserved pools, or (b) run a separate worker
83
+ * process per reserved pool for lane isolation (ADR-022 §Pool
84
+ * isolation).
85
+ */
86
+ export class BridgeReservedPoolsNotPolledError extends Error {
87
+ override readonly name = 'BridgeReservedPoolsNotPolledError';
88
+ constructor(public readonly missingPools: readonly string[]) {
89
+ super(
90
+ `BridgeModule loaded but JobWorkerModule is not polling reserved ` +
91
+ `pool '${missingPools[0]}'. Add ...BRIDGE_RESERVED_POOLS to your ` +
92
+ `JobWorkerModule.forRoot({ pools }) configuration. Missing pools: ` +
93
+ `${missingPools.join(', ')}. (Bridge-fanout wrappers will sit ` +
94
+ `pending forever without these pollers.)`,
95
+ );
96
+ }
97
+ }
98
+
99
+ export class UniqueConstraintError extends Error {
100
+ override readonly name = 'UniqueConstraintError';
101
+ constructor(
102
+ public readonly constraint: string,
103
+ public readonly eventId: string,
104
+ public readonly triggerId: string,
105
+ ) {
106
+ super(
107
+ `UniqueConstraintError: duplicate insert into bridge_delivery for ` +
108
+ `(event_id='${eventId}', trigger_id='${triggerId}') — violates ` +
109
+ `constraint '${constraint}'.`,
110
+ );
111
+ }
112
+ }