@hed-hog/core 0.0.295 → 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 (144) 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/oauth/oauth.controller.js.map +1 -1
  86. package/dist/oauth/oauth.service.d.ts +3 -3
  87. package/dist/oauth/oauth.service.d.ts.map +1 -1
  88. package/dist/oauth/oauth.service.js.map +1 -1
  89. package/dist/profile/profile.controller.d.ts +3 -3
  90. package/dist/profile/profile.service.d.ts +3 -3
  91. package/dist/setting/setting.controller.d.ts +12 -8
  92. package/dist/setting/setting.controller.d.ts.map +1 -1
  93. package/dist/setting/setting.service.d.ts +12 -8
  94. package/dist/setting/setting.service.d.ts.map +1 -1
  95. package/dist/setting/setting.service.js +21 -1
  96. package/dist/setting/setting.service.js.map +1 -1
  97. package/dist/user/user.controller.d.ts +4 -4
  98. package/dist/user/user.service.d.ts +9 -9
  99. package/hedhog/data/dashboard_component_role.yaml +223 -223
  100. package/hedhog/data/dashboard_role.yaml +18 -18
  101. package/hedhog/data/route.yaml +2 -0
  102. package/hedhog/data/setting_group.yaml +955 -470
  103. package/hedhog/data/setting_subgroup.yaml +303 -0
  104. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +44 -18
  105. package/hedhog/frontend/app/configurations/[slug]/page.tsx.ejs +134 -27
  106. package/hedhog/frontend/app/configurations/layout.tsx.ejs +84 -23
  107. package/hedhog/frontend/app/dashboard/components/widgets/permissions-chart.tsx.ejs +62 -62
  108. package/hedhog/frontend/app/dashboard/page.tsx.ejs +29 -29
  109. package/hedhog/frontend/app/preferences/page.tsx.ejs +2 -5
  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/ai/ai.service.ts +3 -3
  117. package/src/auth/auth.controller.ts +11 -11
  118. package/src/auth/auth.service.ts +8 -8
  119. package/src/core.module.ts +4 -1
  120. package/src/index.ts +15 -0
  121. package/src/integration/README.md +397 -0
  122. package/src/integration/USAGE_EXAMPLE.md +279 -0
  123. package/src/integration/index.ts +4 -0
  124. package/src/integration/integration-api.validation.ts +154 -0
  125. package/src/integration/integration.module.ts +53 -0
  126. package/src/integration/services/domain-event.publisher.ts +136 -0
  127. package/src/integration/services/event-subscriber.registry.ts +89 -0
  128. package/src/integration/services/inbox.service.ts +218 -0
  129. package/src/integration/services/index.ts +12 -0
  130. package/src/integration/services/integration-developer-api.service.ts +96 -0
  131. package/src/integration/services/integration-link.service.ts +154 -0
  132. package/src/integration/services/integration-settings.service.ts +128 -0
  133. package/src/integration/services/outbox-polling.coordinator.ts +146 -0
  134. package/src/integration/services/outbox.notifier.ts +48 -0
  135. package/src/integration/services/outbox.processor.job.ts +97 -0
  136. package/src/integration/services/outbox.processor.ts +266 -0
  137. package/src/integration/services/outbox.service.ts +209 -0
  138. package/src/integration/types/event.types.ts +93 -0
  139. package/src/integration/types/index.ts +3 -0
  140. package/src/integration/types/subscriber.types.ts +37 -0
  141. package/src/oauth/oauth.controller.ts +17 -17
  142. package/src/oauth/oauth.service.ts +20 -20
  143. package/src/setting/setting.service.ts +27 -2
  144. package/src/task/task.service.ts +5 -5
