@hed-hog/core 0.0.295 → 0.0.296

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/dist/auth/auth.controller.d.ts +4 -4
  2. package/dist/auth/auth.service.d.ts +4 -4
  3. package/dist/challenge/challenge.service.d.ts +2 -2
  4. package/dist/core.module.d.ts.map +1 -1
  5. package/dist/core.module.js +4 -1
  6. package/dist/core.module.js.map +1 -1
  7. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +1 -1
  8. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +2 -2
  9. package/dist/index.d.ts +13 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +14 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/integration/index.d.ts +4 -0
  14. package/dist/integration/index.d.ts.map +1 -0
  15. package/dist/integration/index.js +20 -0
  16. package/dist/integration/index.js.map +1 -0
  17. package/dist/integration/integration-api.validation.d.ts +2 -0
  18. package/dist/integration/integration-api.validation.d.ts.map +1 -0
  19. package/dist/integration/integration-api.validation.js +126 -0
  20. package/dist/integration/integration-api.validation.js.map +1 -0
  21. package/dist/integration/integration.module.d.ts +3 -0
  22. package/dist/integration/integration.module.d.ts.map +1 -0
  23. package/dist/integration/integration.module.js +54 -0
  24. package/dist/integration/integration.module.js.map +1 -0
  25. package/dist/integration/services/domain-event.publisher.d.ts +31 -0
  26. package/dist/integration/services/domain-event.publisher.d.ts.map +1 -0
  27. package/dist/integration/services/domain-event.publisher.js +79 -0
  28. package/dist/integration/services/domain-event.publisher.js.map +1 -0
  29. package/dist/integration/services/event-subscriber.registry.d.ts +37 -0
  30. package/dist/integration/services/event-subscriber.registry.d.ts.map +1 -0
  31. package/dist/integration/services/event-subscriber.registry.js +86 -0
  32. package/dist/integration/services/event-subscriber.registry.js.map +1 -0
  33. package/dist/integration/services/inbox.service.d.ts +55 -0
  34. package/dist/integration/services/inbox.service.d.ts.map +1 -0
  35. package/dist/integration/services/inbox.service.js +173 -0
  36. package/dist/integration/services/inbox.service.js.map +1 -0
  37. package/dist/integration/services/index.d.ts +12 -0
  38. package/dist/integration/services/index.d.ts.map +1 -0
  39. package/dist/integration/services/index.js +28 -0
  40. package/dist/integration/services/index.js.map +1 -0
  41. package/dist/integration/services/integration-developer-api.service.d.ts +30 -0
  42. package/dist/integration/services/integration-developer-api.service.d.ts.map +1 -0
  43. package/dist/integration/services/integration-developer-api.service.js +55 -0
  44. package/dist/integration/services/integration-developer-api.service.js.map +1 -0
  45. package/dist/integration/services/integration-link.service.d.ts +52 -0
  46. package/dist/integration/services/integration-link.service.d.ts.map +1 -0
  47. package/dist/integration/services/integration-link.service.js +128 -0
  48. package/dist/integration/services/integration-link.service.js.map +1 -0
  49. package/dist/integration/services/integration-settings.service.d.ts +23 -0
  50. package/dist/integration/services/integration-settings.service.d.ts.map +1 -0
  51. package/dist/integration/services/integration-settings.service.js +81 -0
  52. package/dist/integration/services/integration-settings.service.js.map +1 -0
  53. package/dist/integration/services/outbox-polling.coordinator.d.ts +45 -0
  54. package/dist/integration/services/outbox-polling.coordinator.d.ts.map +1 -0
  55. package/dist/integration/services/outbox-polling.coordinator.js +143 -0
  56. package/dist/integration/services/outbox-polling.coordinator.js.map +1 -0
  57. package/dist/integration/services/outbox.notifier.d.ts +30 -0
  58. package/dist/integration/services/outbox.notifier.d.ts.map +1 -0
  59. package/dist/integration/services/outbox.notifier.js +57 -0
  60. package/dist/integration/services/outbox.notifier.js.map +1 -0
  61. package/dist/integration/services/outbox.processor.d.ts +42 -0
  62. package/dist/integration/services/outbox.processor.d.ts.map +1 -0
  63. package/dist/integration/services/outbox.processor.job.d.ts +43 -0
  64. package/dist/integration/services/outbox.processor.job.d.ts.map +1 -0
  65. package/dist/integration/services/outbox.processor.job.js +100 -0
  66. package/dist/integration/services/outbox.processor.job.js.map +1 -0
  67. package/dist/integration/services/outbox.processor.js +208 -0
  68. package/dist/integration/services/outbox.processor.js.map +1 -0
  69. package/dist/integration/services/outbox.service.d.ts +53 -0
  70. package/dist/integration/services/outbox.service.d.ts.map +1 -0
  71. package/dist/integration/services/outbox.service.js +149 -0
  72. package/dist/integration/services/outbox.service.js.map +1 -0
  73. package/dist/integration/types/event.types.d.ts +88 -0
  74. package/dist/integration/types/event.types.d.ts.map +1 -0
  75. package/dist/integration/types/event.types.js +35 -0
  76. package/dist/integration/types/event.types.js.map +1 -0
  77. package/dist/integration/types/index.d.ts +3 -0
  78. package/dist/integration/types/index.d.ts.map +1 -0
  79. package/dist/integration/types/index.js +19 -0
  80. package/dist/integration/types/index.js.map +1 -0
  81. package/dist/integration/types/subscriber.types.d.ts +31 -0
  82. package/dist/integration/types/subscriber.types.d.ts.map +1 -0
  83. package/dist/integration/types/subscriber.types.js +3 -0
  84. package/dist/integration/types/subscriber.types.js.map +1 -0
  85. package/dist/oauth/oauth.controller.js.map +1 -1
  86. package/dist/oauth/oauth.service.d.ts +3 -3
  87. package/dist/oauth/oauth.service.d.ts.map +1 -1
  88. package/dist/oauth/oauth.service.js.map +1 -1
  89. package/dist/profile/profile.controller.d.ts +3 -3
  90. package/dist/profile/profile.service.d.ts +3 -3
  91. package/dist/setting/setting.controller.d.ts +12 -8
  92. package/dist/setting/setting.controller.d.ts.map +1 -1
  93. package/dist/setting/setting.service.d.ts +12 -8
  94. package/dist/setting/setting.service.d.ts.map +1 -1
  95. package/dist/setting/setting.service.js +21 -1
  96. package/dist/setting/setting.service.js.map +1 -1
  97. package/dist/user/user.controller.d.ts +4 -4
  98. package/dist/user/user.service.d.ts +9 -9
  99. package/hedhog/data/dashboard_component_role.yaml +223 -223
  100. package/hedhog/data/dashboard_role.yaml +18 -18
  101. package/hedhog/data/route.yaml +2 -0
  102. package/hedhog/data/setting_group.yaml +955 -470
  103. package/hedhog/data/setting_subgroup.yaml +303 -0
  104. package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +44 -18
  105. package/hedhog/frontend/app/configurations/[slug]/page.tsx.ejs +134 -27
  106. package/hedhog/frontend/app/configurations/layout.tsx.ejs +84 -23
  107. package/hedhog/frontend/app/dashboard/components/widgets/permissions-chart.tsx.ejs +62 -62
  108. package/hedhog/frontend/app/dashboard/page.tsx.ejs +29 -29
  109. package/hedhog/frontend/app/preferences/page.tsx.ejs +2 -5
  110. package/hedhog/table/inbox_event.yaml +40 -0
  111. package/hedhog/table/integration_link.yaml +33 -0
  112. package/hedhog/table/outbox_event.yaml +45 -0
  113. package/hedhog/table/setting.yaml +7 -0
  114. package/hedhog/table/setting_subgroup.yaml +19 -0
  115. package/package.json +8 -8
  116. package/src/ai/ai.service.ts +3 -3
  117. package/src/auth/auth.controller.ts +11 -11
  118. package/src/auth/auth.service.ts +8 -8
  119. package/src/core.module.ts +4 -1
  120. package/src/index.ts +15 -0
  121. package/src/integration/README.md +397 -0
  122. package/src/integration/USAGE_EXAMPLE.md +279 -0
  123. package/src/integration/index.ts +4 -0
  124. package/src/integration/integration-api.validation.ts +154 -0
  125. package/src/integration/integration.module.ts +53 -0
  126. package/src/integration/services/domain-event.publisher.ts +136 -0
  127. package/src/integration/services/event-subscriber.registry.ts +89 -0
  128. package/src/integration/services/inbox.service.ts +218 -0
  129. package/src/integration/services/index.ts +12 -0
  130. package/src/integration/services/integration-developer-api.service.ts +96 -0
  131. package/src/integration/services/integration-link.service.ts +154 -0
  132. package/src/integration/services/integration-settings.service.ts +128 -0
  133. package/src/integration/services/outbox-polling.coordinator.ts +146 -0
  134. package/src/integration/services/outbox.notifier.ts +48 -0
  135. package/src/integration/services/outbox.processor.job.ts +97 -0
  136. package/src/integration/services/outbox.processor.ts +266 -0
  137. package/src/integration/services/outbox.service.ts +209 -0
  138. package/src/integration/types/event.types.ts +93 -0
  139. package/src/integration/types/index.ts +3 -0
  140. package/src/integration/types/subscriber.types.ts +37 -0
  141. package/src/oauth/oauth.controller.ts +17 -17
  142. package/src/oauth/oauth.service.ts +20 -20
  143. package/src/setting/setting.service.ts +27 -2
  144. package/src/task/task.service.ts +5 -5
@@ -0,0 +1,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
+ }