@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.
- package/CHANGELOG.md +14 -0
- package/dist/src/cli/index.js +1616 -1070
- package/dist/src/cli/index.js.map +1 -1
- package/package.json +3 -1
- package/runtime/analytics/index.ts +31 -0
- package/runtime/analytics/metrics.ts +85 -0
- package/runtime/analytics/packs/crm-entity-measures.ts +20 -0
- package/runtime/analytics/packs/index.ts +5 -0
- package/runtime/analytics/packs/monetary-measures.ts +20 -0
- package/runtime/analytics/specs.ts +54 -0
- package/runtime/analytics/types.ts +105 -0
- package/runtime/base-classes/activity-entity-repository.ts +50 -0
- package/runtime/base-classes/activity-entity-service.ts +48 -0
- package/runtime/base-classes/base-read-use-cases.ts +88 -0
- package/runtime/base-classes/base-repository.ts +289 -0
- package/runtime/base-classes/base-service.ts +183 -0
- package/runtime/base-classes/index.ts +38 -0
- package/runtime/base-classes/knowledge-entity-repository.ts +12 -0
- package/runtime/base-classes/knowledge-entity-service.ts +14 -0
- package/runtime/base-classes/lifecycle-events.ts +152 -0
- package/runtime/base-classes/metadata-entity-repository.ts +80 -0
- package/runtime/base-classes/metadata-entity-service.ts +48 -0
- package/runtime/base-classes/synced-entity-repository.ts +57 -0
- package/runtime/base-classes/synced-entity-service.ts +50 -0
- package/runtime/base-classes/with-analytics.ts +22 -0
- package/runtime/constants/tokens.ts +29 -0
- package/runtime/eav-helpers.ts +74 -0
- package/runtime/pipes/zod-validation.pipe.ts +64 -0
- package/runtime/shared/openapi/error-response.dto.ts +24 -0
- package/runtime/shared/openapi/errors.ts +39 -0
- package/runtime/shared/openapi/index.ts +20 -0
- package/runtime/shared/openapi/registry.tokens.ts +13 -0
- package/runtime/shared/openapi/registry.ts +151 -0
- package/runtime/subsystems/analytics/analytics-query.protocol.ts +37 -0
- package/runtime/subsystems/analytics/analytics.module.ts +64 -0
- package/runtime/subsystems/analytics/analytics.tokens.ts +24 -0
- package/runtime/subsystems/analytics/cube-backend.ts +75 -0
- package/runtime/subsystems/analytics/index.ts +15 -0
- package/runtime/subsystems/analytics/noop-backend.ts +27 -0
- package/runtime/subsystems/auth/auth.module.ts +91 -0
- package/runtime/subsystems/auth/auth.tokens.ts +27 -0
- package/runtime/subsystems/auth/backends/encryption-key/env.ts +76 -0
- package/runtime/subsystems/auth/backends/oauth-state-store/in-memory.ts +42 -0
- package/runtime/subsystems/auth/index.ts +77 -0
- package/runtime/subsystems/auth/protocols/auth-strategy.ts +46 -0
- package/runtime/subsystems/auth/protocols/encryption-key.ts +21 -0
- package/runtime/subsystems/auth/protocols/integration-store.ts +66 -0
- package/runtime/subsystems/auth/protocols/oauth-state-store.ts +16 -0
- package/runtime/subsystems/auth/runtime/integration-broken.error.ts +21 -0
- package/runtime/subsystems/auth/runtime/oauth2-refresh.strategy.ts +189 -0
- package/runtime/subsystems/auth/runtime/session-expired.error.ts +39 -0
- package/runtime/subsystems/auth/runtime/with-auth-retry.ts +50 -0
- package/runtime/subsystems/bridge/assert-tenant-id.ts +57 -0
- package/runtime/subsystems/bridge/bridge-delivery-handler.ts +220 -0
- package/runtime/subsystems/bridge/bridge-delivery.drizzle-backend.ts +149 -0
- package/runtime/subsystems/bridge/bridge-delivery.memory-backend.ts +140 -0
- package/runtime/subsystems/bridge/bridge-delivery.schema.ts +142 -0
- package/runtime/subsystems/bridge/bridge-errors.ts +112 -0
- package/runtime/subsystems/bridge/bridge-outbox-drain-hook.ts +175 -0
- package/runtime/subsystems/bridge/bridge.module.ts +160 -0
- package/runtime/subsystems/bridge/bridge.protocol.ts +351 -0
- package/runtime/subsystems/bridge/bridge.tokens.ts +68 -0
- package/runtime/subsystems/bridge/event-flow.service.ts +175 -0
- package/runtime/subsystems/bridge/generated/.gitkeep +0 -0
- package/runtime/subsystems/bridge/generated/registry.ts +6 -0
- package/runtime/subsystems/bridge/index.ts +84 -0
- package/runtime/subsystems/bridge/reserved-pools.ts +36 -0
- package/runtime/subsystems/cache/cache.drizzle-backend.ts +150 -0
- package/runtime/subsystems/cache/cache.memory-backend.ts +116 -0
- package/runtime/subsystems/cache/cache.module.ts +115 -0
- package/runtime/subsystems/cache/cache.protocol.ts +45 -0
- package/runtime/subsystems/cache/cache.schema.ts +27 -0
- package/runtime/subsystems/cache/cache.tokens.ts +17 -0
- package/runtime/subsystems/cache/index.ts +22 -0
- package/runtime/subsystems/events/domain-events.schema.ts +77 -0
- package/runtime/subsystems/events/event-bus.drizzle-backend.ts +327 -0
- package/runtime/subsystems/events/event-bus.memory-backend.ts +142 -0
- package/runtime/subsystems/events/event-bus.protocol.ts +86 -0
- package/runtime/subsystems/events/event-bus.redis-backend.ts +304 -0
- package/runtime/subsystems/events/events-errors.ts +30 -0
- package/runtime/subsystems/events/events.module.ts +230 -0
- package/runtime/subsystems/events/events.tokens.ts +62 -0
- package/runtime/subsystems/events/generated/bus.ts +103 -0
- package/runtime/subsystems/events/generated/index.ts +7 -0
- package/runtime/subsystems/events/generated/registry.ts +84 -0
- package/runtime/subsystems/events/generated/schemas.ts +59 -0
- package/runtime/subsystems/events/generated/types.ts +94 -0
- package/runtime/subsystems/events/index.ts +21 -0
- package/runtime/subsystems/index.ts +63 -0
- package/runtime/subsystems/jobs/generated/job-orchestration.schema.multi-tenant.ts +217 -0
- package/runtime/subsystems/jobs/generated/job-orchestration.schema.single-tenant.ts +217 -0
- package/runtime/subsystems/jobs/generated/scope-entity-type.ts +10 -0
- package/runtime/subsystems/jobs/index.ts +120 -0
- package/runtime/subsystems/jobs/job-handler.base.ts +206 -0
- package/runtime/subsystems/jobs/job-orchestration.schema.ts +217 -0
- package/runtime/subsystems/jobs/job-orchestrator.drizzle-backend.ts +536 -0
- package/runtime/subsystems/jobs/job-orchestrator.memory-backend.ts +850 -0
- package/runtime/subsystems/jobs/job-orchestrator.protocol.ts +179 -0
- package/runtime/subsystems/jobs/job-run-service.drizzle-backend.ts +171 -0
- package/runtime/subsystems/jobs/job-run-service.memory-backend.ts +165 -0
- package/runtime/subsystems/jobs/job-run-service.protocol.ts +79 -0
- package/runtime/subsystems/jobs/job-step-service.drizzle-backend.ts +66 -0
- package/runtime/subsystems/jobs/job-step-service.memory-backend.ts +119 -0
- package/runtime/subsystems/jobs/job-step-service.protocol.ts +53 -0
- package/runtime/subsystems/jobs/job-worker.module.ts +302 -0
- package/runtime/subsystems/jobs/job-worker.ts +615 -0
- package/runtime/subsystems/jobs/jobs-domain.module.ts +119 -0
- package/runtime/subsystems/jobs/jobs-domain.tokens.ts +30 -0
- package/runtime/subsystems/jobs/jobs-errors.ts +150 -0
- package/runtime/subsystems/jobs/memory-job-store.ts +35 -0
- package/runtime/subsystems/jobs/pool-config.loader.ts +218 -0
- package/runtime/subsystems/storage/index.ts +18 -0
- package/runtime/subsystems/storage/storage.local-backend.ts +113 -0
- package/runtime/subsystems/storage/storage.memory-backend.ts +78 -0
- package/runtime/subsystems/storage/storage.module.ts +60 -0
- package/runtime/subsystems/storage/storage.protocol.ts +78 -0
- package/runtime/subsystems/storage/storage.tokens.ts +9 -0
- package/runtime/subsystems/storage/storage.utils.ts +20 -0
- package/runtime/subsystems/sync/deep-equal.differ.ts +198 -0
- package/runtime/subsystems/sync/execute-sync.use-case.ts +334 -0
- package/runtime/subsystems/sync/index.ts +98 -0
- package/runtime/subsystems/sync/sync-audit.schema.ts +300 -0
- package/runtime/subsystems/sync/sync-change-source.protocol.ts +99 -0
- package/runtime/subsystems/sync/sync-cursor-store.drizzle-backend.ts +104 -0
- package/runtime/subsystems/sync/sync-cursor-store.memory-backend.ts +64 -0
- package/runtime/subsystems/sync/sync-cursor-store.protocol.ts +53 -0
- package/runtime/subsystems/sync/sync-errors.ts +54 -0
- package/runtime/subsystems/sync/sync-field-diff.protocol.ts +61 -0
- package/runtime/subsystems/sync/sync-loopback.protocol.ts +33 -0
- package/runtime/subsystems/sync/sync-run-recorder.drizzle-backend.ts +123 -0
- package/runtime/subsystems/sync/sync-run-recorder.memory-backend.ts +143 -0
- package/runtime/subsystems/sync/sync-run-recorder.protocol.ts +86 -0
- package/runtime/subsystems/sync/sync-sink.protocol.ts +55 -0
- package/runtime/subsystems/sync/sync.module.ts +156 -0
- package/runtime/subsystems/sync/sync.tokens.ts +57 -0
- 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,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';
|