@pattern-stack/codegen 0.4.1 → 0.4.3

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 (158) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/runtime/subsystems/bridge/bridge.module.d.ts +1 -0
  3. package/dist/runtime/subsystems/bridge/bridge.module.js +38 -21
  4. package/dist/runtime/subsystems/bridge/bridge.module.js.map +1 -1
  5. package/dist/runtime/subsystems/bridge/index.d.ts +1 -0
  6. package/dist/runtime/subsystems/bridge/index.js +29 -12
  7. package/dist/runtime/subsystems/bridge/index.js.map +1 -1
  8. package/dist/runtime/subsystems/index.js +31 -14
  9. package/dist/runtime/subsystems/index.js.map +1 -1
  10. package/dist/runtime/subsystems/jobs/index.d.ts +1 -0
  11. package/dist/runtime/subsystems/jobs/index.js +27 -10
  12. package/dist/runtime/subsystems/jobs/index.js.map +1 -1
  13. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.d.ts +3 -1
  14. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js +9 -4
  15. package/dist/runtime/subsystems/jobs/job-orchestrator.memory-backend.js.map +1 -1
  16. package/dist/runtime/subsystems/jobs/job-worker.d.ts +3 -1
  17. package/dist/runtime/subsystems/jobs/job-worker.js +6 -2
  18. package/dist/runtime/subsystems/jobs/job-worker.js.map +1 -1
  19. package/dist/runtime/subsystems/jobs/job-worker.module.d.ts +3 -1
  20. package/dist/runtime/subsystems/jobs/job-worker.module.js +27 -10
  21. package/dist/runtime/subsystems/jobs/job-worker.module.js.map +1 -1
  22. package/dist/runtime/subsystems/jobs/jobs-domain.module.js +9 -4
  23. package/dist/runtime/subsystems/jobs/jobs-domain.module.js.map +1 -1
  24. package/dist/src/cli/index.js +29 -2
  25. package/dist/src/cli/index.js.map +1 -1
  26. package/package.json +2 -1
  27. package/runtime/analytics/index.ts +31 -0
  28. package/runtime/analytics/metrics.ts +85 -0
  29. package/runtime/analytics/packs/crm-entity-measures.ts +20 -0
  30. package/runtime/analytics/packs/index.ts +5 -0
  31. package/runtime/analytics/packs/monetary-measures.ts +20 -0
  32. package/runtime/analytics/specs.ts +54 -0
  33. package/runtime/analytics/types.ts +105 -0
  34. package/runtime/base-classes/activity-entity-repository.ts +50 -0
  35. package/runtime/base-classes/activity-entity-service.ts +48 -0
  36. package/runtime/base-classes/base-read-use-cases.ts +88 -0
  37. package/runtime/base-classes/base-repository.ts +289 -0
  38. package/runtime/base-classes/base-service.ts +183 -0
  39. package/runtime/base-classes/index.ts +38 -0
  40. package/runtime/base-classes/knowledge-entity-repository.ts +12 -0
  41. package/runtime/base-classes/knowledge-entity-service.ts +14 -0
  42. package/runtime/base-classes/lifecycle-events.ts +152 -0
  43. package/runtime/base-classes/metadata-entity-repository.ts +80 -0
  44. package/runtime/base-classes/metadata-entity-service.ts +48 -0
  45. package/runtime/base-classes/synced-entity-repository.ts +57 -0
  46. package/runtime/base-classes/synced-entity-service.ts +50 -0
  47. package/runtime/base-classes/with-analytics.ts +22 -0
  48. package/runtime/constants/tokens.ts +29 -0
  49. package/runtime/eav-helpers.ts +74 -0
  50. package/runtime/pipes/zod-validation.pipe.ts +64 -0
  51. package/runtime/shared/openapi/error-response.dto.ts +24 -0
  52. package/runtime/shared/openapi/errors.ts +39 -0
  53. package/runtime/shared/openapi/index.ts +20 -0
  54. package/runtime/shared/openapi/registry.tokens.ts +13 -0
  55. package/runtime/shared/openapi/registry.ts +151 -0
  56. package/runtime/subsystems/analytics/analytics-query.protocol.ts +37 -0
  57. package/runtime/subsystems/analytics/analytics.module.ts +64 -0
  58. package/runtime/subsystems/analytics/analytics.tokens.ts +24 -0
  59. package/runtime/subsystems/analytics/cube-backend.ts +75 -0
  60. package/runtime/subsystems/analytics/index.ts +15 -0
  61. package/runtime/subsystems/analytics/noop-backend.ts +27 -0
  62. package/runtime/subsystems/auth/auth.module.ts +91 -0
  63. package/runtime/subsystems/auth/auth.tokens.ts +27 -0
  64. package/runtime/subsystems/auth/backends/encryption-key/env.ts +76 -0
  65. package/runtime/subsystems/auth/backends/oauth-state-store/in-memory.ts +42 -0
  66. package/runtime/subsystems/auth/index.ts +77 -0
  67. package/runtime/subsystems/auth/protocols/auth-strategy.ts +46 -0
  68. package/runtime/subsystems/auth/protocols/encryption-key.ts +21 -0
  69. package/runtime/subsystems/auth/protocols/integration-store.ts +66 -0
  70. package/runtime/subsystems/auth/protocols/oauth-state-store.ts +16 -0
  71. package/runtime/subsystems/auth/runtime/integration-broken.error.ts +21 -0
  72. package/runtime/subsystems/auth/runtime/oauth2-refresh.strategy.ts +189 -0
  73. package/runtime/subsystems/auth/runtime/session-expired.error.ts +39 -0
  74. package/runtime/subsystems/auth/runtime/with-auth-retry.ts +50 -0
  75. package/runtime/subsystems/bridge/assert-tenant-id.ts +57 -0
  76. package/runtime/subsystems/bridge/bridge-delivery-handler.ts +220 -0
  77. package/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.ts +149 -0
  78. package/runtime/subsystems/bridge/bridge-delivery.memory-backend.ts +140 -0
  79. package/runtime/subsystems/bridge/bridge-delivery.schema.ts +142 -0
  80. package/runtime/subsystems/bridge/bridge-errors.ts +112 -0
  81. package/runtime/subsystems/bridge/bridge-outbox-drain-hook.ts +175 -0
  82. package/runtime/subsystems/bridge/bridge.module.ts +160 -0
  83. package/runtime/subsystems/bridge/bridge.protocol.ts +351 -0
  84. package/runtime/subsystems/bridge/bridge.tokens.ts +68 -0
  85. package/runtime/subsystems/bridge/event-flow.service.ts +175 -0
  86. package/runtime/subsystems/bridge/generated/.gitkeep +0 -0
  87. package/runtime/subsystems/bridge/generated/registry.ts +6 -0
  88. package/runtime/subsystems/bridge/index.ts +84 -0
  89. package/runtime/subsystems/bridge/reserved-pools.ts +36 -0
  90. package/runtime/subsystems/cache/cache.drizzle-backend.ts +150 -0
  91. package/runtime/subsystems/cache/cache.memory-backend.ts +116 -0
  92. package/runtime/subsystems/cache/cache.module.ts +115 -0
  93. package/runtime/subsystems/cache/cache.protocol.ts +45 -0
  94. package/runtime/subsystems/cache/cache.schema.ts +27 -0
  95. package/runtime/subsystems/cache/cache.tokens.ts +17 -0
  96. package/runtime/subsystems/cache/index.ts +22 -0
  97. package/runtime/subsystems/events/domain-events.schema.ts +77 -0
  98. package/runtime/subsystems/events/event-bus.drizzle-backend.ts +327 -0
  99. package/runtime/subsystems/events/event-bus.memory-backend.ts +142 -0
  100. package/runtime/subsystems/events/event-bus.protocol.ts +86 -0
  101. package/runtime/subsystems/events/event-bus.redis-backend.ts +304 -0
  102. package/runtime/subsystems/events/events-errors.ts +30 -0
  103. package/runtime/subsystems/events/events.module.ts +230 -0
  104. package/runtime/subsystems/events/events.tokens.ts +62 -0
  105. package/runtime/subsystems/events/generated/bus.ts +103 -0
  106. package/runtime/subsystems/events/generated/index.ts +7 -0
  107. package/runtime/subsystems/events/generated/registry.ts +84 -0
  108. package/runtime/subsystems/events/generated/schemas.ts +59 -0
  109. package/runtime/subsystems/events/generated/types.ts +94 -0
  110. package/runtime/subsystems/events/index.ts +21 -0
  111. package/runtime/subsystems/index.ts +63 -0
  112. package/runtime/subsystems/jobs/generated/job-orchestration.schema.multi-tenant.ts +217 -0
  113. package/runtime/subsystems/jobs/generated/job-orchestration.schema.single-tenant.ts +217 -0
  114. package/runtime/subsystems/jobs/generated/scope-entity-type.ts +10 -0
  115. package/runtime/subsystems/jobs/index.ts +120 -0
  116. package/runtime/subsystems/jobs/job-handler.base.ts +206 -0
  117. package/runtime/subsystems/jobs/job-orchestration.schema.ts +217 -0
  118. package/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.ts +536 -0
  119. package/runtime/subsystems/jobs/job-orchestrator.memory-backend.ts +860 -0
  120. package/runtime/subsystems/jobs/job-orchestrator.protocol.ts +179 -0
  121. package/runtime/subsystems/jobs/job-run-service.drizzle-backend.ts +171 -0
  122. package/runtime/subsystems/jobs/job-run-service.memory-backend.ts +165 -0
  123. package/runtime/subsystems/jobs/job-run-service.protocol.ts +79 -0
  124. package/runtime/subsystems/jobs/job-step-service.drizzle-backend.ts +66 -0
  125. package/runtime/subsystems/jobs/job-step-service.memory-backend.ts +119 -0
  126. package/runtime/subsystems/jobs/job-step-service.protocol.ts +53 -0
  127. package/runtime/subsystems/jobs/job-worker.module.ts +312 -0
  128. package/runtime/subsystems/jobs/job-worker.ts +624 -0
  129. package/runtime/subsystems/jobs/jobs-domain.module.ts +119 -0
  130. package/runtime/subsystems/jobs/jobs-domain.tokens.ts +30 -0
  131. package/runtime/subsystems/jobs/jobs-errors.ts +150 -0
  132. package/runtime/subsystems/jobs/memory-job-store.ts +35 -0
  133. package/runtime/subsystems/jobs/pool-config.loader.ts +218 -0
  134. package/runtime/subsystems/storage/index.ts +18 -0
  135. package/runtime/subsystems/storage/storage.local-backend.ts +113 -0
  136. package/runtime/subsystems/storage/storage.memory-backend.ts +78 -0
  137. package/runtime/subsystems/storage/storage.module.ts +60 -0
  138. package/runtime/subsystems/storage/storage.protocol.ts +78 -0
  139. package/runtime/subsystems/storage/storage.tokens.ts +9 -0
  140. package/runtime/subsystems/storage/storage.utils.ts +20 -0
  141. package/runtime/subsystems/sync/deep-equal.differ.ts +198 -0
  142. package/runtime/subsystems/sync/execute-sync.use-case.ts +334 -0
  143. package/runtime/subsystems/sync/index.ts +98 -0
  144. package/runtime/subsystems/sync/sync-audit.schema.ts +300 -0
  145. package/runtime/subsystems/sync/sync-change-source.protocol.ts +99 -0
  146. package/runtime/subsystems/sync/sync-cursor-store.drizzle-backend.ts +104 -0
  147. package/runtime/subsystems/sync/sync-cursor-store.memory-backend.ts +64 -0
  148. package/runtime/subsystems/sync/sync-cursor-store.protocol.ts +53 -0
  149. package/runtime/subsystems/sync/sync-errors.ts +54 -0
  150. package/runtime/subsystems/sync/sync-field-diff.protocol.ts +61 -0
  151. package/runtime/subsystems/sync/sync-loopback.protocol.ts +33 -0
  152. package/runtime/subsystems/sync/sync-run-recorder.drizzle-backend.ts +123 -0
  153. package/runtime/subsystems/sync/sync-run-recorder.memory-backend.ts +143 -0
  154. package/runtime/subsystems/sync/sync-run-recorder.protocol.ts +86 -0
  155. package/runtime/subsystems/sync/sync-sink.protocol.ts +55 -0
  156. package/runtime/subsystems/sync/sync.module.ts +156 -0
  157. package/runtime/subsystems/sync/sync.tokens.ts +57 -0
  158. package/runtime/types/drizzle.ts +23 -0