@@ -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
+
@@ -0,0 +1,154 @@
1
+ import { strict as assert } from 'assert';
2
+ import { DomainEventPublisher } from './services/domain-event.publisher';
3
+ import { EventSubscriberRegistry } from './services/event-subscriber.registry';
4
+ import { InboxService } from './services/inbox.service';
5
+ import { IntegrationLinkService } from './services/integration-link.service';
6
+ import { LinkType } from './types';
7
+
8
+ async function validatePublisher(): Promise<void> {
9
+ const createdEvent = {
10
+ id: 'evt-1',
11
+ eventName: 'operations.project.billing_ready',
12
+ sourceModule: 'operations',
13
+ aggregateType: 'project',
14
+ aggregateId: 'prj-1',
15
+ payload: { projectId: 'prj-1' },
16
+ status: 'pending',
17
+ attemptCount: 0,
18
+ lastError: null,
19
+ availableAt: new Date(),
20
+ processedAt: null,
21
+ createdAt: new Date(),
22
+ updatedAt: new Date(),
23
+ };
24
+
25
+ const outboxService = {
26
+ createEvent: async () => createdEvent,
27
+ };
28
+
29
+ let notified = 0;
30
+ const notifier = {
31
+ notifyEventWritten: () => {
32
+ notified += 1;
33
+ },
34
+ };
35
+
36
+ const publisher = new DomainEventPublisher(
37
+ outboxService as any,
38
+ notifier as any,
39
+ );
40
+
41
+ const result = await publisher.publishEvent({
42
+ eventName: 'operations.project.billing_ready',
43
+ sourceModule: 'operations',
44
+ aggregateType: 'project',
45
+ aggregateId: 'prj-1',
46
+ payload: { projectId: 'prj-1' },
47
+ });
48
+
49
+ assert.equal(result.id, 'evt-1');
50
+ assert.equal(notified, 1);
51
+ }
52
+
53
+ async function validateRegistry(): Promise<void> {
54
+ const registry = new EventSubscriberRegistry();
55
+
56
+ registry.subscribe(
57
+ 'operations.contract.payable_generated',
58
+ 'consumer-low',
59
+ async () => undefined,
60
+ 1,
61
+ );
62
+ registry.subscribe(
63
+ 'operations.contract.payable_generated',
64
+ 'consumer-high',
65
+ async () => undefined,
66
+ 10,
67
+ );
68
+
69
+ const handlers = registry.getHandlers('operations.contract.payable_generated');
70
+ assert.equal(handlers.length, 2);
71
+ const firstHandler = handlers[0];
72
+ assert.ok(firstHandler);
73
+ assert.equal(firstHandler.consumerName, 'consumer-high');
74
+ }
75
+
76
+ async function validateLinks(): Promise<void> {
77
+ const link = {
78
+ id: 'link-1',
79
+ sourceModule: 'operations',
80
+ sourceEntityType: 'project',
81
+ sourceEntityId: 'prj-1',
82
+ targetModule: 'finance',
83
+ targetEntityType: 'payable',
84
+ targetEntityId: 'pay-1',
85
+ linkType: LinkType.REFERENCE,
86
+ metadata: null,
87
+ createdAt: new Date(),
88
+ updatedAt: new Date(),
89
+ };
90
+
91
+ const prisma = {
92
+ integrationLink: {
93
+ create: async () => link,
94
+ findMany: async () => [link],
95
+ },
96
+ };
97
+
98
+ const service = new IntegrationLinkService(prisma as any);
99
+ const created = await service.createLink({
100
+ sourceModule: 'operations',
101
+ sourceEntityType: 'project',
102
+ sourceEntityId: 'prj-1',
103
+ targetModule: 'finance',
104
+ targetEntityType: 'payable',
105
+ targetEntityId: 'pay-1',
106
+ linkType: LinkType.REFERENCE,
107
+ });
108
+
109
+ const outbound = await service.findOutbound('operations', 'project', 'prj-1');
110
+ assert.equal(created.id, 'link-1');
111
+ assert.equal(outbound.length, 1);
112
+ }
113
+
114
+ async function validateInboxIdempotency(): Promise<void> {
115
+ const existing = {
116
+ id: 1,
117
+ outbox_event_id: 1,
118
+ consumer_name: 'finance-module',
119
+ status: 'processed',
120
+ attempt_count: 1,
121
+ last_error: null,
122
+ processed_at: new Date(),
123
+ created_at: new Date(),
124
+ updated_at: new Date(),
125
+ };
126
+
127
+ const prisma = {
128
+ inbox_event: {
129
+ findFirst: async () => existing,
130
+ create: async () => {
131
+ throw new Error('create should not be called for existing inbox item');
132
+ },
133
+ },
134
+ };
135
+
136
+ const inbox = new InboxService(prisma as any);
137
+ const result = await inbox.getOrCreate('1', 'finance-module');
138
+ assert.equal(result.id, '1');
139
+ }
140
+
141
+ async function main(): Promise<void> {
142
+ await validatePublisher();
143
+ await validateRegistry();
144
+ await validateLinks();
145
+ await validateInboxIdempotency();
146
+ // eslint-disable-next-line no-console
147
+ console.log('Integration API validation passed');
148
+ }
149
+
150
+ main().catch((error) => {
151
+ // eslint-disable-next-line no-console
152
+ console.error('Integration API validation failed', error);
153
+ process.exit(1);
154
+ });
@@ -0,0 +1,53 @@
1
+ import { PrismaModule } from '@hed-hog/api-prisma';
2
+ import { Global, Module, forwardRef } from '@nestjs/common';
3
+ import { ScheduleModule } from '@nestjs/schedule';
4
+ import { SettingModule } from '../setting/setting.module';
5
+ import {
6
+ DomainEventPublisher,
7
+ EventSubscriberRegistry,
8
+ InboxService,
9
+ IntegrationDeveloperApiService,
10
+ IntegrationLinkService,
11
+ IntegrationSettingsService,
12
+ OutboxNotifier,
13
+ OutboxPollingCoordinator,
14
+ OutboxProcessor,
15
+ OutboxProcessorJob,
16
+ OutboxService,
17
+ } from './services';
18
+
19
+ @Global()
20
+ @Module({
21
+ imports: [
22
+ ScheduleModule.forRoot(),
23
+ PrismaModule,
24
+ forwardRef(() => SettingModule),
25
+ ],
26
+ providers: [
27
+ OutboxNotifier,
28
+ OutboxService,
29
+ InboxService,
30
+ IntegrationLinkService,
31
+ EventSubscriberRegistry,
32
+ IntegrationDeveloperApiService,
33
+ IntegrationSettingsService,
34
+ DomainEventPublisher,
35
+ OutboxProcessor,
36
+ OutboxPollingCoordinator,
37
+ OutboxProcessorJob,
38
+ ],
39
+ exports: [
40
+ OutboxNotifier,
41
+ OutboxService,
42
+ InboxService,
43
+ IntegrationLinkService,
44
+ EventSubscriberRegistry,
45
+ IntegrationDeveloperApiService,
46
+ IntegrationSettingsService,
47
+ DomainEventPublisher,
48
+ OutboxProcessor,
49
+ OutboxPollingCoordinator,
50
+ OutboxProcessorJob,
51
+ ],
52
+ })
53
+ export class IntegrationModule {}
@@ -0,0 +1,136 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { DomainEvent, OutboxEvent } from '../types';
3
+ import { OutboxNotifier } from './outbox.notifier';
4
+ import { OutboxEventPersistenceClient, OutboxService } from './outbox.service';
5
+
6
+ export interface PublishDomainEventInput {
7
+ eventName: string;
8
+ sourceModule: string;
9
+ aggregateType: string;
10
+ aggregateId: string;
11
+ payload?: Record<string, any>;
12
+ metadata?: Record<string, any>;
13
+ }
14
+
15
+ export interface PublishDomainEventOptions {
16
+ persistenceClient?: OutboxEventPersistenceClient;
17
+ }
18
+
19
+ @Injectable()
20
+ export class DomainEventPublisher {
21
+ constructor(
22
+ private readonly outboxService: OutboxService,
23
+ private readonly outboxNotifier: OutboxNotifier,
24
+ ) {}
25
+
26
+ /**
27
+ * Publish a domain event
28
+ * Writes to durable outbox and notifies processor for immediate wakeup
29
+ */
30
+ async publishEvent(
31
+ input: PublishDomainEventInput,
32
+ options?: PublishDomainEventOptions,
33
+ ): Promise<OutboxEvent>;
34
+
35
+ async publishEvent(
36
+ eventName: string,
37
+ sourceModule: string,
38
+ aggregateType: string,
39
+ aggregateId: string,
40
+ payload?: Record<string, any>,
41
+ metadata?: Record<string, any>,
42
+ options?: PublishDomainEventOptions,
43
+ ): Promise<OutboxEvent>;
44
+
45
+ async publishEvent(
46
+ eventOrName: PublishDomainEventInput | string,
47
+ sourceModuleOrOptions?: string | PublishDomainEventOptions,
48
+ aggregateType?: string,
49
+ aggregateId?: string,
50
+ payload?: Record<string, any>,
51
+ metadata?: Record<string, any>,
52
+ options?: PublishDomainEventOptions,
53
+ ): Promise<OutboxEvent> {
54
+ const payloadData = payload || {};
55
+ const input =
56
+ typeof eventOrName === 'string'
57
+ ? {
58
+ eventName: eventOrName,
59
+ sourceModule: sourceModuleOrOptions as string,
60
+ aggregateType: aggregateType as string,
61
+ aggregateId: aggregateId as string,
62
+ payload: payloadData,
63
+ metadata,
64
+ }
65
+ : eventOrName;
66
+
67
+ const publishOptions =
68
+ typeof eventOrName === 'string'
69
+ ? options
70
+ : (sourceModuleOrOptions as PublishDomainEventOptions | undefined);
71
+
72
+ const persistedPayload = this.enrichPayloadWithMetadata(
73
+ input.payload || {},
74
+ input.metadata,
75
+ );
76
+
77
+ // Write to outbox (durable, authoritative)
78
+ const outboxEvent = await this.outboxService.createEvent(
79
+ {
80
+ eventName: input.eventName,
81
+ sourceModule: input.sourceModule,
82
+ aggregateType: input.aggregateType,
83
+ aggregateId: input.aggregateId,
84
+ payload: persistedPayload,
85
+ },
86
+ publishOptions?.persistenceClient,
87
+ );
88
+
89
+ // Notify processor for immediate wakeup (best-effort, not guaranteed)
90
+ this.outboxNotifier.notifyEventWritten();
91
+
92
+ return outboxEvent;
93
+ }
94
+
95
+ /**
96
+ * Publish multiple events in sequence
97
+ */
98
+ async publishEvents(
99
+ events: Array<DomainEvent | PublishDomainEventInput>,
100
+ options?: PublishDomainEventOptions,
101
+ ): Promise<OutboxEvent[]> {
102
+ const published: OutboxEvent[] = [];
103
+
104
+ for (const event of events) {
105
+ published.push(
106
+ await this.publishEvent(
107
+ {
108
+ eventName: event.eventName,
109
+ sourceModule: event.sourceModule,
110
+ aggregateType: event.aggregateType,
111
+ aggregateId: event.aggregateId,
112
+ payload: event.payload,
113
+ metadata: (event as PublishDomainEventInput).metadata,
114
+ },
115
+ options,
116
+ ),
117
+ );
118
+ }
119
+
120
+ return published;
121
+ }
122
+
123
+ private enrichPayloadWithMetadata(
124
+ payload: Record<string, any>,
125
+ metadata?: Record<string, any>,
126
+ ): Record<string, any> {
127
+ if (!metadata) {
128
+ return payload;
129
+ }
130
+
131
+ return {
132
+ ...payload,
133
+ _integrationMeta: metadata,
134
+ };
135
+ }
136
+ }