@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,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
|
+
|