@@ -0,0 +1,230 @@
1
+ /**
2
+ * EventsModule — DynamicModule factory for the event bus subsystem.
3
+ *
4
+ * Register once in AppModule:
5
+ * ```typescript
6
+ * @Module({
7
+ * imports: [
8
+ * EventsModule.forRoot({ backend: 'drizzle' }),
9
+ * ],
10
+ * })
11
+ * export class AppModule {}
12
+ * ```
13
+ *
14
+ * Tests swap to the memory backend without touching application code:
15
+ * ```typescript
16
+ * Test.createTestingModule({
17
+ * imports: [EventsModule.forRoot({ backend: 'memory' })],
18
+ * });
19
+ * ```
20
+ *
21
+ * Per-pool drain isolation (EVT-4):
22
+ * ```typescript
23
+ * EventsModule.forRoot({ backend: 'drizzle', pools: ['events_change'] });
24
+ * ```
25
+ * Each process restricts its drain loop to the pools listed here. `pools`
26
+ * is undefined by default → drain all pending rows (backwards-compatible).
27
+ *
28
+ * Typed facade + multi-tenancy (EVT-6):
29
+ * - `TYPED_EVENT_BUS` resolves to the generated `TypedEventBus` wrapping
30
+ * whichever backend is selected.
31
+ * - `multiTenant: true` makes `TypedEventBus.publish` throw
32
+ * `MissingTenantIdError` when the caller forgets `metadata.tenantId`.
33
+ *
34
+ * `global: true` means entity modules do not need to import EventsModule
35
+ * individually — the EVENT_BUS and TYPED_EVENT_BUS tokens are available
36
+ * project-wide.
37
+ *
38
+ * Async configuration (`forRootAsync`):
39
+ * The async factory returns `EventsModuleOptions`; the EVENT_BUS provider
40
+ * then receives its backend dependencies — DRIZZLE for the drizzle
41
+ * backend, REDIS_URL for the redis backend, the resolved options for the
42
+ * memory backend — through a proper `useFactory` so Nest DI wires them
43
+ * correctly. Earlier revisions hand-constructed backends with
44
+ * `new Class()` which silently left `db` / `redisUrl` undefined
45
+ * (issue #108).
46
+ */
47
+ import { Module, type DynamicModule, type Provider } from '@nestjs/common';
48
+ import {
49
+ EVENT_BUS,
50
+ EVENTS_MODULE_OPTIONS,
51
+ EVENTS_MULTI_TENANT,
52
+ REDIS_URL,
53
+ TYPED_EVENT_BUS,
54
+ } from './events.tokens';
55
+ import { DRIZZLE } from '../../constants/tokens';
56
+ import type { DrizzleClient } from '../../types/drizzle';
57
+ import { DrizzleEventBus } from './event-bus.drizzle-backend';
58
+ import { MemoryEventBus } from './event-bus.memory-backend';
59
+ import { RedisEventBus } from './event-bus.redis-backend';
60
+ import { TypedEventBus } from './generated/bus';
61
+
62
+ export interface EventsModuleOptions {
63
+ backend: 'drizzle' | 'memory' | 'redis';
64
+ /**
65
+ * Redis connection URL used when `backend` is `'redis'`.
66
+ * Falls back to the REDIS_URL environment variable, then
67
+ * `redis://localhost:6379` if neither is set.
68
+ */
69
+ redisUrl?: string;
70
+ /**
71
+ * Restrict the drain loop to these pools. Each pool name matches the
72
+ * `domain_events.pool` column (populated from `event.metadata.pool` at
73
+ * publish time). Leave undefined to drain all pending rows.
74
+ *
75
+ * Typical lane split: one process per `events_inbound` /
76
+ * `events_change` / `events_outbound` so a slow outbound handler
77
+ * cannot stall change-event propagation (see ADR-022).
78
+ */
79
+ pools?: string[];
80
+ /**
81
+ * Multi-tenancy opt-in (EVT-6).
82
+ *
83
+ * When `true`, every `TypedEventBus.publish()` call must supply
84
+ * `opts.metadata.tenantId` — otherwise it throws `MissingTenantIdError`.
85
+ * The tenantId is preserved on `event.metadata` and, for the Drizzle
86
+ * backend, written to `domain_events.tenant_id` (EVT-4).
87
+ *
88
+ * Drain-side tenant filtering is deferred — the tenant-context model
89
+ * (per-process vs. per-request vs. async-local-storage) is still
90
+ * unsettled; see ADR-024 §Multi-tenancy. Only the publish-side
91
+ * requirement ships here.
92
+ *
93
+ * Defaults to `false`.
94
+ */
95
+ multiTenant?: boolean;
96
+ }
97
+
98
+ export interface EventsModuleAsyncOptions {
99
+ useFactory: (...args: unknown[]) => Promise<EventsModuleOptions> | EventsModuleOptions;
100
+ inject?: unknown[];
101
+ imports?: unknown[];
102
+ }
103
+
104
+ /**
105
+ * Shared provider set: `TypedEventBus` itself, the `TYPED_EVENT_BUS` token
106
+ * binding, and the resolved `EVENTS_MULTI_TENANT` flag. Returned from one
107
+ * place so every `forRoot` branch and `forRootAsync` agree.
108
+ */
109
+ function buildTypedBusProviders(multiTenant: boolean): Provider[] {
110
+ return [
111
+ TypedEventBus,
112
+ { provide: TYPED_EVENT_BUS, useExisting: TypedEventBus },
113
+ { provide: EVENTS_MULTI_TENANT, useValue: multiTenant },
114
+ ];
115
+ }
116
+
117
+ /**
118
+ * Construct the backend instance for the async path, routing constructor
119
+ * arguments through Nest-resolved dependencies.
120
+ *
121
+ * DRIZZLE is declared optional at inject time so that memory-backend
122
+ * consumers aren't required to also import `DatabaseModule`. If the
123
+ * drizzle backend is selected but no DRIZZLE provider is registered, we
124
+ * throw a clear error instead of silently constructing a broken bus.
125
+ */
126
+ function buildEventBusAsync(
127
+ options: EventsModuleOptions,
128
+ db: DrizzleClient | null,
129
+ redisUrl: string,
130
+ ): unknown {
131
+ if (options.backend === 'drizzle') {
132
+ if (!db) {
133
+ throw new Error(
134
+ "EventsModule.forRootAsync: backend: 'drizzle' selected but DRIZZLE provider is not available. " +
135
+ 'Ensure DatabaseModule (or another provider exposing DRIZZLE) is imported before EventsModule.forRootAsync.',
136
+ );
137
+ }
138
+ return new DrizzleEventBus(db, options);
139
+ }
140
+ if (options.backend === 'redis') {
141
+ return new RedisEventBus(redisUrl);
142
+ }
143
+ return new MemoryEventBus(options);
144
+ }
145
+
146
+ @Module({})
147
+ export class EventsModule {
148
+ static forRootAsync(asyncOptions: EventsModuleAsyncOptions): DynamicModule {
149
+ return {
150
+ module: EventsModule,
151
+ global: true,
152
+ imports: (asyncOptions.imports ?? []) as Parameters<typeof Module>[0]['imports'],
153
+ providers: [
154
+ {
155
+ provide: EVENTS_MODULE_OPTIONS,
156
+ useFactory: asyncOptions.useFactory,
157
+ inject: (asyncOptions.inject ?? []) as (string | symbol | Function)[],
158
+ },
159
+ {
160
+ provide: EVENTS_MULTI_TENANT,
161
+ useFactory: (options: EventsModuleOptions) => options.multiTenant ?? false,
162
+ inject: [EVENTS_MODULE_OPTIONS],
163
+ },
164
+ {
165
+ provide: REDIS_URL,
166
+ useFactory: (options: EventsModuleOptions) =>
167
+ options.redisUrl ?? process.env['REDIS_URL'] ?? 'redis://localhost:6379',
168
+ inject: [EVENTS_MODULE_OPTIONS],
169
+ },
170
+ {
171
+ provide: EVENT_BUS,
172
+ useFactory: (
173
+ options: EventsModuleOptions,
174
+ db: DrizzleClient | null,
175
+ redisUrl: string,
176
+ ) => buildEventBusAsync(options, db, redisUrl),
177
+ inject: [
178
+ EVENTS_MODULE_OPTIONS,
179
+ { token: DRIZZLE, optional: true },
180
+ REDIS_URL,
181
+ ],
182
+ },
183
+ TypedEventBus,
184
+ { provide: TYPED_EVENT_BUS, useExisting: TypedEventBus },
185
+ ],
186
+ exports: [EVENT_BUS, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT],
187
+ };
188
+ }
189
+
190
+ static forRoot(
191
+ options: EventsModuleOptions = { backend: 'drizzle' },
192
+ ): DynamicModule {
193
+ const multiTenant = options.multiTenant ?? false;
194
+
195
+ if (options.backend === 'redis') {
196
+ const resolvedUrl =
197
+ options.redisUrl ?? process.env['REDIS_URL'] ?? 'redis://localhost:6379';
198
+
199
+ return {
200
+ module: EventsModule,
201
+ global: true,
202
+ providers: [
203
+ { provide: EVENTS_MODULE_OPTIONS, useValue: options },
204
+ { provide: REDIS_URL, useValue: resolvedUrl },
205
+ { provide: EVENT_BUS, useClass: RedisEventBus },
206
+ // Register concrete class so NestJS can resolve lifecycle hooks
207
+ RedisEventBus,
208
+ ...buildTypedBusProviders(multiTenant),
209
+ ],
210
+ exports: [EVENT_BUS, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT],
211
+ };
212
+ }
213
+
214
+ const provider =
215
+ options.backend === 'drizzle'
216
+ ? { provide: EVENT_BUS, useClass: DrizzleEventBus }
217
+ : { provide: EVENT_BUS, useClass: MemoryEventBus };
218
+
219
+ return {
220
+ module: EventsModule,
221
+ global: true,
222
+ providers: [
223
+ { provide: EVENTS_MODULE_OPTIONS, useValue: options },
224
+ provider,
225
+ ...buildTypedBusProviders(multiTenant),
226
+ ],
227
+ exports: [EVENT_BUS, TYPED_EVENT_BUS, EVENTS_MULTI_TENANT],
228
+ };
229
+ }
230
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Injection token for the event bus.
3
+ *
4
+ * String constant (not Symbol) so it matches by value across import boundaries.
5
+ * Matches the token in runtime/constants/tokens.ts — both are 'EVENT_BUS'.
6
+ *
7
+ * Usage in use cases:
8
+ * ```typescript
9
+ * constructor(@Inject(EVENT_BUS) private readonly eventBus: IEventBus) {}
10
+ * ```
11
+ */
12
+ export const EVENT_BUS = 'EVENT_BUS' as const;
13
+
14
+ /**
15
+ * Injection token for the generated `TypedEventBus` facade.
16
+ *
17
+ * `TypedEventBus` lives in `runtime/subsystems/events/generated/bus.ts` and
18
+ * wraps `IEventBus` with project-specific `AppDomainEvent`-typed `publish<T>()`
19
+ * and `subscribe<T>()`. Use cases inject this token in preference to
20
+ * `EVENT_BUS` when they want compile-time type safety on event shapes.
21
+ *
22
+ * String constant (not Symbol) so it matches by value across import
23
+ * boundaries — same convention as `EVENT_BUS`.
24
+ *
25
+ * Provider registration lands in EVT-6 (EventsModule wiring); the token is
26
+ * declared here so generated code can import it without depending on the
27
+ * still-being-formalised module.
28
+ */
29
+ export const TYPED_EVENT_BUS = 'TYPED_EVENT_BUS' as const;
30
+
31
+ /**
32
+ * Injection token for the resolved multi-tenancy flag.
33
+ *
34
+ * Provided by `EventsModule.forRoot(...)` as `options.multiTenant ?? false`.
35
+ * Consumed by `TypedEventBus` to enforce the tenantId-is-required rule at
36
+ * publish time.
37
+ *
38
+ * String constant (not Symbol) so it matches by value across import
39
+ * boundaries — same convention as the other events tokens. (The jobs
40
+ * subsystem uses Symbols for the analogous token; events chose strings
41
+ * from the start and we keep the file internally consistent.)
42
+ */
43
+ export const EVENTS_MULTI_TENANT = 'EVENTS_MULTI_TENANT' as const;
44
+
45
+ /**
46
+ * Injection token for the Redis connection URL used by RedisEventBus.
47
+ * Provided automatically by EventsModule.forRoot({ backend: 'redis' }).
48
+ */
49
+ export const REDIS_URL = Symbol('REDIS_URL');
50
+
51
+ /**
52
+ * Injection token for the resolved `EventsModuleOptions` object.
53
+ *
54
+ * Provided automatically by `EventsModule.forRoot(...)` /
55
+ * `EventsModule.forRootAsync(...)`. Backends that need to observe module
56
+ * configuration (e.g. `DrizzleEventBus` reading `opts.pools` for
57
+ * pool-filtered drain) inject via this token.
58
+ *
59
+ * String-valued (not `Symbol`) so it matches by value across import
60
+ * boundaries — same convention as `EVENT_BUS`.
61
+ */
62
+ export const EVENTS_MODULE_OPTIONS = 'EVENTS_MODULE_OPTIONS' as const;
@@ -0,0 +1,103 @@
1
+ // AUTO-GENERATED by @pattern-stack/codegen. Do not edit.
2
+ // Run `codegen entity new --all` to refresh.
3
+
4
+ import { Injectable, Inject } from '@nestjs/common';
5
+ import { randomUUID } from 'crypto';
6
+ import { EVENT_BUS, EVENTS_MULTI_TENANT } from '../events.tokens';
7
+ import { MissingTenantIdError } from '../events-errors';
8
+ import type { IEventBus, DrizzleTransaction } from '../event-bus.protocol';
9
+ import { eventPayloadSchemas } from './schemas';
10
+ import { getEventMetadata } from './registry';
11
+ import type { EventTypeName, EventOfType, PayloadOfType } from './types';
12
+
13
+ /**
14
+ * Typed facade over IEventBus.
15
+ *
16
+ * Stamps `pool`, `direction`, and `version` into `event.metadata` from
17
+ * the generated `eventRegistry` before delegating to `IEventBus.publish()`.
18
+ * Downstream backends (DrizzleEventBus) read those values to populate the
19
+ * explicit `domain_events` columns.
20
+ *
21
+ * Validation gating (EVT-Q5): `CODEGEN_EVENT_VALIDATE` env flag, default on.
22
+ * Uses `safeParse` + `console.warn` — never throws, so a bad publish does
23
+ * not crash a hot path.
24
+ *
25
+ * Multi-tenancy (EVT-6): when the EventsModule is configured with
26
+ * `multiTenant: true`, every publish must supply `opts.metadata.tenantId`
27
+ * — otherwise `publish()` throws `MissingTenantIdError`. When `multiTenant`
28
+ * is `false` (default), no tenantId is required. If a tenantId IS supplied,
29
+ * it is preserved on `event.metadata` and the Drizzle backend writes it to
30
+ * `domain_events.tenant_id` (EVT-4).
31
+ */
32
+ @Injectable()
33
+ export class TypedEventBus {
34
+ constructor(
35
+ @Inject(EVENT_BUS) private readonly bus: IEventBus,
36
+ @Inject(EVENTS_MULTI_TENANT) private readonly multiTenant: boolean,
37
+ ) {}
38
+
39
+ async publish<T extends EventTypeName>(
40
+ type: T,
41
+ aggregateId: string,
42
+ payload: PayloadOfType<T>,
43
+ opts?: { tx?: DrizzleTransaction; metadata?: Record<string, unknown> },
44
+ ): Promise<void> {
45
+ const meta = getEventMetadata(type);
46
+
47
+ const flag = process.env['CODEGEN_EVENT_VALIDATE'];
48
+ const shouldValidate =
49
+ flag === undefined ? true : flag !== 'false' && flag !== '0';
50
+ if (shouldValidate) {
51
+ // `eventPayloadSchemas` is typed as `Record<EventTypeName, z.ZodType>`,
52
+ // so under `noUncheckedIndexedAccess` the indexed lookup widens
53
+ // to `z.ZodType | undefined`. When no events are registered at
54
+ // codegen time `EventTypeName` degrades to `string` and the
55
+ // schemas object is literally `{}` — the guard below is the
56
+ // honest handling of that empty-registry case (skip validation;
57
+ // it's a warn-only best-effort check per the class docblock).
58
+ const schema = eventPayloadSchemas[type];
59
+ if (schema) {
60
+ const check = schema.safeParse(payload);
61
+ if (!check.success) {
62
+ console.warn(
63
+ `[TypedEventBus] payload validation failed for ${String(type)}:`,
64
+ check.error.issues,
65
+ );
66
+ }
67
+ }
68
+ }
69
+
70
+ const tenantId = opts?.metadata?.['tenantId'];
71
+ if (this.multiTenant && (tenantId === undefined || tenantId === null)) {
72
+ throw new MissingTenantIdError(type as string);
73
+ }
74
+
75
+ const aggregateType =
76
+ meta.aggregate ?? meta.source ?? meta.destination ?? (type as string);
77
+
78
+ await this.bus.publish(
79
+ {
80
+ id: randomUUID(),
81
+ type,
82
+ aggregateId,
83
+ aggregateType,
84
+ payload: payload as Record<string, unknown>,
85
+ occurredAt: new Date(),
86
+ metadata: {
87
+ ...(opts?.metadata ?? {}),
88
+ pool: meta.pool,
89
+ direction: meta.direction,
90
+ version: meta.version,
91
+ },
92
+ },
93
+ opts?.tx,
94
+ );
95
+ }
96
+
97
+ subscribe<T extends EventTypeName>(
98
+ type: T,
99
+ handler: (event: EventOfType<T>) => Promise<void>,
100
+ ): () => void {
101
+ return this.bus.subscribe<EventOfType<T>>(type, handler as never);
102
+ }
103
+ }
@@ -0,0 +1,7 @@
1
+ // AUTO-GENERATED by @pattern-stack/codegen. Do not edit.
2
+ // Run `codegen entity new --all` to refresh.
3
+
4
+ export * from './types';
5
+ export * from './schemas';
6
+ export * from './registry';
7
+ export * from './bus';
@@ -0,0 +1,84 @@
1
+ // AUTO-GENERATED by @pattern-stack/codegen. Do not edit.
2
+ // Run `codegen entity new --all` to refresh.
3
+
4
+
5
+ import type { EventTypeName } from './types';
6
+
7
+ export interface EventMetadata {
8
+ type: EventTypeName;
9
+ direction: 'inbound' | 'change' | 'outbound';
10
+ pool: 'events_inbound' | 'events_change' | 'events_outbound';
11
+ aggregate?: string;
12
+ source?: string;
13
+ destination?: string;
14
+ version: number;
15
+ retry: { attempts: number; backoff: 'linear' | 'exponential' };
16
+ }
17
+
18
+ export const eventRegistry = {
19
+ 'contact_created': {
20
+ type: 'contact_created',
21
+ direction: 'change',
22
+ pool: 'events_change',
23
+ aggregate: 'contact',
24
+ version: 1,
25
+ retry: { attempts: 3, backoff: 'exponential' },
26
+ },
27
+ 'contact_marked_champion': {
28
+ type: 'contact_marked_champion',
29
+ direction: 'change',
30
+ pool: 'events_change',
31
+ aggregate: 'contact',
32
+ version: 1,
33
+ retry: { attempts: 3, backoff: 'exponential' },
34
+ },
35
+ 'contact_merged': {
36
+ type: 'contact_merged',
37
+ direction: 'change',
38
+ pool: 'events_change',
39
+ aggregate: 'contact',
40
+ version: 1,
41
+ retry: { attempts: 3, backoff: 'exponential' },
42
+ },
43
+ 'deal_created': {
44
+ type: 'deal_created',
45
+ direction: 'change',
46
+ pool: 'events_change',
47
+ aggregate: 'deal',
48
+ version: 1,
49
+ retry: { attempts: 3, backoff: 'exponential' },
50
+ },
51
+ 'deal_stage_changed': {
52
+ type: 'deal_stage_changed',
53
+ direction: 'change',
54
+ pool: 'events_change',
55
+ aggregate: 'deal',
56
+ version: 1,
57
+ retry: { attempts: 3, backoff: 'exponential' },
58
+ },
59
+ 'stripe_payment_received': {
60
+ type: 'stripe_payment_received',
61
+ direction: 'inbound',
62
+ pool: 'events_inbound',
63
+ source: 'stripe',
64
+ version: 1,
65
+ retry: { attempts: 5, backoff: 'exponential' },
66
+ },
67
+ 'webhook_outbound_contact_sync': {
68
+ type: 'webhook_outbound_contact_sync',
69
+ direction: 'outbound',
70
+ pool: 'events_outbound',
71
+ aggregate: 'contact',
72
+ destination: 'crm',
73
+ version: 1,
74
+ retry: { attempts: 3, backoff: 'exponential' },
75
+ },
76
+ } as const satisfies Record<EventTypeName, EventMetadata>;
77
+
78
+ export function getEventMetadata<T extends EventTypeName>(type: T): EventMetadata {
79
+ const meta = eventRegistry[type];
80
+ if (!meta) {
81
+ throw new Error(`No registry entry for event type '${String(type)}' — declare events under events/*.yaml and re-run \`codegen entity new --all\`.`);
82
+ }
83
+ return meta;
84
+ }
@@ -0,0 +1,59 @@
1
+ // AUTO-GENERATED by @pattern-stack/codegen. Do not edit.
2
+ // Run `codegen entity new --all` to refresh.
3
+
4
+
5
+ import { z } from 'zod';
6
+ import type { EventTypeName } from './types';
7
+
8
+ export const contactCreatedPayloadSchema = z.object({
9
+ accountId: z.string().uuid().nullable(),
10
+ contactId: z.string().uuid(),
11
+ createdBy: z.string().uuid(),
12
+ }).strict();
13
+
14
+ export const contactMarkedChampionPayloadSchema = z.object({
15
+ contactId: z.string().uuid(),
16
+ opportunityId: z.string().uuid(),
17
+ }).strict();
18
+
19
+ export const contactMergedPayloadSchema = z.object({
20
+ mergedBy: z.string().uuid(),
21
+ sourceId: z.string().uuid(),
22
+ targetId: z.string().uuid(),
23
+ }).strict();
24
+
25
+ export const dealCreatedPayloadSchema = z.object({
26
+ accountId: z.string().uuid(),
27
+ dealId: z.string().uuid(),
28
+ ownerId: z.string().uuid(),
29
+ }).strict();
30
+
31
+ export const dealStageChangedPayloadSchema = z.object({
32
+ dealId: z.string().uuid(),
33
+ newStage: z.string(),
34
+ oldStage: z.string(),
35
+ }).strict();
36
+
37
+ export const stripePaymentReceivedPayloadSchema = z.object({
38
+ amountCents: z.number(),
39
+ currency: z.string(),
40
+ customerId: z.string(),
41
+ eventId: z.string(),
42
+ receivedAt: z.coerce.date(),
43
+ }).strict();
44
+
45
+ export const webhookOutboundContactSyncPayloadSchema = z.object({
46
+ contactId: z.string().uuid(),
47
+ occurredAt: z.coerce.date(),
48
+ operation: z.string(),
49
+ }).strict();
50
+
51
+ export const eventPayloadSchemas = {
52
+ 'contact_created': contactCreatedPayloadSchema,
53
+ 'contact_marked_champion': contactMarkedChampionPayloadSchema,
54
+ 'contact_merged': contactMergedPayloadSchema,
55
+ 'deal_created': dealCreatedPayloadSchema,
56
+ 'deal_stage_changed': dealStageChangedPayloadSchema,
57
+ 'stripe_payment_received': stripePaymentReceivedPayloadSchema,
58
+ 'webhook_outbound_contact_sync': webhookOutboundContactSyncPayloadSchema,
59
+ } as const satisfies Record<EventTypeName, z.ZodType>;
@@ -0,0 +1,94 @@
1
+ // AUTO-GENERATED by @pattern-stack/codegen. Do not edit.
2
+ // Run `codegen entity new --all` to refresh.
3
+
4
+
5
+ import type { DomainEvent } from '../event-bus.protocol';
6
+
7
+ /** Emitted after a contact row is committed. */
8
+ export interface ContactCreatedEvent extends DomainEvent {
9
+ readonly type: 'contact_created';
10
+ readonly aggregateType: 'contact';
11
+ readonly payload: {
12
+ accountId: string | null;
13
+ contactId: string;
14
+ createdBy: string;
15
+ };
16
+ }
17
+
18
+ export interface ContactMarkedChampionEvent extends DomainEvent {
19
+ readonly type: 'contact_marked_champion';
20
+ readonly aggregateType: 'contact';
21
+ readonly payload: {
22
+ contactId: string;
23
+ opportunityId: string;
24
+ };
25
+ }
26
+
27
+ export interface ContactMergedEvent extends DomainEvent {
28
+ readonly type: 'contact_merged';
29
+ readonly aggregateType: 'contact';
30
+ readonly payload: {
31
+ mergedBy: string;
32
+ sourceId: string;
33
+ targetId: string;
34
+ };
35
+ }
36
+
37
+ export interface DealCreatedEvent extends DomainEvent {
38
+ readonly type: 'deal_created';
39
+ readonly aggregateType: 'deal';
40
+ readonly payload: {
41
+ accountId: string;
42
+ dealId: string;
43
+ ownerId: string;
44
+ };
45
+ }
46
+
47
+ export interface DealStageChangedEvent extends DomainEvent {
48
+ readonly type: 'deal_stage_changed';
49
+ readonly aggregateType: 'deal';
50
+ readonly payload: {
51
+ dealId: string;
52
+ newStage: string;
53
+ oldStage: string;
54
+ };
55
+ }
56
+
57
+ /** Stripe charge.succeeded webhook, post-signature-verification. */
58
+ export interface StripePaymentReceivedEvent extends DomainEvent {
59
+ readonly type: 'stripe_payment_received';
60
+ readonly aggregateType: 'stripe';
61
+ readonly payload: {
62
+ amountCents: number;
63
+ currency: string;
64
+ customerId: string;
65
+ /** Stripe event id (evt_...) */
66
+ eventId: string;
67
+ receivedAt: Date;
68
+ };
69
+ }
70
+
71
+ /** Outbound notification to the configured CRM when a contact changes. */
72
+ export interface WebhookOutboundContactSyncEvent extends DomainEvent {
73
+ readonly type: 'webhook_outbound_contact_sync';
74
+ readonly aggregateType: 'contact';
75
+ readonly payload: {
76
+ contactId: string;
77
+ occurredAt: Date;
78
+ /** create | update | delete */
79
+ operation: string;
80
+ };
81
+ }
82
+
83
+ export type AppDomainEvent =
84
+ | ContactCreatedEvent
85
+ | ContactMarkedChampionEvent
86
+ | ContactMergedEvent
87
+ | DealCreatedEvent
88
+ | DealStageChangedEvent
89
+ | StripePaymentReceivedEvent
90
+ | WebhookOutboundContactSyncEvent;
91
+
92
+ export type EventTypeName = AppDomainEvent['type'];
93
+ export type EventOfType<T extends EventTypeName> = Extract<AppDomainEvent, { type: T }>;
94
+ export type PayloadOfType<T extends EventTypeName> = EventOfType<T>['payload'];
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Events subsystem — public API
3
+ *
4
+ * Import the module in AppModule, inject the bus via EVENT_BUS token.
5
+ */
6
+ export type { DomainEvent, IEventBus, DrizzleTransaction } from './event-bus.protocol';
7
+ export {
8
+ EVENT_BUS,
9
+ EVENTS_MODULE_OPTIONS,
10
+ EVENTS_MULTI_TENANT,
11
+ TYPED_EVENT_BUS,
12
+ } from './events.tokens';
13
+ export { TypedEventBus } from './generated/bus';
14
+ export { MissingTenantIdError } from './events-errors';
15
+ export { EventsModule } from './events.module';
16
+ export type { EventsModuleOptions } from './events.module';
17
+ export { MemoryEventBus } from './event-bus.memory-backend';
18
+ export { DrizzleEventBus } from './event-bus.drizzle-backend';
19
+ export { RedisEventBus } from './event-bus.redis-backend';
20
+ export { domainEvents } from './domain-events.schema';
21
+ export type { DomainEventRecord } from './domain-events.schema';