@hed-hog/core 0.0.294 → 0.0.296

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 (140) hide show
  1. package/dist/auth/auth.controller.d.ts +4 -4
  2. package/dist/auth/auth.service.d.ts +4 -4
  3. package/dist/challenge/challenge.service.d.ts +2 -2
  4. package/dist/core.module.d.ts.map +1 -1
  5. package/dist/core.module.js +4 -1
  6. package/dist/core.module.js.map +1 -1
  7. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +1 -1
  8. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +2 -2
  9. package/dist/index.d.ts +13 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +14 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/integration/index.d.ts +4 -0
  14. package/dist/integration/index.d.ts.map +1 -0
  15. package/dist/integration/index.js +20 -0
  16. package/dist/integration/index.js.map +1 -0
  17. package/dist/integration/integration-api.validation.d.ts +2 -0
  18. package/dist/integration/integration-api.validation.d.ts.map +1 -0
  19. package/dist/integration/integration-api.validation.js +126 -0
  20. package/dist/integration/integration-api.validation.js.map +1 -0
  21. package/dist/integration/integration.module.d.ts +3 -0
  22. package/dist/integration/integration.module.d.ts.map +1 -0
  23. package/dist/integration/integration.module.js +54 -0
  24. package/dist/integration/integration.module.js.map +1 -0
  25. package/dist/integration/services/domain-event.publisher.d.ts +31 -0
  26. package/dist/integration/services/domain-event.publisher.d.ts.map +1 -0
  27. package/dist/integration/services/domain-event.publisher.js +79 -0
  28. package/dist/integration/services/domain-event.publisher.js.map +1 -0
  29. package/dist/integration/services/event-subscriber.registry.d.ts +37 -0
  30. package/dist/integration/services/event-subscriber.registry.d.ts.map +1 -0
  31. package/dist/integration/services/event-subscriber.registry.js +86 -0
  32. package/dist/integration/services/event-subscriber.registry.js.map +1 -0
  33. package/dist/integration/services/inbox.service.d.ts +55 -0
  34. package/dist/integration/services/inbox.service.d.ts.map +1 -0
  35. package/dist/integration/services/inbox.service.js +173 -0
  36. package/dist/integration/services/inbox.service.js.map +1 -0
  37. package/dist/integration/services/index.d.ts +12 -0
  38. package/dist/integration/services/index.d.ts.map +1 -0
  39. package/dist/integration/services/index.js +28 -0
  40. package/dist/integration/services/index.js.map +1 -0
  41. package/dist/integration/services/integration-developer-api.service.d.ts +30 -0
  42. package/dist/integration/services/integration-developer-api.service.d.ts.map +1 -0
  43. package/dist/integration/services/integration-developer-api.service.js +55 -0
  44. package/dist/integration/services/integration-developer-api.service.js.map +1 -0
  45. package/dist/integration/services/integration-link.service.d.ts +52 -0
  46. package/dist/integration/services/integration-link.service.d.ts.map +1 -0
  47. package/dist/integration/services/integration-link.service.js +128 -0
  48. package/dist/integration/services/integration-link.service.js.map +1 -0
  49. package/dist/integration/services/integration-settings.service.d.ts +23 -0
  50. package/dist/integration/services/integration-settings.service.d.ts.map +1 -0
  51. package/dist/integration/services/integration-settings.service.js +81 -0
  52. package/dist/integration/services/integration-settings.service.js.map +1 -0
  53. package/dist/integration/services/outbox-polling.coordinator.d.ts +45 -0
  54. package/dist/integration/services/outbox-polling.coordinator.d.ts.map +1 -0
  55. package/dist/integration/services/outbox-polling.coordinator.js +143 -0
  56. package/dist/integration/services/outbox-polling.coordinator.js.map +1 -0
  57. package/dist/integration/services/outbox.notifier.d.ts +30 -0
  58. package/dist/integration/services/outbox.notifier.d.ts.map +1 -0
  59. package/dist/integration/services/outbox.notifier.js +57 -0
  60. package/dist/integration/services/outbox.notifier.js.map +1 -0
  61. package/dist/integration/services/outbox.processor.d.ts +42 -0
  62. package/dist/integration/services/outbox.processor.d.ts.map +1 -0
  63. package/dist/integration/services/outbox.processor.job.d.ts +43 -0
  64. package/dist/integration/services/outbox.processor.job.d.ts.map +1 -0
  65. package/dist/integration/services/outbox.processor.job.js +100 -0
  66. package/dist/integration/services/outbox.processor.job.js.map +1 -0
  67. package/dist/integration/services/outbox.processor.js +208 -0
  68. package/dist/integration/services/outbox.processor.js.map +1 -0
  69. package/dist/integration/services/outbox.service.d.ts +53 -0
  70. package/dist/integration/services/outbox.service.d.ts.map +1 -0
  71. package/dist/integration/services/outbox.service.js +149 -0
  72. package/dist/integration/services/outbox.service.js.map +1 -0
  73. package/dist/integration/types/event.types.d.ts +88 -0
  74. package/dist/integration/types/event.types.d.ts.map +1 -0
  75. package/dist/integration/types/event.types.js +35 -0
  76. package/dist/integration/types/event.types.js.map +1 -0
  77. package/dist/integration/types/index.d.ts +3 -0
  78. package/dist/integration/types/index.d.ts.map +1 -0
  79. package/dist/integration/types/index.js +19 -0
  80. package/dist/integration/types/index.js.map +1 -0
  81. package/dist/integration/types/subscriber.types.d.ts +31 -0
  82. package/dist/integration/types/subscriber.types.d.ts.map +1 -0
  83. package/dist/integration/types/subscriber.types.js +3 -0
  84. package/dist/integration/types/subscriber.types.js.map +1 -0
  85. package/dist/mail/mail.controller.d.ts +2 -2
  86. package/dist/mail/mail.service.d.ts +2 -2
  87. package/dist/mail-sent/mail-sent.controller.d.ts +3 -3
  88. package/dist/mail-sent/mail-sent.service.d.ts +3 -3
  89. package/dist/oauth/oauth.controller.js.map +1 -1
  90. package/dist/oauth/oauth.service.d.ts +3 -3
  91. package/dist/oauth/oauth.service.d.ts.map +1 -1
  92. package/dist/oauth/oauth.service.js.map +1 -1
  93. package/dist/profile/profile.controller.d.ts +3 -3
  94. package/dist/profile/profile.service.d.ts +3 -3
  95. package/dist/setting/setting.controller.d.ts +12 -8
  96. package/dist/setting/setting.controller.d.ts.map +1 -1
  97. package/dist/setting/setting.service.d.ts +12 -8
  98. package/dist/setting/setting.service.d.ts.map +1 -1
  99. package/dist/setting/setting.service.js +21 -1
  100. package/dist/setting/setting.service.js.map +1 -1
  101. package/dist/user/user.controller.d.ts +4 -4
  102. package/dist/user/user.service.d.ts +9 -9
  103. package/hedhog/data/route.yaml +2 -0
  104. package/hedhog/data/setting_group.yaml +955 -470
  105. package/hedhog/data/setting_subgroup.yaml +303 -0
  106. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +44 -18
  107. package/hedhog/frontend/app/configurations/[slug]/page.tsx.ejs +134 -27
  108. package/hedhog/frontend/app/configurations/layout.tsx.ejs +84 -23
  109. package/hedhog/frontend/app/preferences/page.tsx.ejs +45 -48
  110. package/hedhog/table/inbox_event.yaml +40 -0
  111. package/hedhog/table/integration_link.yaml +33 -0
  112. package/hedhog/table/outbox_event.yaml +45 -0
  113. package/hedhog/table/setting.yaml +7 -0
  114. package/hedhog/table/setting_subgroup.yaml +19 -0
  115. package/package.json +8 -8
  116. package/src/core.module.ts +4 -1
  117. package/src/index.ts +15 -0
  118. package/src/integration/README.md +397 -0
  119. package/src/integration/USAGE_EXAMPLE.md +279 -0
  120. package/src/integration/index.ts +4 -0
  121. package/src/integration/integration-api.validation.ts +154 -0
  122. package/src/integration/integration.module.ts +53 -0
  123. package/src/integration/services/domain-event.publisher.ts +136 -0
  124. package/src/integration/services/event-subscriber.registry.ts +89 -0
  125. package/src/integration/services/inbox.service.ts +218 -0
  126. package/src/integration/services/index.ts +12 -0
  127. package/src/integration/services/integration-developer-api.service.ts +96 -0
  128. package/src/integration/services/integration-link.service.ts +154 -0
  129. package/src/integration/services/integration-settings.service.ts +128 -0
  130. package/src/integration/services/outbox-polling.coordinator.ts +146 -0
  131. package/src/integration/services/outbox.notifier.ts +48 -0
  132. package/src/integration/services/outbox.processor.job.ts +97 -0
  133. package/src/integration/services/outbox.processor.ts +266 -0
  134. package/src/integration/services/outbox.service.ts +209 -0
  135. package/src/integration/types/event.types.ts +93 -0
  136. package/src/integration/types/index.ts +3 -0
  137. package/src/integration/types/subscriber.types.ts +37 -0
  138. package/src/oauth/oauth.controller.ts +17 -17
  139. package/src/oauth/oauth.service.ts +20 -20
  140. package/src/setting/setting.service.ts +27 -2
