@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.
- package/dist/auth/auth.controller.d.ts +4 -4
- package/dist/auth/auth.service.d.ts +4 -4
- package/dist/challenge/challenge.service.d.ts +2 -2
- package/dist/core.module.d.ts.map +1 -1
- package/dist/core.module.js +4 -1
- package/dist/core.module.js.map +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +1 -1
- package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +2 -2
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/dist/integration/index.d.ts +4 -0
- package/dist/integration/index.d.ts.map +1 -0
- package/dist/integration/index.js +20 -0
- package/dist/integration/index.js.map +1 -0
- package/dist/integration/integration-api.validation.d.ts +2 -0
- package/dist/integration/integration-api.validation.d.ts.map +1 -0
- package/dist/integration/integration-api.validation.js +126 -0
- package/dist/integration/integration-api.validation.js.map +1 -0
- package/dist/integration/integration.module.d.ts +3 -0
- package/dist/integration/integration.module.d.ts.map +1 -0
- package/dist/integration/integration.module.js +54 -0
- package/dist/integration/integration.module.js.map +1 -0
- package/dist/integration/services/domain-event.publisher.d.ts +31 -0
- package/dist/integration/services/domain-event.publisher.d.ts.map +1 -0
- package/dist/integration/services/domain-event.publisher.js +79 -0
- package/dist/integration/services/domain-event.publisher.js.map +1 -0
- package/dist/integration/services/event-subscriber.registry.d.ts +37 -0
- package/dist/integration/services/event-subscriber.registry.d.ts.map +1 -0
- package/dist/integration/services/event-subscriber.registry.js +86 -0
- package/dist/integration/services/event-subscriber.registry.js.map +1 -0
- package/dist/integration/services/inbox.service.d.ts +55 -0
- package/dist/integration/services/inbox.service.d.ts.map +1 -0
- package/dist/integration/services/inbox.service.js +173 -0
- package/dist/integration/services/inbox.service.js.map +1 -0
- package/dist/integration/services/index.d.ts +12 -0
- package/dist/integration/services/index.d.ts.map +1 -0
- package/dist/integration/services/index.js +28 -0
- package/dist/integration/services/index.js.map +1 -0
- package/dist/integration/services/integration-developer-api.service.d.ts +30 -0
- package/dist/integration/services/integration-developer-api.service.d.ts.map +1 -0
- package/dist/integration/services/integration-developer-api.service.js +55 -0
- package/dist/integration/services/integration-developer-api.service.js.map +1 -0
- package/dist/integration/services/integration-link.service.d.ts +52 -0
- package/dist/integration/services/integration-link.service.d.ts.map +1 -0
- package/dist/integration/services/integration-link.service.js +128 -0
- package/dist/integration/services/integration-link.service.js.map +1 -0
- package/dist/integration/services/integration-settings.service.d.ts +23 -0
- package/dist/integration/services/integration-settings.service.d.ts.map +1 -0
- package/dist/integration/services/integration-settings.service.js +81 -0
- package/dist/integration/services/integration-settings.service.js.map +1 -0
- package/dist/integration/services/outbox-polling.coordinator.d.ts +45 -0
- package/dist/integration/services/outbox-polling.coordinator.d.ts.map +1 -0
- package/dist/integration/services/outbox-polling.coordinator.js +143 -0
- package/dist/integration/services/outbox-polling.coordinator.js.map +1 -0
- package/dist/integration/services/outbox.notifier.d.ts +30 -0
- package/dist/integration/services/outbox.notifier.d.ts.map +1 -0
- package/dist/integration/services/outbox.notifier.js +57 -0
- package/dist/integration/services/outbox.notifier.js.map +1 -0
- package/dist/integration/services/outbox.processor.d.ts +42 -0
- package/dist/integration/services/outbox.processor.d.ts.map +1 -0
- package/dist/integration/services/outbox.processor.job.d.ts +43 -0
- package/dist/integration/services/outbox.processor.job.d.ts.map +1 -0
- package/dist/integration/services/outbox.processor.job.js +100 -0
- package/dist/integration/services/outbox.processor.job.js.map +1 -0
- package/dist/integration/services/outbox.processor.js +208 -0
- package/dist/integration/services/outbox.processor.js.map +1 -0
- package/dist/integration/services/outbox.service.d.ts +53 -0
- package/dist/integration/services/outbox.service.d.ts.map +1 -0
- package/dist/integration/services/outbox.service.js +149 -0
- package/dist/integration/services/outbox.service.js.map +1 -0
- package/dist/integration/types/event.types.d.ts +88 -0
- package/dist/integration/types/event.types.d.ts.map +1 -0
- package/dist/integration/types/event.types.js +35 -0
- package/dist/integration/types/event.types.js.map +1 -0
- package/dist/integration/types/index.d.ts +3 -0
- package/dist/integration/types/index.d.ts.map +1 -0
- package/dist/integration/types/index.js +19 -0
- package/dist/integration/types/index.js.map +1 -0
- package/dist/integration/types/subscriber.types.d.ts +31 -0
- package/dist/integration/types/subscriber.types.d.ts.map +1 -0
- package/dist/integration/types/subscriber.types.js +3 -0
- package/dist/integration/types/subscriber.types.js.map +1 -0
- package/dist/mail/mail.controller.d.ts +2 -2
- package/dist/mail/mail.service.d.ts +2 -2
- package/dist/mail-sent/mail-sent.controller.d.ts +3 -3
- package/dist/mail-sent/mail-sent.service.d.ts +3 -3
- package/dist/oauth/oauth.controller.js.map +1 -1
- package/dist/oauth/oauth.service.d.ts +3 -3
- package/dist/oauth/oauth.service.d.ts.map +1 -1
- package/dist/oauth/oauth.service.js.map +1 -1
- package/dist/profile/profile.controller.d.ts +3 -3
- package/dist/profile/profile.service.d.ts +3 -3
- package/dist/setting/setting.controller.d.ts +12 -8
- package/dist/setting/setting.controller.d.ts.map +1 -1
- package/dist/setting/setting.service.d.ts +12 -8
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +21 -1
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/user/user.controller.d.ts +4 -4
- package/dist/user/user.service.d.ts +9 -9
- package/hedhog/data/route.yaml +2 -0
- package/hedhog/data/setting_group.yaml +955 -470
- package/hedhog/data/setting_subgroup.yaml +303 -0
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +44 -18
- package/hedhog/frontend/app/configurations/[slug]/page.tsx.ejs +134 -27
- package/hedhog/frontend/app/configurations/layout.tsx.ejs +84 -23
- package/hedhog/frontend/app/preferences/page.tsx.ejs +45 -48
- package/hedhog/table/inbox_event.yaml +40 -0
- package/hedhog/table/integration_link.yaml +33 -0
- package/hedhog/table/outbox_event.yaml +45 -0
- package/hedhog/table/setting.yaml +7 -0
- package/hedhog/table/setting_subgroup.yaml +19 -0
- package/package.json +8 -8
- package/src/core.module.ts +4 -1
- package/src/index.ts +15 -0
- package/src/integration/README.md +397 -0
- package/src/integration/USAGE_EXAMPLE.md +279 -0
- package/src/integration/index.ts +4 -0
- package/src/integration/integration-api.validation.ts +154 -0
- package/src/integration/integration.module.ts +53 -0
- package/src/integration/services/domain-event.publisher.ts +136 -0
- package/src/integration/services/event-subscriber.registry.ts +89 -0
- package/src/integration/services/inbox.service.ts +218 -0
- package/src/integration/services/index.ts +12 -0
- package/src/integration/services/integration-developer-api.service.ts +96 -0
- package/src/integration/services/integration-link.service.ts +154 -0
- package/src/integration/services/integration-settings.service.ts +128 -0
- package/src/integration/services/outbox-polling.coordinator.ts +146 -0
- package/src/integration/services/outbox.notifier.ts +48 -0
- package/src/integration/services/outbox.processor.job.ts +97 -0
- package/src/integration/services/outbox.processor.ts +266 -0
- package/src/integration/services/outbox.service.ts +209 -0
- package/src/integration/types/event.types.ts +93 -0
- package/src/integration/types/index.ts +3 -0
- package/src/integration/types/subscriber.types.ts +37 -0
- package/src/oauth/oauth.controller.ts +17 -17
- package/src/oauth/oauth.service.ts +20 -20
- 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,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 {
|
|
4
|
+
import { user_account_provider_52222e2ecb_enum } from '@hed-hog/api-prisma';
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
120
|
+
return this.service.handleCallback({ res, locale, ipAddress, userAgent, provider: provider as user_account_provider_52222e2ecb_enum, email, type: 'disconnect' });
|
|
121
121
|
}
|
|
122
122
|
}
|