@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,266 @@
1
+ import { Injectable, Logger } from '@nestjs/common';
2
+ import { InboxEventStatus, OutboxEventStatus } from '../types';
3
+ import { EventSubscriberRegistry } from './event-subscriber.registry';
4
+ import { InboxService } from './inbox.service';
5
+ import { IntegrationLinkService } from './integration-link.service';
6
+ import { IntegrationSettingsService } from './integration-settings.service';
7
+ import { OutboxService } from './outbox.service';
8
+
9
+ @Injectable()
10
+ export class OutboxProcessor {
11
+ private readonly logger = new Logger(OutboxProcessor.name);
12
+
13
+ constructor(
14
+ private readonly outboxService: OutboxService,
15
+ private readonly inboxService: InboxService,
16
+ private readonly linkService: IntegrationLinkService,
17
+ private readonly registry: EventSubscriberRegistry,
18
+ private readonly integrationSettingsService: IntegrationSettingsService,
19
+ ) {}
20
+
21
+ /**
22
+ * Process a batch of pending events
23
+ * Called in a loop or by scheduled job
24
+ */
25
+ async processBatch(options?: { batchSizeOverride?: number }): Promise<number> {
26
+ const settings = await this.integrationSettingsService.getRuntimeSettings();
27
+ const batchSize = options?.batchSizeOverride ?? settings.batchSize;
28
+ const leaseMs = settings.processingLeaseMs;
29
+
30
+ // Find pending events
31
+ const events = await this.outboxService.findPending(batchSize, leaseMs);
32
+
33
+ if (events.length === 0) {
34
+ return 0;
35
+ }
36
+
37
+ this.logger.debug(`Processing ${events.length} outbox events`);
38
+
39
+ let processedCount = 0;
40
+
41
+ for (const event of events) {
42
+ try {
43
+ // Mark as processing with lease
44
+ await this.outboxService.markProcessing(event.id, leaseMs);
45
+
46
+ // Get handlers for this event
47
+ const handlers = this.registry.getHandlers(event.eventName);
48
+
49
+ if (handlers.length === 0) {
50
+ this.logger.warn(
51
+ `No handlers registered for event: ${event.eventName}`,
52
+ );
53
+ // Mark as processed even though no handlers
54
+ await this.outboxService.updateStatus(
55
+ event.id,
56
+ OutboxEventStatus.PROCESSED,
57
+ {
58
+ processedAt: new Date(),
59
+ },
60
+ );
61
+ processedCount++;
62
+ continue;
63
+ }
64
+
65
+ // Execute each handler
66
+ let anyFailed = false;
67
+ for (const handlerDef of handlers) {
68
+ const inboxEvent = await this.inboxService.getOrCreate(
69
+ event.id,
70
+ handlerDef.consumerName,
71
+ );
72
+
73
+ // Skip if already processed
74
+ if (inboxEvent.status === InboxEventStatus.PROCESSED) {
75
+ this.logger.debug(
76
+ `Event ${event.id} already processed by ${handlerDef.consumerName}`,
77
+ );
78
+ continue;
79
+ }
80
+
81
+ await this.inboxService.markProcessing(inboxEvent.id);
82
+
83
+ try {
84
+ // Execute handler
85
+ await handlerDef.handler(
86
+ {
87
+ eventName: event.eventName,
88
+ sourceModule: event.sourceModule,
89
+ aggregateType: event.aggregateType,
90
+ aggregateId: event.aggregateId,
91
+ payload: event.payload,
92
+ timestamp: event.createdAt,
93
+ },
94
+ {
95
+ logger: this.logger,
96
+ inboxService: this.inboxService,
97
+ linkService: this.linkService,
98
+ },
99
+ );
100
+
101
+ // Mark as processed
102
+ await this.inboxService.markProcessed(inboxEvent.id);
103
+ this.logger.debug(
104
+ `Handler ${handlerDef.consumerName} processed event ${event.id}`,
105
+ );
106
+ } catch (error) {
107
+ anyFailed = true;
108
+ this.logger.error(
109
+ `Handler ${handlerDef.consumerName} failed for event ${event.id}:`,
110
+ error,
111
+ );
112
+ await this.inboxService.markFailed(
113
+ inboxEvent.id,
114
+ error instanceof Error ? error.message : String(error),
115
+ );
116
+ }
117
+ }
118
+
119
+ // Update outbox event status based on handler results
120
+ if (!anyFailed) {
121
+ await this.outboxService.updateStatus(
122
+ event.id,
123
+ OutboxEventStatus.PROCESSED,
124
+ {
125
+ processedAt: new Date(),
126
+ },
127
+ );
128
+ processedCount++;
129
+ } else {
130
+ // At least one handler failed, retry logic applies
131
+ await this.handleEventFailure(event.id);
132
+ }
133
+ } catch (error) {
134
+ this.logger.error(`Error processing event ${event.id}:`, error);
135
+ await this.handleEventFailure(event.id);
136
+ }
137
+ }
138
+
139
+ return processedCount;
140
+ }
141
+
142
+ /**
143
+ * Handle event that failed to process
144
+ */
145
+ private async handleEventFailure(eventId: string): Promise<void> {
146
+ const settings = await this.integrationSettingsService.getRuntimeSettings();
147
+ const maxAttempts = settings.maxAttempts;
148
+ const retryBaseDelayMs = settings.retryBaseDelayMs;
149
+ const deadLetterEnabled = settings.deadLetterEnabled;
150
+
151
+ const event = await this.outboxService.getById(eventId);
152
+ if (!event) {
153
+ return;
154
+ }
155
+
156
+ const nextAttemptCount = event.attemptCount + 1;
157
+
158
+ if (nextAttemptCount >= maxAttempts) {
159
+ if (deadLetterEnabled) {
160
+ this.logger.warn(
161
+ `Event ${eventId} moved to dead letter after ${maxAttempts} attempts`,
162
+ );
163
+ await this.outboxService.updateStatus(
164
+ eventId,
165
+ OutboxEventStatus.DEAD_LETTER,
166
+ {
167
+ attemptCount: nextAttemptCount,
168
+ },
169
+ );
170
+ } else {
171
+ this.logger.warn(
172
+ `Event ${eventId} failed after ${maxAttempts} attempts (dead letter disabled)`,
173
+ );
174
+ await this.outboxService.updateStatus(
175
+ eventId,
176
+ OutboxEventStatus.FAILED,
177
+ {
178
+ attemptCount: nextAttemptCount,
179
+ },
180
+ );
181
+ }
182
+ } else {
183
+ // Calculate exponential backoff
184
+ const delayMs = retryBaseDelayMs * Math.pow(2, event.attemptCount);
185
+ const availableAt = new Date(Date.now() + delayMs);
186
+
187
+ this.logger.debug(
188
+ `Retrying event ${eventId} in ${delayMs}ms (attempt ${nextAttemptCount}/${maxAttempts})`,
189
+ );
190
+
191
+ await this.outboxService.updateStatus(
192
+ eventId,
193
+ OutboxEventStatus.PENDING,
194
+ {
195
+ attemptCount: nextAttemptCount,
196
+ availableAt,
197
+ },
198
+ );
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Process all pending events at startup (drain)
204
+ */
205
+ async startupDrain(): Promise<number> {
206
+ const settings = await this.integrationSettingsService.getRuntimeSettings();
207
+ const drainBatchSize = settings.startupDrainBatchSize;
208
+
209
+ this.logger.log(`Starting outbox drain with batch size: ${drainBatchSize}`);
210
+
211
+ let totalProcessed = 0;
212
+
213
+ // Keep processing until no more events
214
+ let processed = 1;
215
+ while (processed > 0) {
216
+ processed = await this.processBatch({
217
+ batchSizeOverride: drainBatchSize,
218
+ });
219
+ totalProcessed += processed;
220
+
221
+ // Small delay to prevent overwhelming the system
222
+ if (processed > 0) {
223
+ await new Promise((resolve) => setTimeout(resolve, 100));
224
+ }
225
+ }
226
+
227
+ this.logger.log(`Startup drain completed. Processed ${totalProcessed} events`);
228
+ return totalProcessed;
229
+ }
230
+
231
+ /**
232
+ * Get processor statistics
233
+ */
234
+ async getStats() {
235
+ const pendingCount = await this.outboxService.countByStatus(
236
+ OutboxEventStatus.PENDING,
237
+ );
238
+ const processingCount = await this.outboxService.countByStatus(
239
+ OutboxEventStatus.PROCESSING,
240
+ );
241
+ const processedCount = await this.outboxService.countByStatus(
242
+ OutboxEventStatus.PROCESSED,
243
+ );
244
+ const failedCount = await this.outboxService.countByStatus(
245
+ OutboxEventStatus.FAILED,
246
+ );
247
+ const deadLetterCount = await this.outboxService.countByStatus(
248
+ OutboxEventStatus.DEAD_LETTER,
249
+ );
250
+
251
+ return {
252
+ pending: pendingCount,
253
+ processing: processingCount,
254
+ processed: processedCount,
255
+ failed: failedCount,
256
+ dead_letter: deadLetterCount,
257
+ total:
258
+ pendingCount +
259
+ processingCount +
260
+ processedCount +
261
+ failedCount +
262
+ deadLetterCount,
263
+ handlerCount: this.registry.getHandlerCount(),
264
+ };
265
+ }
266
+ }
@@ -0,0 +1,209 @@
1
+ import { PrismaService } from '@hed-hog/api-prisma';
2
+ import { Injectable } from '@nestjs/common';
3
+ import { OutboxEvent, OutboxEventStatus } from '../types';
4
+
5
+ export interface CreateOutboxEventDto {
6
+ eventName: string;
7
+ sourceModule: string;
8
+ aggregateType: string;
9
+ aggregateId: string;
10
+ payload: Record<string, any>;
11
+ }
12
+
13
+ export interface OutboxEventPersistenceClient {
14
+ outbox_event: PrismaService['outbox_event'];
15
+ }
16
+
17
+ interface OutboxEventRecord {
18
+ id: number;
19
+ event_name: string;
20
+ source_module: string;
21
+ aggregate_type: string;
22
+ aggregate_id: string;
23
+ payload: Record<string, any>;
24
+ status: OutboxEventStatus;
25
+ attempt_count: number;
26
+ last_error: string | null;
27
+ available_at: Date;
28
+ processed_at: Date | null;
29
+ created_at: Date;
30
+ updated_at: Date;
31
+ }
32
+
33
+ @Injectable()
34
+ export class OutboxService {
35
+ constructor(private readonly prisma: PrismaService) {}
36
+
37
+ private toDomainEvent(record: OutboxEventRecord): OutboxEvent {
38
+ return {
39
+ id: String(record.id),
40
+ eventName: record.event_name,
41
+ sourceModule: record.source_module,
42
+ aggregateType: record.aggregate_type,
43
+ aggregateId: record.aggregate_id,
44
+ payload: record.payload,
45
+ status: record.status,
46
+ attemptCount: record.attempt_count,
47
+ lastError: record.last_error,
48
+ availableAt: record.available_at,
49
+ processedAt: record.processed_at,
50
+ createdAt: record.created_at,
51
+ updatedAt: record.updated_at,
52
+ };
53
+ }
54
+
55
+ private toDatabaseId(eventId: string | number): number {
56
+ return typeof eventId === 'number' ? eventId : Number(eventId);
57
+ }
58
+
59
+ private toDatabaseUpdate(
60
+ partialUpdate?: {
61
+ attemptCount?: number;
62
+ lastError?: string | null;
63
+ availableAt?: Date;
64
+ processedAt?: Date | null;
65
+ },
66
+ ) {
67
+ if (!partialUpdate) {
68
+ return undefined;
69
+ }
70
+
71
+ return {
72
+ attempt_count: partialUpdate.attemptCount,
73
+ last_error: partialUpdate.lastError,
74
+ available_at: partialUpdate.availableAt,
75
+ processed_at: partialUpdate.processedAt,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Write a new event to the outbox
81
+ */
82
+ async createEvent(
83
+ dto: CreateOutboxEventDto,
84
+ persistenceClient?: OutboxEventPersistenceClient,
85
+ ): Promise<OutboxEvent> {
86
+ const client = persistenceClient ?? this.prisma;
87
+
88
+ const record = await client.outbox_event.create({
89
+ data: {
90
+ event_name: dto.eventName,
91
+ source_module: dto.sourceModule,
92
+ aggregate_type: dto.aggregateType,
93
+ aggregate_id: dto.aggregateId,
94
+ payload: dto.payload,
95
+ status: OutboxEventStatus.PENDING,
96
+ available_at: new Date(),
97
+ attempt_count: 0,
98
+ },
99
+ });
100
+
101
+ return this.toDomainEvent(record as OutboxEventRecord);
102
+ }
103
+
104
+ /**
105
+ * Find pending events available for processing, ordered by creation time
106
+ */
107
+ async findPending(
108
+ limit: number,
109
+ leaseMs: number,
110
+ ): Promise<OutboxEvent[]> {
111
+ const now = new Date();
112
+ const leaseThreshold = new Date(now.getTime() - leaseMs);
113
+
114
+ const records = await this.prisma.outbox_event.findMany({
115
+ where: {
116
+ OR: [
117
+ { status: OutboxEventStatus.PENDING },
118
+ {
119
+ status: OutboxEventStatus.PROCESSING,
120
+ available_at: { lte: leaseThreshold },
121
+ },
122
+ ],
123
+ },
124
+ orderBy: { created_at: 'asc' },
125
+ take: limit,
126
+ });
127
+
128
+ return records.map((record) => this.toDomainEvent(record as OutboxEventRecord));
129
+ }
130
+
131
+ /**
132
+ * Update event status and related fields
133
+ */
134
+ async updateStatus(
135
+ eventId: string | number,
136
+ status: OutboxEventStatus,
137
+ partialUpdate?: {
138
+ attemptCount?: number;
139
+ lastError?: string | null;
140
+ availableAt?: Date;
141
+ processedAt?: Date | null;
142
+ },
143
+ ): Promise<OutboxEvent> {
144
+ const record = await this.prisma.outbox_event.update({
145
+ where: { id: this.toDatabaseId(eventId) },
146
+ data: {
147
+ status,
148
+ ...this.toDatabaseUpdate(partialUpdate),
149
+ },
150
+ });
151
+
152
+ return this.toDomainEvent(record as OutboxEventRecord);
153
+ }
154
+
155
+ /**
156
+ * Mark event as processing and set lease time
157
+ */
158
+ async markProcessing(
159
+ eventId: string | number,
160
+ leaseMs: number,
161
+ ): Promise<OutboxEvent> {
162
+ const leaseUntil = new Date(Date.now() + leaseMs);
163
+ return this.updateStatus(eventId, OutboxEventStatus.PROCESSING, {
164
+ availableAt: leaseUntil,
165
+ });
166
+ }
167
+
168
+ /**
169
+ * Increment attempt count and optionally update error
170
+ */
171
+ async incrementAttempt(
172
+ eventId: string | number,
173
+ error?: string,
174
+ ): Promise<OutboxEvent> {
175
+ const record = await this.prisma.outbox_event.findUnique({
176
+ where: { id: this.toDatabaseId(eventId) },
177
+ });
178
+ if (!record) {
179
+ throw new Error(`Event ${eventId} not found`);
180
+ }
181
+
182
+ const event = this.toDomainEvent(record as OutboxEventRecord);
183
+
184
+ return this.updateStatus(eventId, event.status, {
185
+ attemptCount: event.attemptCount + 1,
186
+ lastError: error || null,
187
+ });
188
+ }
189
+
190
+ /**
191
+ * Get event by ID
192
+ */
193
+ async getById(eventId: string | number): Promise<OutboxEvent | null> {
194
+ const record = await this.prisma.outbox_event.findUnique({
195
+ where: { id: this.toDatabaseId(eventId) },
196
+ });
197
+
198
+ return record ? this.toDomainEvent(record as OutboxEventRecord) : null;
199
+ }
200
+
201
+ /**
202
+ * Count events by status
203
+ */
204
+ async countByStatus(status: OutboxEventStatus): Promise<number> {
205
+ return this.prisma.outbox_event.count({
206
+ where: { status },
207
+ });
208
+ }
209
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Event status throughout the integration lifecycle
3
+ */
4
+ export enum OutboxEventStatus {
5
+ PENDING = 'pending',
6
+ PROCESSING = 'processing',
7
+ PROCESSED = 'processed',
8
+ FAILED = 'failed',
9
+ DEAD_LETTER = 'dead_letter',
10
+ }
11
+
12
+ /**
13
+ * Inbox event processing status per consumer
14
+ */
15
+ export enum InboxEventStatus {
16
+ RECEIVED = 'received',
17
+ PROCESSING = 'processing',
18
+ PROCESSED = 'processed',
19
+ FAILED = 'failed',
20
+ SKIPPED = 'skipped',
21
+ }
22
+
23
+ /**
24
+ * Type of link between entities from different modules
25
+ */
26
+ export enum LinkType {
27
+ REFERENCE = 'reference',
28
+ CASCADE = 'cascade',
29
+ AGGREGATE_ROOT = 'aggregate_root',
30
+ }
31
+
32
+ /**
33
+ * Domain event interface that is published and processed
34
+ */
35
+ export interface DomainEvent {
36
+ eventName: string;
37
+ sourceModule: string;
38
+ aggregateType: string;
39
+ aggregateId: string;
40
+ payload: Record<string, any>;
41
+ timestamp: Date;
42
+ }
43
+
44
+ /**
45
+ * Outbox event record from database
46
+ */
47
+ export interface OutboxEvent {
48
+ id: string;
49
+ eventName: string;
50
+ sourceModule: string;
51
+ aggregateType: string;
52
+ aggregateId: string;
53
+ payload: Record<string, any>;
54
+ status: OutboxEventStatus;
55
+ attemptCount: number;
56
+ lastError: string | null;
57
+ availableAt: Date;
58
+ processedAt: Date | null;
59
+ createdAt: Date;
60
+ updatedAt: Date;
61
+ }
62
+
63
+ /**
64
+ * Inbox event record from database for idempotency tracking
65
+ */
66
+ export interface InboxEvent {
67
+ id: string;
68
+ outboxEventId: string;
69
+ consumerName: string;
70
+ status: InboxEventStatus;
71
+ attemptCount: number;
72
+ lastError: string | null;
73
+ processedAt: Date | null;
74
+ createdAt: Date;
75
+ updatedAt: Date;
76
+ }
77
+
78
+ /**
79
+ * Integration link between entities across modules
80
+ */
81
+ export interface IntegrationLink {
82
+ id: string;
83
+ sourceModule: string;
84
+ sourceEntityType: string;
85
+ sourceEntityId: string;
86
+ targetModule: string;
87
+ targetEntityType: string;
88
+ targetEntityId: string;
89
+ linkType: LinkType;
90
+ metadata: Record<string, any> | null;
91
+ createdAt: Date;
92
+ updatedAt: Date;
93
+ }
@@ -0,0 +1,3 @@
1
+ export * from './event.types';
2
+ export * from './subscriber.types';
3
+
@@ -0,0 +1,37 @@
1
+ import { Logger } from '@nestjs/common';
2
+ import { DomainEvent } from './event.types';
3
+
4
+ /**
5
+ * Context passed to event handlers
6
+ */
7
+ export interface SubscriberContext {
8
+ logger: Logger;
9
+ inboxService: any; // To be injected at runtime
10
+ linkService: any; // To be injected at runtime
11
+ }
12
+
13
+ /**
14
+ * Event handler function signature
15
+ */
16
+ export type EventHandler = (
17
+ event: DomainEvent,
18
+ context: SubscriberContext,
19
+ ) => Promise<void>;
20
+
21
+ /**
22
+ * Event subscriber definition
23
+ */
24
+ export interface SubscriberDefinition {
25
+ eventName: string;
26
+ consumerName: string;
27
+ priority?: number;
28
+ handler: EventHandler;
29
+ }
30
+
31
+ /**
32
+ * Subscription registry entry
33
+ */
34
+ export interface SubscriptionEntry {
35
+ definition: SubscriberDefinition;
36
+ handler: EventHandler;
37
+ }
@@ -1,17 +1,17 @@
1
1
 
2
2
  import { Public, User } from '@hed-hog/api';
3
3
  import { Locale } from '@hed-hog/api-locale';
4
- import { user_account_provider_enum } from '@hed-hog/api-prisma';
4
+ import { user_account_provider_52222e2ecb_enum } from '@hed-hog/api-prisma';
5
5
  import {
6
- Body,
7
- Controller,
8
- Delete,
9
- Get,
10
- Headers,
11
- Ip,
12
- Param,
13
- Query,
14
- Res,
6
+ Body,
7
+ Controller,
8
+ Delete,
9
+ Get,
10
+ Headers,
11
+ Ip,
12
+ Param,
13
+ Query,
14
+ Res,
15
15
  } from '@nestjs/common';
16
16
  import { SettingService } from '../setting/setting.service';
17
17
  import { OAuthService } from './oauth.service';
@@ -37,7 +37,7 @@ export class OAuthController {
37
37
  @Get(':provider/login')
38
38
  async login(@Param('provider') provider: string, @Res() res) {
39
39
  const url = await this.service.getAuthUrl(
40
- provider as user_account_provider_enum,
40
+ provider as user_account_provider_52222e2ecb_enum,
41
41
  `/callback/${provider}/login`,
42
42
  );
43
43
  return res.redirect(url);
@@ -48,7 +48,7 @@ export class OAuthController {
48
48
  @Get(':provider/register')
49
49
  async register(@Param('provider') provider: string, @Res() res) {
50
50
  const url = await this.service.getAuthUrl(
51
- provider as user_account_provider_enum,
51
+ provider as user_account_provider_52222e2ecb_enum,
52
52
  `/callback/${provider}/register`,
53
53
  );
54
54
  return res.redirect(url);
@@ -59,7 +59,7 @@ export class OAuthController {
59
59
  @Get(':provider/connect')
60
60
  async connect(@Param('provider') provider: string, @Res() res) {
61
61
  const url = this.service.getAuthUrl(
62
- provider as user_account_provider_enum,
62
+ provider as user_account_provider_52222e2ecb_enum,
63
63
  `/callback/${provider}/connect`,
64
64
  );
65
65
  return res.redirect(url);
@@ -76,7 +76,7 @@ export class OAuthController {
76
76
  @Query('code') code: string,
77
77
  @Res({ passthrough: true }) res,
78
78
  ) {
79
- return this.service.handleCallback({ res, locale, ipAddress, userAgent, provider: provider as user_account_provider_enum, code, type: 'login' });
79
+ return this.service.handleCallback({ res, locale, ipAddress, userAgent, provider: provider as user_account_provider_52222e2ecb_enum, code, type: 'login' });
80
80
  }
81
81
 
82
82
 
@@ -90,7 +90,7 @@ export class OAuthController {
90
90
  @Query('code') code: string,
91
91
  @Res({ passthrough: true }) res,
92
92
  ) {
93
- return this.service.handleCallback({ res, locale, ipAddress, userAgent, provider: provider as user_account_provider_enum, code, type: 'register' });
93
+ return this.service.handleCallback({ res, locale, ipAddress, userAgent, provider: provider as user_account_provider_52222e2ecb_enum, code, type: 'register' });
94
94
  }
95
95
 
96
96
 
@@ -104,7 +104,7 @@ export class OAuthController {
104
104
  @Query('code') code: string,
105
105
  @Res({ passthrough: true }) res,
106
106
  ) {
107
- return this.service.handleCallback({ res, locale, ipAddress, userAgent, provider: provider as user_account_provider_enum, code, type: 'connect', userId: id });
107
+ return this.service.handleCallback({ res, locale, ipAddress, userAgent, provider: provider as user_account_provider_52222e2ecb_enum, code, type: 'connect', userId: id });
108
108
  }
109
109
 
110
110
 
@@ -117,6 +117,6 @@ export class OAuthController {
117
117
  @Body('email') email: string,
118
118
  @Res({ passthrough: true }) res,
119
119
  ) {
120
- return this.service.handleCallback({ res, locale, ipAddress, userAgent, provider: provider as user_account_provider_enum, email, type: 'disconnect' });
120
+ return this.service.handleCallback({ res, locale, ipAddress, userAgent, provider: provider as user_account_provider_52222e2ecb_enum, email, type: 'disconnect' });
121
121
  }
122
122
  }