@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.
- 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/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/dashboard_component_role.yaml +223 -223
- package/hedhog/data/dashboard_role.yaml +18 -18
- 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/dashboard/components/widgets/permissions-chart.tsx.ejs +62 -62
- package/hedhog/frontend/app/dashboard/page.tsx.ejs +29 -29
- package/hedhog/frontend/app/preferences/page.tsx.ejs +2 -5
- 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/ai/ai.service.ts +3 -3
- package/src/auth/auth.controller.ts +11 -11
- package/src/auth/auth.service.ts +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
- package/src/task/task.service.ts +5 -5
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import { EventHandler, SubscriberDefinition } from '../types';
|
|
3
|
+
|
|
4
|
+
@Injectable()
|
|
5
|
+
export class EventSubscriberRegistry {
|
|
6
|
+
private readonly subscribers = new Map<string, SubscriberDefinition[]>();
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Register a subscriber for an event
|
|
10
|
+
*/
|
|
11
|
+
registerHandler(definition: SubscriberDefinition): void {
|
|
12
|
+
const { eventName } = definition;
|
|
13
|
+
|
|
14
|
+
if (!this.subscribers.has(eventName)) {
|
|
15
|
+
this.subscribers.set(eventName, []);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
this.subscribers.get(eventName)!.push(definition);
|
|
19
|
+
|
|
20
|
+
// Sort by priority (higher priority first)
|
|
21
|
+
const handlers = this.subscribers.get(eventName)!;
|
|
22
|
+
handlers.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Ergonomic registration for module services.
|
|
27
|
+
*/
|
|
28
|
+
subscribe(
|
|
29
|
+
eventName: string,
|
|
30
|
+
consumerName: string,
|
|
31
|
+
handler: EventHandler,
|
|
32
|
+
priority = 0,
|
|
33
|
+
): void {
|
|
34
|
+
this.registerHandler({
|
|
35
|
+
eventName,
|
|
36
|
+
consumerName,
|
|
37
|
+
handler,
|
|
38
|
+
priority,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Register multiple handlers in one call.
|
|
44
|
+
*/
|
|
45
|
+
subscribeMany(definitions: SubscriberDefinition[]): void {
|
|
46
|
+
for (const definition of definitions) {
|
|
47
|
+
this.registerHandler(definition);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Get all handlers for an event, sorted by priority
|
|
53
|
+
*/
|
|
54
|
+
getHandlers(eventName: string): SubscriberDefinition[] {
|
|
55
|
+
return this.subscribers.get(eventName) || [];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Check if event has any handlers
|
|
60
|
+
*/
|
|
61
|
+
hasHandlers(eventName: string): boolean {
|
|
62
|
+
return this.subscribers.has(eventName) && this.subscribers.get(eventName)!.length > 0;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get all registered event names
|
|
67
|
+
*/
|
|
68
|
+
getEventNames(): string[] {
|
|
69
|
+
return Array.from(this.subscribers.keys());
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Get total count of all registered handlers
|
|
74
|
+
*/
|
|
75
|
+
getHandlerCount(): number {
|
|
76
|
+
let count = 0;
|
|
77
|
+
for (const handlers of this.subscribers.values()) {
|
|
78
|
+
count += handlers.length;
|
|
79
|
+
}
|
|
80
|
+
return count;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Clear all handlers (useful for testing)
|
|
85
|
+
*/
|
|
86
|
+
clear(): void {
|
|
87
|
+
this.subscribers.clear();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { PrismaService } from '@hed-hog/api-prisma';
|
|
2
|
+
import { Injectable } from '@nestjs/common';
|
|
3
|
+
import { InboxEvent, InboxEventStatus } from '../types';
|
|
4
|
+
|
|
5
|
+
interface InboxEventRecord {
|
|
6
|
+
id: number;
|
|
7
|
+
outbox_event_id: number;
|
|
8
|
+
consumer_name: string;
|
|
9
|
+
status: InboxEventStatus;
|
|
10
|
+
attempt_count: number;
|
|
11
|
+
last_error: string | null;
|
|
12
|
+
processed_at: Date | null;
|
|
13
|
+
created_at: Date;
|
|
14
|
+
updated_at: Date;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@Injectable()
|
|
18
|
+
export class InboxService {
|
|
19
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
20
|
+
|
|
21
|
+
private toDomainEvent(record: InboxEventRecord): InboxEvent {
|
|
22
|
+
return {
|
|
23
|
+
id: String(record.id),
|
|
24
|
+
outboxEventId: String(record.outbox_event_id),
|
|
25
|
+
consumerName: record.consumer_name,
|
|
26
|
+
status: record.status,
|
|
27
|
+
attemptCount: record.attempt_count,
|
|
28
|
+
lastError: record.last_error,
|
|
29
|
+
processedAt: record.processed_at,
|
|
30
|
+
createdAt: record.created_at,
|
|
31
|
+
updatedAt: record.updated_at,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private toDatabaseId(eventId: string | number): number {
|
|
36
|
+
return typeof eventId === 'number' ? eventId : Number(eventId);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
private toDatabaseUpdate(
|
|
40
|
+
partialUpdate?: {
|
|
41
|
+
attemptCount?: number;
|
|
42
|
+
lastError?: string | null;
|
|
43
|
+
processedAt?: Date | null;
|
|
44
|
+
},
|
|
45
|
+
) {
|
|
46
|
+
if (!partialUpdate) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
attempt_count: partialUpdate.attemptCount,
|
|
52
|
+
last_error: partialUpdate.lastError,
|
|
53
|
+
processed_at: partialUpdate.processedAt,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Get or create inbox event for idempotency tracking
|
|
59
|
+
* Returns existing if already processed, otherwise creates new
|
|
60
|
+
*/
|
|
61
|
+
async getOrCreate(
|
|
62
|
+
outboxEventId: string | number,
|
|
63
|
+
consumerName: string,
|
|
64
|
+
): Promise<InboxEvent> {
|
|
65
|
+
// Try to find existing
|
|
66
|
+
let inboxEvent = await this.prisma.inbox_event.findFirst({
|
|
67
|
+
where: {
|
|
68
|
+
outbox_event_id: this.toDatabaseId(outboxEventId),
|
|
69
|
+
consumer_name: consumerName,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Create if doesn't exist
|
|
74
|
+
if (!inboxEvent) {
|
|
75
|
+
inboxEvent = await this.prisma.inbox_event.create({
|
|
76
|
+
data: {
|
|
77
|
+
outbox_event_id: this.toDatabaseId(outboxEventId),
|
|
78
|
+
consumer_name: consumerName,
|
|
79
|
+
status: InboxEventStatus.RECEIVED,
|
|
80
|
+
attempt_count: 0,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return this.toDomainEvent(inboxEvent as InboxEventRecord);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Update inbox event status
|
|
90
|
+
*/
|
|
91
|
+
async updateStatus(
|
|
92
|
+
inboxEventId: string | number,
|
|
93
|
+
status: InboxEventStatus,
|
|
94
|
+
partialUpdate?: {
|
|
95
|
+
attemptCount?: number;
|
|
96
|
+
lastError?: string | null;
|
|
97
|
+
processedAt?: Date | null;
|
|
98
|
+
},
|
|
99
|
+
): Promise<InboxEvent> {
|
|
100
|
+
const inboxEvent = await this.prisma.inbox_event.update({
|
|
101
|
+
where: { id: this.toDatabaseId(inboxEventId) },
|
|
102
|
+
data: {
|
|
103
|
+
status,
|
|
104
|
+
...this.toDatabaseUpdate(partialUpdate),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return this.toDomainEvent(inboxEvent as InboxEventRecord);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Mark inbox event as processing
|
|
113
|
+
*/
|
|
114
|
+
async markProcessing(inboxEventId: string | number): Promise<InboxEvent> {
|
|
115
|
+
return this.updateStatus(inboxEventId, InboxEventStatus.PROCESSING);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Mark inbox event as successfully processed
|
|
120
|
+
*/
|
|
121
|
+
async markProcessed(inboxEventId: string | number): Promise<InboxEvent> {
|
|
122
|
+
return this.updateStatus(inboxEventId, InboxEventStatus.PROCESSED, {
|
|
123
|
+
processedAt: new Date(),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Mark inbox event as failed
|
|
129
|
+
*/
|
|
130
|
+
async markFailed(
|
|
131
|
+
inboxEventId: string | number,
|
|
132
|
+
error: string,
|
|
133
|
+
): Promise<InboxEvent> {
|
|
134
|
+
const record = await this.prisma.inbox_event.findUnique({
|
|
135
|
+
where: { id: this.toDatabaseId(inboxEventId) },
|
|
136
|
+
});
|
|
137
|
+
if (!record) {
|
|
138
|
+
throw new Error(`Inbox event ${inboxEventId} not found`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const inboxEvent = this.toDomainEvent(record as InboxEventRecord);
|
|
142
|
+
|
|
143
|
+
return this.updateStatus(inboxEventId, InboxEventStatus.FAILED, {
|
|
144
|
+
attemptCount: inboxEvent.attemptCount + 1,
|
|
145
|
+
lastError: error,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Mark inbox event as skipped
|
|
151
|
+
*/
|
|
152
|
+
async markSkipped(inboxEventId: string | number): Promise<InboxEvent> {
|
|
153
|
+
return this.updateStatus(inboxEventId, InboxEventStatus.SKIPPED);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Increment attempt count
|
|
158
|
+
*/
|
|
159
|
+
async incrementAttempt(inboxEventId: string | number): Promise<InboxEvent> {
|
|
160
|
+
const record = await this.prisma.inbox_event.findUnique({
|
|
161
|
+
where: { id: this.toDatabaseId(inboxEventId) },
|
|
162
|
+
});
|
|
163
|
+
if (!record) {
|
|
164
|
+
throw new Error(`Inbox event ${inboxEventId} not found`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const inboxEvent = this.toDomainEvent(record as InboxEventRecord);
|
|
168
|
+
|
|
169
|
+
return this.updateStatus(inboxEventId, inboxEvent.status, {
|
|
170
|
+
attemptCount: inboxEvent.attemptCount + 1,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Get inbox event by ID
|
|
176
|
+
*/
|
|
177
|
+
async getById(inboxEventId: string | number): Promise<InboxEvent | null> {
|
|
178
|
+
const inboxEvent = await this.prisma.inbox_event.findUnique({
|
|
179
|
+
where: { id: this.toDatabaseId(inboxEventId) },
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return inboxEvent ? this.toDomainEvent(inboxEvent as InboxEventRecord) : null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Check if event was already processed by consumer
|
|
187
|
+
*/
|
|
188
|
+
async isProcessed(
|
|
189
|
+
outboxEventId: string | number,
|
|
190
|
+
consumerName: string,
|
|
191
|
+
): Promise<boolean> {
|
|
192
|
+
const inboxEvent = await this.prisma.inbox_event.findFirst({
|
|
193
|
+
where: {
|
|
194
|
+
outbox_event_id: this.toDatabaseId(outboxEventId),
|
|
195
|
+
consumer_name: consumerName,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
inboxEvent !== null &&
|
|
201
|
+
(inboxEvent as InboxEventRecord).status === InboxEventStatus.PROCESSED
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Count unprocessed items for a consumer
|
|
207
|
+
*/
|
|
208
|
+
async countUnprocessed(consumerName: string): Promise<number> {
|
|
209
|
+
return this.prisma.inbox_event.count({
|
|
210
|
+
where: {
|
|
211
|
+
consumer_name: consumerName,
|
|
212
|
+
status: {
|
|
213
|
+
notIn: [InboxEventStatus.PROCESSED, InboxEventStatus.SKIPPED],
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './domain-event.publisher';
|
|
2
|
+
export * from './event-subscriber.registry';
|
|
3
|
+
export * from './inbox.service';
|
|
4
|
+
export * from './integration-developer-api.service';
|
|
5
|
+
export * from './integration-link.service';
|
|
6
|
+
export * from './integration-settings.service';
|
|
7
|
+
export * from './outbox-polling.coordinator';
|
|
8
|
+
export * from './outbox.notifier';
|
|
9
|
+
export * from './outbox.processor';
|
|
10
|
+
export * from './outbox.processor.job';
|
|
11
|
+
export * from './outbox.service';
|
|
12
|
+
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Injectable } from '@nestjs/common';
|
|
2
|
+
import {
|
|
3
|
+
DomainEvent,
|
|
4
|
+
EventHandler,
|
|
5
|
+
IntegrationLink,
|
|
6
|
+
LinkType,
|
|
7
|
+
OutboxEvent,
|
|
8
|
+
SubscriberDefinition,
|
|
9
|
+
} from '../types';
|
|
10
|
+
import {
|
|
11
|
+
DomainEventPublisher,
|
|
12
|
+
PublishDomainEventInput,
|
|
13
|
+
PublishDomainEventOptions,
|
|
14
|
+
} from './domain-event.publisher';
|
|
15
|
+
import { EventSubscriberRegistry } from './event-subscriber.registry';
|
|
16
|
+
import {
|
|
17
|
+
CreateIntegrationLinkDto,
|
|
18
|
+
IntegrationLinkPersistenceClient,
|
|
19
|
+
IntegrationLinkService,
|
|
20
|
+
} from './integration-link.service';
|
|
21
|
+
|
|
22
|
+
export interface IntegrationSubscriptionInput {
|
|
23
|
+
eventName: string;
|
|
24
|
+
consumerName: string;
|
|
25
|
+
handler: EventHandler;
|
|
26
|
+
priority?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface IntegrationLinkQuery {
|
|
30
|
+
module: string;
|
|
31
|
+
entityType: string;
|
|
32
|
+
entityId: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Injectable()
|
|
36
|
+
export class IntegrationDeveloperApiService {
|
|
37
|
+
constructor(
|
|
38
|
+
private readonly publisher: DomainEventPublisher,
|
|
39
|
+
private readonly subscriberRegistry: EventSubscriberRegistry,
|
|
40
|
+
private readonly linkService: IntegrationLinkService,
|
|
41
|
+
) {}
|
|
42
|
+
|
|
43
|
+
async publishEvent(
|
|
44
|
+
input: PublishDomainEventInput,
|
|
45
|
+
options?: PublishDomainEventOptions,
|
|
46
|
+
): Promise<OutboxEvent> {
|
|
47
|
+
return this.publisher.publishEvent(input, options);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async publishEvents(
|
|
51
|
+
events: Array<DomainEvent | PublishDomainEventInput>,
|
|
52
|
+
options?: PublishDomainEventOptions,
|
|
53
|
+
): Promise<OutboxEvent[]> {
|
|
54
|
+
return this.publisher.publishEvents(events, options);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
subscribe(input: IntegrationSubscriptionInput): void {
|
|
58
|
+
this.subscriberRegistry.subscribe(
|
|
59
|
+
input.eventName,
|
|
60
|
+
input.consumerName,
|
|
61
|
+
input.handler,
|
|
62
|
+
input.priority || 0,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
subscribeMany(definitions: SubscriberDefinition[]): void {
|
|
67
|
+
this.subscriberRegistry.subscribeMany(definitions);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async createLink(
|
|
71
|
+
dto: CreateIntegrationLinkDto,
|
|
72
|
+
persistenceClient?: IntegrationLinkPersistenceClient,
|
|
73
|
+
): Promise<IntegrationLink> {
|
|
74
|
+
return this.linkService.createLink(dto, persistenceClient);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async findLinksBySource(query: IntegrationLinkQuery): Promise<IntegrationLink[]> {
|
|
78
|
+
return this.linkService.findOutbound(
|
|
79
|
+
query.module,
|
|
80
|
+
query.entityType,
|
|
81
|
+
query.entityId,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async findLinksByTarget(query: IntegrationLinkQuery): Promise<IntegrationLink[]> {
|
|
86
|
+
return this.linkService.findInbound(
|
|
87
|
+
query.module,
|
|
88
|
+
query.entityType,
|
|
89
|
+
query.entityId,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async findLinksByType(linkType: LinkType): Promise<IntegrationLink[]> {
|
|
94
|
+
return this.linkService.findByLinkType(linkType);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { PrismaService } from '@hed-hog/api-prisma';
|
|
2
|
+
import { Injectable } from '@nestjs/common';
|
|
3
|
+
import { IntegrationLink, LinkType } from '../types';
|
|
4
|
+
|
|
5
|
+
export interface CreateIntegrationLinkDto {
|
|
6
|
+
sourceModule: string;
|
|
7
|
+
sourceEntityType: string;
|
|
8
|
+
sourceEntityId: string;
|
|
9
|
+
targetModule: string;
|
|
10
|
+
targetEntityType: string;
|
|
11
|
+
targetEntityId: string;
|
|
12
|
+
linkType: LinkType;
|
|
13
|
+
metadata?: Record<string, any>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface IntegrationLinkPersistenceClient {
|
|
17
|
+
integrationLink: PrismaService['integrationLink'];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@Injectable()
|
|
21
|
+
export class IntegrationLinkService {
|
|
22
|
+
constructor(private readonly prisma: PrismaService) {}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Create a link between entities from different modules
|
|
26
|
+
*/
|
|
27
|
+
async createLink(
|
|
28
|
+
dto: CreateIntegrationLinkDto,
|
|
29
|
+
persistenceClient?: IntegrationLinkPersistenceClient,
|
|
30
|
+
): Promise<IntegrationLink> {
|
|
31
|
+
const client = persistenceClient ?? this.prisma;
|
|
32
|
+
|
|
33
|
+
return client.integrationLink.create({
|
|
34
|
+
data: {
|
|
35
|
+
sourceModule: dto.sourceModule,
|
|
36
|
+
sourceEntityType: dto.sourceEntityType,
|
|
37
|
+
sourceEntityId: dto.sourceEntityId,
|
|
38
|
+
targetModule: dto.targetModule,
|
|
39
|
+
targetEntityType: dto.targetEntityType,
|
|
40
|
+
targetEntityId: dto.targetEntityId,
|
|
41
|
+
linkType: dto.linkType,
|
|
42
|
+
metadata: dto.metadata || null,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Find all links originating from a source entity
|
|
49
|
+
*/
|
|
50
|
+
async findOutbound(
|
|
51
|
+
sourceModule: string,
|
|
52
|
+
sourceEntityType: string,
|
|
53
|
+
sourceEntityId: string,
|
|
54
|
+
): Promise<IntegrationLink[]> {
|
|
55
|
+
return this.prisma.integrationLink.findMany({
|
|
56
|
+
where: {
|
|
57
|
+
sourceModule,
|
|
58
|
+
sourceEntityType,
|
|
59
|
+
sourceEntityId,
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Find all links terminating at a target entity
|
|
66
|
+
*/
|
|
67
|
+
async findInbound(
|
|
68
|
+
targetModule: string,
|
|
69
|
+
targetEntityType: string,
|
|
70
|
+
targetEntityId: string,
|
|
71
|
+
): Promise<IntegrationLink[]> {
|
|
72
|
+
return this.prisma.integrationLink.findMany({
|
|
73
|
+
where: {
|
|
74
|
+
targetModule,
|
|
75
|
+
targetEntityType,
|
|
76
|
+
targetEntityId,
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Find links of a specific type
|
|
83
|
+
*/
|
|
84
|
+
async findByLinkType(linkType: LinkType): Promise<IntegrationLink[]> {
|
|
85
|
+
return this.prisma.integrationLink.findMany({
|
|
86
|
+
where: { linkType },
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Delete a link
|
|
92
|
+
*/
|
|
93
|
+
async deleteLink(linkId: string): Promise<IntegrationLink> {
|
|
94
|
+
return this.prisma.integrationLink.delete({
|
|
95
|
+
where: { id: linkId },
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Get link by ID
|
|
101
|
+
*/
|
|
102
|
+
async getById(linkId: string): Promise<IntegrationLink | null> {
|
|
103
|
+
return this.prisma.integrationLink.findUnique({
|
|
104
|
+
where: { id: linkId },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Find bidirectional link between two entities
|
|
110
|
+
*/
|
|
111
|
+
async findBidirectional(
|
|
112
|
+
module1: string,
|
|
113
|
+
entity1Type: string,
|
|
114
|
+
entity1Id: string,
|
|
115
|
+
module2: string,
|
|
116
|
+
entity2Type: string,
|
|
117
|
+
entity2Id: string,
|
|
118
|
+
): Promise<IntegrationLink | null> {
|
|
119
|
+
// Look for forward or reverse direction
|
|
120
|
+
const link = await this.prisma.integrationLink.findFirst({
|
|
121
|
+
where: {
|
|
122
|
+
OR: [
|
|
123
|
+
{
|
|
124
|
+
sourceModule: module1,
|
|
125
|
+
sourceEntityType: entity1Type,
|
|
126
|
+
sourceEntityId: entity1Id,
|
|
127
|
+
targetModule: module2,
|
|
128
|
+
targetEntityType: entity2Type,
|
|
129
|
+
targetEntityId: entity2Id,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
sourceModule: module2,
|
|
133
|
+
sourceEntityType: entity2Type,
|
|
134
|
+
sourceEntityId: entity2Id,
|
|
135
|
+
targetModule: module1,
|
|
136
|
+
targetEntityType: entity1Type,
|
|
137
|
+
targetEntityId: entity1Id,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
return link;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Count links from a module
|
|
148
|
+
*/
|
|
149
|
+
async countFromModule(sourceModule: string): Promise<number> {
|
|
150
|
+
return this.prisma.integrationLink.count({
|
|
151
|
+
where: { sourceModule },
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { Injectable, Logger } from '@nestjs/common';
|
|
2
|
+
import { SettingService } from '../../setting/setting.service';
|
|
3
|
+
|
|
4
|
+
export interface IntegrationRuntimeSettings {
|
|
5
|
+
outboxEnabled: boolean;
|
|
6
|
+
outboxProcessorEnabled: boolean;
|
|
7
|
+
startupDrainEnabled: boolean;
|
|
8
|
+
pollingIntervalMs: number;
|
|
9
|
+
idlePollingIntervalMs: number;
|
|
10
|
+
batchSize: number;
|
|
11
|
+
startupDrainBatchSize: number;
|
|
12
|
+
maxAttempts: number;
|
|
13
|
+
processingLeaseMs: number;
|
|
14
|
+
retryBaseDelayMs: number;
|
|
15
|
+
deadLetterEnabled: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
@Injectable()
|
|
19
|
+
export class IntegrationSettingsService {
|
|
20
|
+
private readonly logger = new Logger(IntegrationSettingsService.name);
|
|
21
|
+
|
|
22
|
+
constructor(private readonly settingService: SettingService) {}
|
|
23
|
+
|
|
24
|
+
async getRuntimeSettings(): Promise<IntegrationRuntimeSettings> {
|
|
25
|
+
const values = await this.settingService.getSettingValues([
|
|
26
|
+
'outbox-enabled',
|
|
27
|
+
'outbox-processor-enabled',
|
|
28
|
+
'outbox-startup-drain-enabled',
|
|
29
|
+
'outbox-polling-interval-ms',
|
|
30
|
+
'outbox-idle-polling-interval-ms',
|
|
31
|
+
'outbox-batch-size',
|
|
32
|
+
'outbox-startup-drain-batch-size',
|
|
33
|
+
'outbox-max-attempts',
|
|
34
|
+
'outbox-processing-lease-ms',
|
|
35
|
+
'outbox-retry-base-delay-ms',
|
|
36
|
+
'outbox-dead-letter-enabled',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const pollingIntervalMs = this.getPositiveInt(
|
|
40
|
+
values['outbox-polling-interval-ms'],
|
|
41
|
+
5000,
|
|
42
|
+
100,
|
|
43
|
+
'outbox-polling-interval-ms',
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
let idlePollingIntervalMs = this.getPositiveInt(
|
|
47
|
+
values['outbox-idle-polling-interval-ms'],
|
|
48
|
+
30000,
|
|
49
|
+
100,
|
|
50
|
+
'outbox-idle-polling-interval-ms',
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
if (idlePollingIntervalMs < pollingIntervalMs) {
|
|
54
|
+
this.logger.warn(
|
|
55
|
+
`Setting outbox-idle-polling-interval-ms (${idlePollingIntervalMs}) is lower than outbox-polling-interval-ms (${pollingIntervalMs}). Using ${pollingIntervalMs}.`,
|
|
56
|
+
);
|
|
57
|
+
idlePollingIntervalMs = pollingIntervalMs;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
outboxEnabled: this.getBoolean(values['outbox-enabled'], true),
|
|
62
|
+
outboxProcessorEnabled: this.getBoolean(
|
|
63
|
+
values['outbox-processor-enabled'],
|
|
64
|
+
true,
|
|
65
|
+
),
|
|
66
|
+
startupDrainEnabled: this.getBoolean(
|
|
67
|
+
values['outbox-startup-drain-enabled'],
|
|
68
|
+
true,
|
|
69
|
+
),
|
|
70
|
+
pollingIntervalMs,
|
|
71
|
+
idlePollingIntervalMs,
|
|
72
|
+
batchSize: this.getPositiveInt(values['outbox-batch-size'], 10, 1, 'outbox-batch-size'),
|
|
73
|
+
startupDrainBatchSize: this.getPositiveInt(
|
|
74
|
+
values['outbox-startup-drain-batch-size'],
|
|
75
|
+
50,
|
|
76
|
+
1,
|
|
77
|
+
'outbox-startup-drain-batch-size',
|
|
78
|
+
),
|
|
79
|
+
maxAttempts: this.getPositiveInt(values['outbox-max-attempts'], 3, 1, 'outbox-max-attempts'),
|
|
80
|
+
processingLeaseMs: this.getPositiveInt(
|
|
81
|
+
values['outbox-processing-lease-ms'],
|
|
82
|
+
30000,
|
|
83
|
+
1000,
|
|
84
|
+
'outbox-processing-lease-ms',
|
|
85
|
+
),
|
|
86
|
+
retryBaseDelayMs: this.getPositiveInt(
|
|
87
|
+
values['outbox-retry-base-delay-ms'],
|
|
88
|
+
1000,
|
|
89
|
+
100,
|
|
90
|
+
'outbox-retry-base-delay-ms',
|
|
91
|
+
),
|
|
92
|
+
deadLetterEnabled: this.getBoolean(values['outbox-dead-letter-enabled'], true),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
private getPositiveInt(
|
|
97
|
+
value: unknown,
|
|
98
|
+
fallback: number,
|
|
99
|
+
min: number,
|
|
100
|
+
slug: string,
|
|
101
|
+
): number {
|
|
102
|
+
const parsed = Number(value);
|
|
103
|
+
if (!Number.isFinite(parsed) || parsed < min) {
|
|
104
|
+
this.logger.warn(
|
|
105
|
+
`Invalid ${slug} setting value (${String(value)}). Using fallback ${fallback}.`,
|
|
106
|
+
);
|
|
107
|
+
return fallback;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return Math.floor(parsed);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private getBoolean(value: unknown, fallback: boolean): boolean {
|
|
114
|
+
if (typeof value === 'boolean') {
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (value === 'true' || value === '1') {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (value === 'false' || value === '0') {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return fallback;
|
|
127
|
+
}
|
|
128
|
+
}
|