@hg-ts/outbox-client 0.7.14

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.
@@ -0,0 +1,2 @@
1
+ export * from './outbox.event.js';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export * from './outbox.event.js';
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC"}
@@ -0,0 +1,21 @@
1
+ import zod from '@hg-ts/validation';
2
+ export declare const KAFKA_CONFIG_SCHEMA: zod.ZodObject<{
3
+ host: zod.ZodPipe<zod.ZodString, zod.ZodTransform<string, unknown>>;
4
+ port: zod.ZodPipe<zod.ZodPipe<zod.ZodPipe<zod.ZodUnion<readonly [zod.ZodNumber, zod.ZodString]>, zod.ZodTransform<string | number, unknown>>, zod.ZodTransform<number, string | number>>, zod.ZodNumber>;
5
+ topic: zod.ZodPipe<zod.ZodString, zod.ZodTransform<string, unknown>>;
6
+ clientId: zod.ZodPipe<zod.ZodString, zod.ZodTransform<string, unknown>>;
7
+ groupId: zod.ZodOptional<zod.ZodPipe<zod.ZodString, zod.ZodTransform<string, unknown>>>;
8
+ groupIdPrefix: zod.ZodOptional<zod.ZodPipe<zod.ZodString, zod.ZodTransform<string, unknown>>>;
9
+ }, zod.z.core.$strip>;
10
+ declare const KafkaConfig_base: import("@hg-ts/validation").ZodDto<{
11
+ host: string;
12
+ port: number;
13
+ topic: string;
14
+ clientId: string;
15
+ groupId?: string | undefined;
16
+ groupIdPrefix?: string | undefined;
17
+ }>;
18
+ export declare class KafkaConfig extends KafkaConfig_base {
19
+ }
20
+ export {};
21
+ //# sourceMappingURL=kafka.config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kafka.config.d.ts","sourceRoot":"","sources":["../src/kafka.config.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,mBAAmB,CAAC;AAEpC,eAAO,MAAM,mBAAmB;;;;;;;qBAU9B,CAAC;;;;;;;;;AAEH,qBAAa,WAAY,SAAQ,gBAA6B;CAAG"}
@@ -0,0 +1,15 @@
1
+ import zod from '@hg-ts/validation';
2
+ export const KAFKA_CONFIG_SCHEMA = zod.object({
3
+ host: zod.string().enforceEnv('KAFKA_HOST'),
4
+ port: zod.union([zod.number(), zod.string()])
5
+ .enforceEnv('KAFKA_PORT')
6
+ .transform(value => Number(value))
7
+ .pipe(zod.number().positive().int()),
8
+ topic: zod.string().enforceEnv('KAFKA_TOPIC'),
9
+ clientId: zod.string().enforceEnv('KAFKA_CLIENT_ID'),
10
+ groupId: zod.string().enforceEnv('KAFKA_GROUP').optional(),
11
+ groupIdPrefix: zod.string().enforceEnv('KAFKA_GROUP_PREFIX').optional(),
12
+ });
13
+ export class KafkaConfig extends KAFKA_CONFIG_SCHEMA.toClass() {
14
+ }
15
+ //# sourceMappingURL=kafka.config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kafka.config.js","sourceRoot":"","sources":["../src/kafka.config.ts"],"names":[],"mappings":"AAAA,OAAO,GAAG,MAAM,mBAAmB,CAAC;AAEpC,MAAM,CAAC,MAAM,mBAAmB,GAAG,GAAG,CAAC,MAAM,CAAC;IAC7C,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,UAAU,CAAC,YAAY,CAAC;IAC3C,IAAI,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,MAAM,EAAE,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;SAC3C,UAAU,CAAC,YAAY,CAAC;SACxB,SAAS,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;SACjC,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,CAAC,GAAG,EAAE,CAAC;IACrC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC;IAC7C,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,UAAU,CAAC,iBAAiB,CAAC;IACpD,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,UAAU,CAAC,aAAa,CAAC,CAAC,QAAQ,EAAE;IAC1D,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,UAAU,CAAC,oBAAoB,CAAC,CAAC,QAAQ,EAAE;CACvE,CAAC,CAAC;AAEH,MAAM,OAAO,WAAY,SAAQ,mBAAmB,CAAC,OAAO,EAAE;CAAG"}
@@ -0,0 +1,20 @@
1
+ import { BaseEvent, EventParams } from '@hg-ts/events';
2
+ export type OutboxEventParams<Body extends JsonType> = Required<EventParams<Body>> & {
3
+ entityName: string;
4
+ entityId: string;
5
+ };
6
+ type JsonType = JsonPrimitive | JsonArray | JsonObject;
7
+ type JsonPrimitive = string | number | boolean | null;
8
+ type JsonArray = JsonType[];
9
+ type JsonObject = {
10
+ [key: string]: JsonType;
11
+ };
12
+ export declare class OutboxEvent<Body extends JsonType = JsonType> extends BaseEvent<Body> {
13
+ readonly id: string;
14
+ readonly entityName: string;
15
+ readonly entityId: string;
16
+ constructor(params: OutboxEventParams<Body>);
17
+ static fromString(data: string, events: Class<OutboxEvent, any[]>[]): Nullable<OutboxEvent>;
18
+ }
19
+ export {};
20
+ //# sourceMappingURL=outbox.event.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outbox.event.d.ts","sourceRoot":"","sources":["../src/outbox.event.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,SAAS,EACT,WAAW,EACX,MAAM,eAAe,CAAC;AAGvB,MAAM,MAAM,iBAAiB,CAAC,IAAI,SAAS,QAAQ,IAAI,QAAQ,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,GAAG;IACpF,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,KAAK,QAAQ,GAAG,aAAa,GAAG,SAAS,GAAG,UAAU,CAAC;AACvD,KAAK,aAAa,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC;AACtD,KAAK,SAAS,GAAG,QAAQ,EAAE,CAAC;AAC5B,KAAK,UAAU,GAAG;IACjB,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,CAAC;CACxB,CAAC;AAEF,qBAAa,WAAW,CAAC,IAAI,SAAS,QAAQ,GAAG,QAAQ,CAAE,SAAQ,SAAS,CAAC,IAAI,CAAC;IACjF,SAAgB,EAAE,EAAE,MAAM,CAAC;IAC3B,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,SAAgB,QAAQ,EAAE,MAAM,CAAC;gBAEd,MAAM,EAAE,iBAAiB,CAAC,IAAI,CAAC;WASpC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,WAAW,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,QAAQ,CAAC,WAAW,CAAC;CAkBlG"}
@@ -0,0 +1,29 @@
1
+ import { BaseEvent, } from '@hg-ts/events';
2
+ import { v7 as uuid } from 'uuid';
3
+ export class OutboxEvent extends BaseEvent {
4
+ id;
5
+ entityName;
6
+ entityId;
7
+ constructor(params) {
8
+ super(params);
9
+ this.id = uuid();
10
+ this.entityName = params.entityName;
11
+ this.entityId = params.entityId;
12
+ }
13
+ static fromString(data, events) {
14
+ const parsed = JSON.parse(data);
15
+ const parsedBody = JSON.parse(parsed.body);
16
+ const eventData = {
17
+ ...parsed,
18
+ body: parsedBody,
19
+ };
20
+ const eventCtor = events.find(ctor => ctor.name === eventData.name);
21
+ if (!eventCtor) {
22
+ return null;
23
+ }
24
+ const event = Object.create(eventCtor);
25
+ Object.assign(event, eventData);
26
+ return event;
27
+ }
28
+ }
29
+ //# sourceMappingURL=outbox.event.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outbox.event.js","sourceRoot":"","sources":["../src/outbox.event.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,SAAS,GAET,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,EAAE,IAAI,IAAI,EAAE,MAAM,MAAM,CAAC;AAclC,MAAM,OAAO,WAA8C,SAAQ,SAAe;IACjE,EAAE,CAAS;IACX,UAAU,CAAS;IACnB,QAAQ,CAAS;IAEjC,YAAmB,MAA+B;QACjD,KAAK,CAAC,MAAM,CAAC,CAAC;QAEd,IAAI,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC;QAEjB,IAAI,CAAC,UAAU,GAAG,MAAM,CAAC,UAAU,CAAC;QACpC,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC;IACjC,CAAC;IAEM,MAAM,CAAC,UAAU,CAAC,IAAY,EAAE,MAAmC;QACzE,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAA8B,CAAC;QAC7D,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC3C,MAAM,SAAS,GAAG;YACjB,GAAG,MAAM;YACT,IAAI,EAAE,UAAU;SAChB,CAAC;QAEF,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,IAAI,CAAC,CAAC;QAEpE,IAAI,CAAC,SAAS,EAAE,CAAC;YAChB,OAAO,IAAI,CAAC;QACb,CAAC;QACD,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAEvC,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;QAChC,OAAO,KAAK,CAAC;IACd,CAAC;CACD"}
@@ -0,0 +1,3 @@
1
+ export declare class OutboxModule {
2
+ }
3
+ //# sourceMappingURL=outbox.module.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outbox.module.d.ts","sourceRoot":"","sources":["../src/outbox.module.ts"],"names":[],"mappings":"AAUA,qBAca,YAAY;CAAG"}
@@ -0,0 +1,26 @@
1
+ import { __decorate } from "tslib";
2
+ import { ConfigLoader } from '@hg-ts/config-loader';
3
+ import { EventsModule } from '@hg-ts/events';
4
+ import { Global, Module, } from '@nestjs/common';
5
+ import { KafkaConfig } from './kafka.config.js';
6
+ import { OutboxService } from './outbox.service.js';
7
+ let OutboxModule = class OutboxModule {
8
+ };
9
+ OutboxModule = __decorate([
10
+ Global(),
11
+ Module({
12
+ imports: [EventsModule],
13
+ providers: [
14
+ OutboxService,
15
+ {
16
+ provide: KafkaConfig,
17
+ async useFactory(configLoader) {
18
+ return configLoader.load(KafkaConfig, 'kafka');
19
+ },
20
+ inject: [ConfigLoader],
21
+ },
22
+ ],
23
+ })
24
+ ], OutboxModule);
25
+ export { OutboxModule };
26
+ //# sourceMappingURL=outbox.module.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outbox.module.js","sourceRoot":"","sources":["../src/outbox.module.ts"],"names":[],"mappings":";AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EACN,MAAM,EACN,MAAM,GACN,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAgB7C,IAAM,YAAY,GAAlB,MAAM,YAAY;CAAG,CAAA;AAAf,YAAY;IAdxB,MAAM,EAAE;IACR,MAAM,CAAC;QACP,OAAO,EAAE,CAAC,YAAY,CAAC;QACvB,SAAS,EAAE;YACV,aAAa;YACb;gBACC,OAAO,EAAE,WAAW;gBACpB,KAAK,CAAC,UAAU,CAAC,YAA0B;oBAC1C,OAAO,YAAY,CAAC,IAAI,CAAC,WAAW,EAAE,OAAO,CAAC,CAAC;gBAChD,CAAC;gBACD,MAAM,EAAE,CAAC,YAAY,CAAC;aACtB;SACD;KACD,CAAC;GACW,YAAY,CAAG"}
@@ -0,0 +1,15 @@
1
+ import { OnModuleInit } from '@nestjs/common';
2
+ export declare class OutboxService implements OnModuleInit {
3
+ private readonly logger;
4
+ private readonly config;
5
+ private readonly emitter;
6
+ private client;
7
+ private consumer;
8
+ onModuleInit(): Promise<void>;
9
+ private getClient;
10
+ private getConsumer;
11
+ private getGroupId;
12
+ private handleMessage;
13
+ private createKafkaLogger;
14
+ }
15
+ //# sourceMappingURL=outbox.service.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outbox.service.d.ts","sourceRoot":"","sources":["../src/outbox.service.ts"],"names":[],"mappings":"AAEA,OAAO,EAEN,YAAY,EACZ,MAAM,gBAAgB,CAAC;AAcxB,qBAAa,aAAc,YAAW,YAAY;IAEjD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAEhC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAc;IAErC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;IAEvC,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,QAAQ,CAAW;IAEd,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ1C,OAAO,CAAC,SAAS;YAQH,WAAW;IAQzB,OAAO,CAAC,UAAU;YAIJ,aAAa;IAkB3B,OAAO,CAAC,iBAAiB;CA2BzB"}
@@ -0,0 +1,87 @@
1
+ import { __decorate, __metadata } from "tslib";
2
+ import { EventEmitter } from '@hg-ts/events';
3
+ import { Logger } from '@hg-ts/logger';
4
+ import { Inject, } from '@nestjs/common';
5
+ import { Kafka, logLevel, } from 'kafkajs';
6
+ import { v7 as uuid } from 'uuid';
7
+ import { KafkaConfig } from './kafka.config.js';
8
+ import { OutboxEvent } from './outbox.event.js';
9
+ export class OutboxService {
10
+ logger;
11
+ config;
12
+ emitter;
13
+ client;
14
+ consumer;
15
+ async onModuleInit() {
16
+ this.client = this.getClient();
17
+ this.consumer = await this.getConsumer();
18
+ await this.consumer.run({ eachMessage: async ({ message }) => this.handleMessage(message) });
19
+ }
20
+ getClient() {
21
+ return new Kafka({
22
+ clientId: this.config.clientId,
23
+ brokers: [`${this.config.host}:${this.config.port}`],
24
+ logCreator: this.createKafkaLogger(),
25
+ });
26
+ }
27
+ async getConsumer() {
28
+ const consumer = this.client.consumer({ groupId: this.getGroupId() });
29
+ await this.consumer.connect();
30
+ await this.consumer.subscribe({ topic: this.config.topic });
31
+ return consumer;
32
+ }
33
+ getGroupId() {
34
+ return this.config.groupId ?? `${this.config.groupIdPrefix ?? 'unknown'}-${uuid()}`;
35
+ }
36
+ async handleMessage(message) {
37
+ const rawEvent = message.value?.toString();
38
+ if (!rawEvent) {
39
+ this.logger.error(`No value in received message: key=${message.key?.toString()}`);
40
+ return;
41
+ }
42
+ const eventsToHandle = await this.emitter.getAllEvents();
43
+ const registeredOutboxEvents = eventsToHandle.filter((item) => item.prototype instanceof OutboxEvent);
44
+ const event = OutboxEvent.fromString(rawEvent, registeredOutboxEvents);
45
+ if (event) {
46
+ await this.emitter.emit(event);
47
+ }
48
+ }
49
+ createKafkaLogger() {
50
+ return () => ({ namespace, level, log }) => {
51
+ const { message, ...extra } = log;
52
+ const formatted = `[${namespace}] ${message}`;
53
+ switch (level) {
54
+ case logLevel.ERROR:
55
+ this.logger.error(formatted, JSON.stringify(extra));
56
+ break;
57
+ case logLevel.WARN:
58
+ this.logger.warning(formatted);
59
+ break;
60
+ case logLevel.INFO:
61
+ this.logger.info(formatted);
62
+ break;
63
+ case logLevel.DEBUG:
64
+ this.logger.debug(formatted);
65
+ break;
66
+ case logLevel.NOTHING:
67
+ this.logger.debug(formatted);
68
+ break;
69
+ default:
70
+ this.logger.error(formatted);
71
+ }
72
+ };
73
+ }
74
+ }
75
+ __decorate([
76
+ Inject(),
77
+ __metadata("design:type", Logger)
78
+ ], OutboxService.prototype, "logger", void 0);
79
+ __decorate([
80
+ Inject(),
81
+ __metadata("design:type", KafkaConfig)
82
+ ], OutboxService.prototype, "config", void 0);
83
+ __decorate([
84
+ Inject(),
85
+ __metadata("design:type", EventEmitter)
86
+ ], OutboxService.prototype, "emitter", void 0);
87
+ //# sourceMappingURL=outbox.service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"outbox.service.js","sourceRoot":"","sources":["../src/outbox.service.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@hg-ts/outbox-client",
3
+ "version": "0.7.14",
4
+ "main": "dist/index.js",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./dist/index.js",
8
+ "./vitest.setup": "./dist/vitest.setup.js"
9
+ },
10
+ "repository": "git@gitlab.com:hyper-graph/framework.git",
11
+ "scripts": {
12
+ "prepack": "turbo preparePackage",
13
+ "clear": "rm -rf dist",
14
+ "build": "tsc",
15
+ "build:dev": "tsc-watch",
16
+ "lint:ts": "lint-ts",
17
+ "lint:ts:fix": "lint-ts --fix"
18
+ },
19
+ "devDependencies": {
20
+ "@hg-ts-config/typescript": "0.7.14",
21
+ "@hg-ts/config-loader": "0.7.14",
22
+ "@hg-ts/events": "0.7.14",
23
+ "@hg-ts/linter": "0.7.14",
24
+ "@hg-ts/logger": "0.7.14",
25
+ "@hg-ts/types": "0.7.14",
26
+ "@hg-ts/validation": "0.7.14",
27
+ "@nestjs/common": "11.1.0",
28
+ "@types/node": "22.19.1",
29
+ "eslint": "9.18.0",
30
+ "reflect-metadata": "0.2.2",
31
+ "rxjs": "7.8.1",
32
+ "tsc-watch": "6.3.0",
33
+ "tslib": "2.8.1",
34
+ "typescript": "5.7.3",
35
+ "uuid": "11.1.0"
36
+ },
37
+ "peerDependencies": {
38
+ "@hg-ts/events": "0.7.14",
39
+ "@hg-ts/logger": "0.7.14",
40
+ "@hg-ts/validation": "0.7.14",
41
+ "@nestjs/common": "*",
42
+ "reflect-metadata": "*",
43
+ "rxjs": "*",
44
+ "tslib": "*",
45
+ "uuid": "*"
46
+ },
47
+ "dependencies": {
48
+ "kafkajs": "2.2.4"
49
+ }
50
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './outbox.event.js';
@@ -0,0 +1,15 @@
1
+ import zod from '@hg-ts/validation';
2
+
3
+ export const KAFKA_CONFIG_SCHEMA = zod.object({
4
+ host: zod.string().enforceEnv('KAFKA_HOST'),
5
+ port: zod.union([zod.number(), zod.string()])
6
+ .enforceEnv('KAFKA_PORT')
7
+ .transform(value => Number(value))
8
+ .pipe(zod.number().positive().int()),
9
+ topic: zod.string().enforceEnv('KAFKA_TOPIC'),
10
+ clientId: zod.string().enforceEnv('KAFKA_CLIENT_ID'),
11
+ groupId: zod.string().enforceEnv('KAFKA_GROUP').optional(),
12
+ groupIdPrefix: zod.string().enforceEnv('KAFKA_GROUP_PREFIX').optional(),
13
+ });
14
+
15
+ export class KafkaConfig extends KAFKA_CONFIG_SCHEMA.toClass() {}
@@ -0,0 +1,51 @@
1
+ import {
2
+ BaseEvent,
3
+ EventParams,
4
+ } from '@hg-ts/events';
5
+ import { v7 as uuid } from 'uuid';
6
+
7
+ export type OutboxEventParams<Body extends JsonType> = Required<EventParams<Body>> & {
8
+ entityName: string;
9
+ entityId: string;
10
+ };
11
+
12
+ type JsonType = JsonPrimitive | JsonArray | JsonObject;
13
+ type JsonPrimitive = string | number | boolean | null;
14
+ type JsonArray = JsonType[];
15
+ type JsonObject = {
16
+ [key: string]: JsonType;
17
+ };
18
+
19
+ export class OutboxEvent<Body extends JsonType = JsonType> extends BaseEvent<Body> {
20
+ public readonly id: string;
21
+ public readonly entityName: string;
22
+ public readonly entityId: string;
23
+
24
+ public constructor(params: OutboxEventParams<Body>) {
25
+ super(params);
26
+
27
+ this.id = uuid();
28
+
29
+ this.entityName = params.entityName;
30
+ this.entityId = params.entityId;
31
+ }
32
+
33
+ public static fromString(data: string, events: Class<OutboxEvent, any[]>[]): Nullable<OutboxEvent> {
34
+ const parsed = JSON.parse(data) as OutboxEventParams<string>;
35
+ const parsedBody = JSON.parse(parsed.body);
36
+ const eventData = {
37
+ ...parsed,
38
+ body: parsedBody,
39
+ };
40
+
41
+ const eventCtor = events.find(ctor => ctor.name === eventData.name);
42
+
43
+ if (!eventCtor) {
44
+ return null;
45
+ }
46
+ const event = Object.create(eventCtor);
47
+
48
+ Object.assign(event, eventData);
49
+ return event;
50
+ }
51
+ }
@@ -0,0 +1,25 @@
1
+ import { ConfigLoader } from '@hg-ts/config-loader';
2
+ import { EventsModule } from '@hg-ts/events';
3
+ import {
4
+ Global,
5
+ Module,
6
+ } from '@nestjs/common';
7
+
8
+ import { KafkaConfig } from './kafka.config.js';
9
+ import { OutboxService } from './outbox.service.js';
10
+
11
+ @Global()
12
+ @Module({
13
+ imports: [EventsModule],
14
+ providers: [
15
+ OutboxService,
16
+ {
17
+ provide: KafkaConfig,
18
+ async useFactory(configLoader: ConfigLoader): Promise<KafkaConfig> {
19
+ return configLoader.load(KafkaConfig, 'kafka');
20
+ },
21
+ inject: [ConfigLoader],
22
+ },
23
+ ],
24
+ })
25
+ export class OutboxModule {}
@@ -0,0 +1,104 @@
1
+ import { EventEmitter } from '@hg-ts/events';
2
+ import { Logger } from '@hg-ts/logger';
3
+ import {
4
+ Inject,
5
+ OnModuleInit,
6
+ } from '@nestjs/common';
7
+ import {
8
+ Consumer,
9
+ Kafka,
10
+ KafkaMessage,
11
+ logCreator,
12
+ LogEntry,
13
+ logLevel,
14
+ } from 'kafkajs';
15
+ import { v7 as uuid } from 'uuid';
16
+
17
+ import { KafkaConfig } from './kafka.config.js';
18
+ import { OutboxEvent } from './outbox.event.js';
19
+
20
+ export class OutboxService implements OnModuleInit {
21
+ @Inject()
22
+ private readonly logger: Logger;
23
+ @Inject()
24
+ private readonly config: KafkaConfig;
25
+ @Inject()
26
+ private readonly emitter: EventEmitter;
27
+
28
+ private client: Kafka;
29
+ private consumer: Consumer;
30
+
31
+ public async onModuleInit(): Promise<void> {
32
+ this.client = this.getClient();
33
+
34
+ this.consumer = await this.getConsumer();
35
+
36
+ await this.consumer.run({ eachMessage: async({ message }) => this.handleMessage(message) });
37
+ }
38
+
39
+ private getClient(): Kafka {
40
+ return new Kafka({
41
+ clientId: this.config.clientId,
42
+ brokers: [`${this.config.host}:${this.config.port}`],
43
+ logCreator: this.createKafkaLogger(),
44
+ });
45
+ }
46
+
47
+ private async getConsumer(): Promise<Consumer> {
48
+ const consumer = this.client.consumer({ groupId: this.getGroupId() });
49
+ await this.consumer.connect();
50
+ await this.consumer.subscribe({ topic: this.config.topic });
51
+
52
+ return consumer;
53
+ }
54
+
55
+ private getGroupId(): string {
56
+ return this.config.groupId ?? `${this.config.groupIdPrefix ?? 'unknown'}-${uuid()}`;
57
+ }
58
+
59
+ private async handleMessage(message: KafkaMessage): Promise<void> {
60
+ const rawEvent = message.value?.toString();
61
+ if (!rawEvent) {
62
+ this.logger.error(`No value in received message: key=${message.key?.toString()}`);
63
+ return;
64
+ }
65
+
66
+ const eventsToHandle = await this.emitter.getAllEvents();
67
+ const registeredOutboxEvents = eventsToHandle.filter(
68
+ (item): item is Class<OutboxEvent<any>> => item.prototype instanceof OutboxEvent,
69
+ );
70
+
71
+ const event = OutboxEvent.fromString(rawEvent, registeredOutboxEvents);
72
+ if (event) {
73
+ await this.emitter.emit(event);
74
+ }
75
+ }
76
+
77
+ private createKafkaLogger(): logCreator {
78
+ return () => ({ namespace, level, log }: LogEntry) => {
79
+ const { message, ...extra } = log;
80
+
81
+ const formatted = `[${namespace}] ${message}`;
82
+
83
+ switch (level) {
84
+ case logLevel.ERROR:
85
+ this.logger.error(formatted, JSON.stringify(extra));
86
+ break;
87
+ case logLevel.WARN:
88
+ this.logger.warning(formatted);
89
+ break;
90
+ case logLevel.INFO:
91
+ this.logger.info(formatted);
92
+ break;
93
+ case logLevel.DEBUG:
94
+ this.logger.debug(formatted);
95
+ break;
96
+ case logLevel.NOTHING:
97
+ this.logger.debug(formatted);
98
+ break;
99
+ default:
100
+ this.logger.error(formatted);
101
+ }
102
+ };
103
+ }
104
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "extends": "@hg-ts-config/typescript/esm",
3
+ "compilerOptions": {
4
+ "baseUrl": "src",
5
+ "rootDir": "src",
6
+ "outDir": "dist"
7
+ },
8
+ "exclude": ["dist"],
9
+ "include": ["src"]
10
+ }