@hed-hog/core 0.0.294 → 0.0.296

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