@@ -0,0 +1,397 @@
1
+ # Core Integration Module
2
+
3
+ A reusable, event-driven cross-module integration foundation for the HedHog platform.
4
+
5
+ ## Overview
6
+
7
+ The **integration** module provides:
8
+
9
+ - **Durable Outbox**: Database-backed event queue with automatic retry and exponential backoff
10
+ - **Idempotent Inbox**: Per-consumer tracking ensures handlers never process the same event twice
11
+ - **Entity Integration Links**: Generic mappings between entities across different modules
12
+ - **Event Subscriber Registry**: Global registry for modules to register event handlers
13
+ - **Settings-Driven Configuration**: All timeouts, retry logic, and batch sizes configurable via SettingService
14
+ - **Hybrid Processing Strategy**: Database as source of truth + in-memory notifier for immediate wakeup
15
+
16
+ ## Architecture
17
+
18
+ ```
19
+ Module A (Finance) → publishEvent() → DomainEventPublisher
20
+
21
+ OutboxService (write to DB)
22
+
23
+ OutboxNotifier (wake up processor)
24
+
25
+ OutboxProcessorJob (hybrid loop)
26
+
27
+ ┌───────────────────────────────────────────┐
28
+ ↓ ↓
29
+ Fetch pending events ExecuteHandlers
30
+ ↓ ↓
31
+ Lock with lease time Get handlers from registry
32
+ ↓ ↓
33
+ Mark as processing For each registered handler:
34
+ ↓ ↓
35
+ Track inbox per consumer 1. Get/create InboxEvent
36
+ ↓ 2. Mark as processing
37
+ Execute handler code 3. Call handler function
38
+ ↓ 4. Mark as processed (or failed)
39
+ Handle failures: 5. Create IntegrationLinks
40
+ - Retry with backoff (as side effects)
41
+ - Dead letter after max
42
+ - Update outbox status
43
+
44
+ Module B (Operations) ← Subscribes via registry, receives events, creates links
45
+ ```
46
+
47
+ ## Database Schema
48
+
49
+ ### `outbox_event` Table
50
+
51
+ Stores domain events for cross-module integration.
52
+
53
+ ```sql
54
+ CREATE TABLE outbox_event (
55
+ id UUID PRIMARY KEY,
56
+ event_name VARCHAR(255) NOT NULL,
57
+ source_module VARCHAR(127) NOT NULL,
58
+ aggregate_type VARCHAR(127) NOT NULL,
59
+ aggregate_id VARCHAR(36) NOT NULL,
60
+ payload JSONB NOT NULL,
61
+ status VARCHAR(63) NOT NULL, -- pending | processing | processed | failed | dead_letter
62
+ attempt_count INT DEFAULT 0,
63
+ last_error VARCHAR(1023),
64
+ available_at TIMESTAMP WITH TIME ZONE,
65
+ processed_at TIMESTAMP WITH TIME ZONE,
66
+ created_at TIMESTAMP WITH TIME ZONE,
67
+ updated_at TIMESTAMP WITH TIME ZONE
68
+ );
69
+
70
+ -- Indexes for efficient polling
71
+ CREATE INDEX idx_outbox_event_status_available_at
72
+ ON outbox_event(status, available_at);
73
+ CREATE INDEX idx_outbox_event_source
74
+ ON outbox_event(source_module, aggregate_type, aggregate_id);
75
+ CREATE INDEX idx_outbox_event_created_at
76
+ ON outbox_event(created_at);
77
+ ```
78
+
79
+ ### `inbox_event` Table
80
+
81
+ Per-consumer idempotency tracking to ensure exactly-once processing semantics.
82
+
83
+ ```sql
84
+ CREATE TABLE inbox_event (
85
+ id UUID PRIMARY KEY,
86
+ outbox_event_id UUID NOT NULL REFERENCES outbox_event(id) ON DELETE CASCADE,
87
+ consumer_name VARCHAR(255) NOT NULL,
88
+ status VARCHAR(63) NOT NULL, -- received | processing | processed | failed | skipped
89
+ attempt_count INT DEFAULT 0,
90
+ last_error VARCHAR(1023),
91
+ processed_at TIMESTAMP WITH TIME ZONE,
92
+ created_at TIMESTAMP WITH TIME ZONE,
93
+ updated_at TIMESTAMP WITH TIME ZONE
94
+ );
95
+
96
+ -- Unique constraint ensures idempotency
97
+ CREATE UNIQUE INDEX idx_inbox_event_idempotency
98
+ ON inbox_event(outbox_event_id, consumer_name);
99
+ CREATE INDEX idx_inbox_event_status
100
+ ON inbox_event(status);
101
+ ```
102
+
103
+ ### `integration_link` Table
104
+
105
+ Generic mappings between entities across modules (no direct foreign keys).
106
+
107
+ ```sql
108
+ CREATE TABLE integration_link (
109
+ id UUID PRIMARY KEY,
110
+ source_module VARCHAR(127) NOT NULL,
111
+ source_entity_type VARCHAR(127) NOT NULL,
112
+ source_entity_id VARCHAR(36) NOT NULL,
113
+ target_module VARCHAR(127) NOT NULL,
114
+ target_entity_type VARCHAR(127) NOT NULL,
115
+ target_entity_id VARCHAR(36) NOT NULL,
116
+ link_type VARCHAR(63) NOT NULL, -- reference | cascade | aggregate_root
117
+ metadata JSONB,
118
+ created_at TIMESTAMP WITH TIME ZONE,
119
+ updated_at TIMESTAMP WITH TIME ZONE
120
+ );
121
+
122
+ -- Efficient lookups
123
+ CREATE INDEX idx_integration_link_source
124
+ ON integration_link(source_module, source_entity_type, source_entity_id);
125
+ CREATE INDEX idx_integration_link_target
126
+ ON integration_link(target_module, target_entity_type, target_entity_id);
127
+ CREATE INDEX idx_integration_link_type
128
+ ON integration_link(link_type);
129
+ ```
130
+
131
+ ## Core Services
132
+
133
+ ### `DomainEventPublisher`
134
+
135
+ Entry point for modules to publish events.
136
+
137
+ ```typescript
138
+ @Injectable()
139
+ export class DomainEventPublisher {
140
+ async publishEvent(
141
+ eventName: string,
142
+ sourceModule: string,
143
+ aggregateType: string,
144
+ aggregateId: string,
145
+ payload?: Record<string, any>,
146
+ ): Promise<void>
147
+
148
+ async publishEvents(events: DomainEvent[]): Promise<void>
149
+ }
150
+ ```
151
+
152
+ **Usage:**
153
+ ```typescript
154
+ @Injectable()
155
+ export class InvoiceService {
156
+ constructor(
157
+ private readonly eventPublisher: DomainEventPublisher,
158
+ ) {}
159
+
160
+ async createInvoice(data: any): Promise<Invoice> {
161
+ const invoice = await this.prisma.invoice.create({ data });
162
+
163
+ await this.eventPublisher.publishEvent(
164
+ 'invoice.created',
165
+ 'finance',
166
+ 'Invoice',
167
+ invoice.id,
168
+ { amount: invoice.amount },
169
+ );
170
+
171
+ return invoice;
172
+ }
173
+ }
174
+ ```
175
+
176
+ ### `IntegrationDeveloperApiService`
177
+
178
+ Single developer-facing facade for publishing events, subscribing handlers, and managing links.
179
+
180
+ ```typescript
181
+ @Injectable()
182
+ export class OperationsBillingIntegration {
183
+ constructor(private readonly integrationApi: IntegrationDeveloperApiService) {}
184
+
185
+ async publishProjectReady(projectId: string): Promise<void> {
186
+ await this.integrationApi.publishEvent({
187
+ eventName: 'operations.project.billing_ready',
188
+ sourceModule: 'operations',
189
+ aggregateType: 'project',
190
+ aggregateId: projectId,
191
+ payload: { projectId },
192
+ metadata: { producer: 'operations' },
193
+ });
194
+ }
195
+
196
+ onModuleInit(): void {
197
+ this.integrationApi.subscribe({
198
+ eventName: 'operations.contract.payable_generated',
199
+ consumerName: 'operations-finance-handler',
200
+ priority: 10,
201
+ handler: async (event, context) => {
202
+ context.logger.log(`Received ${event.eventName} for ${event.aggregateId}`);
203
+ },
204
+ });
205
+ }
206
+ }
207
+ ```
208
+
209
+ ### `EventSubscriberRegistry`
210
+
211
+ Global registry for handler registration.
212
+
213
+ ```typescript
214
+ @Injectable()
215
+ export class EventSubscriberRegistry {
216
+ registerHandler(definition: SubscriberDefinition): void
217
+ getHandlers(eventName: string): SubscriberDefinition[]
218
+ hasHandlers(eventName: string): boolean
219
+ getEventNames(): string[]
220
+ getHandlerCount(): number
221
+ clear(): void
222
+ }
223
+ ```
224
+
225
+ **Usage:**
226
+ ```typescript
227
+ @Injectable()
228
+ export class OperationsEventHandlers implements OnModuleInit {
229
+ constructor(private readonly registry: EventSubscriberRegistry) {}
230
+
231
+ onModuleInit(): void {
232
+ this.registry.registerHandler({
233
+ eventName: 'invoice.created',
234
+ consumerName: 'operations-module',
235
+ priority: 10,
236
+ handler: async (event, context) => {
237
+ // Handle event
238
+ },
239
+ });
240
+ }
241
+ }
242
+ ```
243
+
244
+ ### `InboxService`
245
+
246
+ Tracks per-consumer processing for idempotency.
247
+
248
+ ```typescript
249
+ @Injectable()
250
+ export class InboxService {
251
+ async getOrCreate(
252
+ outboxEventId: string,
253
+ consumerName: string,
254
+ ): Promise<InboxEvent>
255
+
256
+ async markProcessing(inboxEventId: string): Promise<InboxEvent>
257
+ async markProcessed(inboxEventId: string): Promise<InboxEvent>
258
+ async markFailed(inboxEventId: string, error: string): Promise<InboxEvent>
259
+ async markSkipped(inboxEventId: string): Promise<InboxEvent>
260
+ async isProcessed(
261
+ outboxEventId: string,
262
+ consumerName: string,
263
+ ): Promise<boolean>
264
+ }
265
+ ```
266
+
267
+ ### `IntegrationLinkService`
268
+
269
+ Create and query cross-module entity relationships.
270
+
271
+ ```typescript
272
+ @Injectable()
273
+ export class IntegrationLinkService {
274
+ async createLink(dto: CreateIntegrationLinkDto): Promise<IntegrationLink>
275
+ async findOutbound(
276
+ sourceModule: string,
277
+ sourceEntityType: string,
278
+ sourceEntityId: string,
279
+ ): Promise<IntegrationLink[]>
280
+ async findInbound(
281
+ targetModule: string,
282
+ targetEntityType: string,
283
+ targetEntityId: string,
284
+ ): Promise<IntegrationLink[]>
285
+ async findByLinkType(linkType: LinkType): Promise<IntegrationLink[]>
286
+ async findBidirectional(...): Promise<IntegrationLink | null>
287
+ }
288
+ ```
289
+
290
+ The facade `IntegrationDeveloperApiService` also provides link methods:
291
+
292
+ - `createLink(dto, persistenceClient?)`
293
+ - `findLinksBySource({ module, entityType, entityId })`
294
+ - `findLinksByTarget({ module, entityType, entityId })`
295
+ - `findLinksByType(linkType)`
296
+
297
+ ### `OutboxProcessor`
298
+
299
+ Processes pending events, executes handlers, and manages retries.
300
+
301
+ ```typescript
302
+ @Injectable()
303
+ export class OutboxProcessor {
304
+ async processBatch(): Promise<number> // Process one batch
305
+ async startupDrain(): Promise<number> // Drain on startup
306
+ async getStats(): Promise<ProcessorStats>
307
+ }
308
+ ```
309
+
310
+ ## Settings Configuration
311
+
312
+ All integration settings are registered in `setting_group.yaml` under the `integration` group:
313
+
314
+ | Setting Key | Type | Default | Purpose |
315
+ |---|---|---|---|
316
+ | `core.integration.outbox.enabled` | boolean | `true` | Enable/disable outbox system |
317
+ | `core.integration.outbox.processor.enabled` | boolean | `true` | Enable/disable processor job |
318
+ | `core.integration.outbox.startupDrainEnabled` | boolean | `true` | Run drain on startup |
319
+ | `core.integration.outbox.pollingIntervalMs` | number | `5000` | Poll interval when processing |
320
+ | `core.integration.outbox.idlePollingIntervalMs` | number | `30000` | Poll interval when idle |
321
+ | `core.integration.outbox.batchSize` | number | `10` | Events per batch |
322
+ | `core.integration.outbox.maxAttempts` | number | `3` | Retry attempts |
323
+ | `core.integration.outbox.processingLeaseMs` | number | `30000` | Lock duration |
324
+ | `core.integration.outbox.retryBaseDelayMs` | number | `1000` | Base delay for backoff |
325
+ | `core.integration.outbox.deadLetterEnabled` | boolean | `true` | Move failed events to dead letter |
326
+ | `core.integration.outbox.startupDrainBatchSize` | number | `50` | Batch size for startup drain |
327
+
328
+ ## Event Handler Contract
329
+
330
+ ```typescript
331
+ export type EventHandler = (
332
+ event: DomainEvent,
333
+ context: SubscriberContext,
334
+ ) => Promise<void>
335
+
336
+ export interface DomainEvent {
337
+ eventName: string
338
+ sourceModule: string
339
+ aggregateType: string
340
+ aggregateId: string
341
+ payload: Record<string, any>
342
+ timestamp: Date
343
+ }
344
+
345
+ export interface SubscriberContext {
346
+ logger: Logger // NestJS Logger
347
+ inboxService: InboxService // Mark as processed/failed
348
+ linkService: IntegrationLinkService // Create links
349
+ }
350
+ ```
351
+
352
+ ## Failure Handling & Retry Logic
353
+
354
+ 1. Handler executes; if exception thrown → marked as failed
355
+ 2. Exponential backoff: delay = `retryBaseDelayMs` × 2^(attemptCount)
356
+ 3. After `maxAttempts`, event moves to `dead_letter` status
357
+ 4. Processing lease prevents stuck events: if not updated within `processingLeaseMs`, available for reprocessing
358
+ 5. Startup drain recovers all pending events on app restart
359
+
360
+ ## Idempotency
361
+
362
+ The inbox table with unique constraint on `(outboxEventId, consumerName)` ensures:
363
+
364
+ - Same handler never processes same event twice
365
+ - Crashes during processing don't cause duplicates
366
+ - Replay is safe (read-only handlers preferred)
367
+
368
+ **Important**: Applications must still make handlers idempotent in logic:
369
+ - Operations should be repeatable without side effects
370
+ - Or: check for existence before creating
371
+ - Or: use database-level constraints (unique, upsert)
372
+
373
+ ## Performance Considerations
374
+
375
+ - **Polling Interval**: Tune based on event volume and latency requirements
376
+ - **Batch Size**: Larger batches reduce DB queries but block longer
377
+ - **Processing Lease**: Must exceed typical handler execution time
378
+ - **Dead Letter Threshold**: Balance between retries and operational alerts
379
+ - **Indexes**: Already defined for common query patterns
380
+
381
+ ## Testing
382
+
383
+ See [USAGE_EXAMPLE.md](./USAGE_EXAMPLE.md) for examples of how to:
384
+ - Publish events from one module
385
+ - Register handlers in another module
386
+ - Query integration links
387
+ - Test idempotency
388
+
389
+ ## Future Enhancements
390
+
391
+ - Event schema registry and versioning
392
+ - Distributed tracing / correlation IDs
393
+ - Event replay / time-travel features
394
+ - Saga orchestration (choreography → orchestration)
395
+ - Webhook / external event ingestion
396
+ - Dead letter queue UI dashboard
397
+ - Metrics and observability
@@ -0,0 +1,279 @@
1
+ /**
2
+ * EXAMPLE: How a Module Uses the Integration Foundation
3
+ *
4
+ * This example shows how the finance or operations module would use
5
+ * the core integration infrastructure to publish events and handle
6
+ * events from other modules.
7
+ */
8
+
9
+ // ============================================================================
10
+ // STEP 1: Publish a Domain Event from Finance Module
11
+ // ============================================================================
12
+
13
+ import { Injectable } from '@nestjs/common';
14
+ import { IntegrationDeveloperApiService } from '@hed-hog/core';
15
+ import { PrismaService } from '@hed-hog/api-prisma';
16
+
17
+ @Injectable()
18
+ export class InvoiceService {
19
+ constructor(
20
+ private readonly prisma: PrismaService,
21
+ private readonly integrationApi: IntegrationDeveloperApiService,
22
+ ) {}
23
+
24
+ async createInvoice(data: any): Promise<any> {
25
+ // Create the invoice
26
+ const invoice = await this.prisma.invoice.create({ data });
27
+
28
+ // Publish event that other modules can subscribe to
29
+ await this.integrationApi.publishEvent({
30
+ eventName: 'invoice.created',
31
+ sourceModule: 'finance',
32
+ aggregateType: 'Invoice',
33
+ aggregateId: invoice.id,
34
+ payload: {
35
+ invoiceId: invoice.id,
36
+ amount: invoice.amount,
37
+ customerId: invoice.customerId,
38
+ },
39
+ metadata: {
40
+ producer: 'finance',
41
+ },
42
+ });
43
+
44
+ return invoice;
45
+ }
46
+
47
+ async cancelInvoice(invoiceId: string): Promise<void> {
48
+ await this.prisma.invoice.update({
49
+ where: { id: invoiceId },
50
+ data: { status: 'CANCELLED' },
51
+ });
52
+
53
+ // Notify subscribers
54
+ await this.integrationApi.publishEvent({
55
+ eventName: 'invoice.cancelled',
56
+ sourceModule: 'finance',
57
+ aggregateType: 'Invoice',
58
+ aggregateId: invoiceId,
59
+ payload: { invoiceId },
60
+ });
61
+ }
62
+ }
63
+
64
+ // ============================================================================
65
+ // STEP 2: Register Event Handlers in Operations Module
66
+ // ============================================================================
67
+
68
+ import { Injectable, OnModuleInit} from '@nestjs/common';
69
+ import {
70
+ IntegrationDeveloperApiService,
71
+ DomainEvent,
72
+ SubscriberContext,
73
+ LinkType,
74
+ } from '@hed-hog/core';
75
+ import { PrismaService } from '@hed-hog/api-prisma';
76
+
77
+ @Injectable()
78
+ export class OperationsEventHandlers implements OnModuleInit {
79
+ constructor(
80
+ private readonly prisma: PrismaService,
81
+ private readonly integrationApi: IntegrationDeveloperApiService,
82
+ ) {}
83
+
84
+ onModuleInit(): void {
85
+ // Register handler for invoice.created event
86
+ this.integrationApi.subscribe({
87
+ eventName: 'invoice.created',
88
+ consumerName: 'operations-module',
89
+ priority: 10,
90
+ handler: async (event: DomainEvent, context: SubscriberContext) => {
91
+ await this.onInvoiceCreated(event, context);
92
+ },
93
+ });
94
+
95
+ // Register handler for invoice.cancelled event
96
+ this.integrationApi.subscribe({
97
+ eventName: 'invoice.cancelled',
98
+ consumerName: 'operations-module',
99
+ priority: 10,
100
+ handler: async (event: DomainEvent, context: SubscriberContext) => {
101
+ await this.onInvoiceCancelled(event, context);
102
+ },
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Handle invoice.created event
108
+ * This might trigger operational workflows, create tasks, etc.
109
+ */
110
+ private async onInvoiceCreated(
111
+ event: DomainEvent,
112
+ context: SubscriberContext,
113
+ ): Promise<void> {
114
+ context.logger.log(`Processing invoice creation: ${event.aggregateId}`);
115
+
116
+ const { invoiceId, customerId, amount } = event.payload;
117
+
118
+ // Create an operational task
119
+ const task = await this.prisma.task.create({
120
+ data: {
121
+ title: `Process invoice ${invoiceId}`,
122
+ description: `Invoice for amount ${amount} from customer ${customerId}`,
123
+ status: 'PENDING',
124
+ },
125
+ });
126
+
127
+ // Create an integration link between the finance invoice and operations task
128
+ await context.linkService.createLink({
129
+ sourceModule: 'finance',
130
+ sourceEntityType: 'Invoice',
131
+ sourceEntityId: invoiceId,
132
+ targetModule: 'operations',
133
+ targetEntityType: 'Task',
134
+ targetEntityId: task.id,
135
+ linkType: LinkType.CASCADE,
136
+ metadata: {
137
+ reason: 'workflow_trigger',
138
+ invoiceAmount: amount,
139
+ },
140
+ });
141
+
142
+ context.logger.log(
143
+ `Created task ${task.id} for invoice ${invoiceId}`,
144
+ );
145
+ }
146
+
147
+ /**
148
+ * Handle invoice.cancelled event
149
+ */
150
+ private async onInvoiceCancelled(
151
+ event: DomainEvent,
152
+ context: SubscriberContext,
153
+ ): Promise<void> {
154
+ context.logger.log(`Processing invoice cancellation: ${event.aggregateId}`);
155
+
156
+ const { invoiceId } = event.payload;
157
+
158
+ // Find linked tasks
159
+ const links = await context.linkService.findOutbound(
160
+ 'finance',
161
+ 'Invoice',
162
+ invoiceId,
163
+ );
164
+
165
+ for (const link of links) {
166
+ // Cancel related tasks
167
+ if (link.targetModule === 'operations' && link.targetEntityType === 'Task') {
168
+ await this.prisma.task.update({
169
+ where: { id: link.targetEntityId },
170
+ data: { status: 'CANCELLED' },
171
+ });
172
+
173
+ context.logger.log(
174
+ `Cancelled task ${link.targetEntityId} due to invoice cancellation`,
175
+ );
176
+ }
177
+ }
178
+ }
179
+ }
180
+
181
+ // ============================================================================
182
+ // STEP 3: Register Handlers in Operations Module DI
183
+ // ============================================================================
184
+
185
+ import { Module } from '@nestjs/common';
186
+
187
+ @Module({
188
+ providers: [OperationsEventHandlers],
189
+ })
190
+ export class OperationsModule {
191
+ // OperationsEventHandlers will be instantiated and its onModuleInit()
192
+ // will be called automatically, registering event handlers
193
+ }
194
+
195
+ // ============================================================================
196
+ // STEP 4: Query Integration Links to Find Related Entities
197
+ // ============================================================================
198
+
199
+ import { Injectable } from '@nestjs/common';
200
+ import { IntegrationDeveloperApiService, LinkType } from '@hed-hog/core';
201
+
202
+ @Injectable()
203
+ export class InvoiceQueryService {
204
+ constructor(
205
+ private readonly integrationApi: IntegrationDeveloperApiService,
206
+ ) {}
207
+
208
+ /**
209
+ * Find all operational tasks related to an invoice
210
+ */
211
+ async getRelatedTasks(invoiceId: string): Promise<any[]> {
212
+ const links = await this.integrationApi.findLinksBySource({
213
+ module: 'finance',
214
+ entityType: 'Invoice',
215
+ entityId: invoiceId,
216
+ });
217
+
218
+ const taskLinks = links.filter(
219
+ (link) => link.targetModule === 'operations' && link.targetEntityType === 'Task',
220
+ );
221
+
222
+ return taskLinks.map((link) => link.targetEntityId);
223
+ }
224
+
225
+ /**
226
+ * Find the invoice that triggered a task
227
+ */
228
+ async getSourceInvoice(taskId: string): Promise<string | null> {
229
+ const links = await this.integrationApi.findLinksByTarget({
230
+ module: 'operations',
231
+ entityType: 'Task',
232
+ entityId: taskId,
233
+ });
234
+
235
+ const invoiceLink = links.find(
236
+ (link) => link.sourceModule === 'finance' && link.sourceEntityType === 'Invoice',
237
+ );
238
+
239
+ return invoiceLink?.sourceEntityId || null;
240
+ }
241
+ }
242
+
243
+ // ============================================================================
244
+ // ARCHITECTURE NOTES
245
+ // ============================================================================
246
+
247
+ /**
248
+ * KEY PRINCIPLES:
249
+ *
250
+ * 1. DURABLE OUTBOX
251
+ * - Events are written to the database outbox table first
252
+ * - In-memory notification wakes up processor (optional acceleration)
253
+ * - On crashes/restarts, processor catches up on pending events
254
+ * - No events are lost
255
+ *
256
+ * 2. IDEMPOTENT PROCESSING
257
+ * - Each consumer has an inbox entry per outbox event
258
+ * - Unique constraint prevents duplicate processing
259
+ * - If a handler crashes mid-execution, retry will recognize it was attempted
260
+ * - Handlers must be idempotent at the application logic level
261
+ *
262
+ * 3. CROSS-MODULE DECOUPLING
263
+ * - Modules publish events but don't depend on other modules' internals
264
+ * - Integration links are explicit, queryable, and metadata-rich
265
+ * - No direct FK relationships across module boundaries
266
+ * - Modules self-register handlers, core doesn't know about them
267
+ *
268
+ * 4. CONFIGURABILITY
269
+ * - All timing/retry/batch/scope settings are in the core SettingService
270
+ * - Operations/infrastructure teams can tune without code changes
271
+ * - Settings can be changed at runtime
272
+ *
273
+ * 5. FAILURE RECOVERY
274
+ * - Exponential backoff on handler failures
275
+ * - Configurable max attempts
276
+ * - Dead letter queue for poisoned events
277
+ * - Processing lease time prevents stuck events
278
+ * - Startup drain recovers pending events after restart
279
+ */
@@ -0,0 +1,4 @@
1
+ export * from './integration.module';
2
+ export * from './services';
3
+ export * from './types';
4
+