@application-kit/kafka 0.1.1 → 0.3.0
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/CHANGELOG.md +14 -0
- package/README.md +23 -0
- package/dist/cjs/event-contract/index.d.ts +1 -0
- package/dist/cjs/event-contract/index.js +17 -0
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/kafka-commit-interceptor.js +1 -1
- package/dist/cjs/libs/topic-suffixes.js +1 -1
- package/dist/cjs/testing/assert-consumer-topics-consistent.d.ts +13 -0
- package/dist/cjs/testing/assert-consumer-topics-consistent.js +95 -0
- package/dist/cjs/testing/index.d.ts +1 -0
- package/dist/cjs/testing/index.js +17 -0
- package/dist/esm/event-contract/index.d.ts +1 -0
- package/dist/esm/event-contract/index.js +1 -0
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/kafka-commit-interceptor.js +1 -1
- package/dist/esm/libs/topic-suffixes.js +1 -1
- package/dist/esm/testing/assert-consumer-topics-consistent.d.ts +13 -0
- package/dist/esm/testing/assert-consumer-topics-consistent.js +88 -0
- package/dist/esm/testing/index.d.ts +1 -0
- package/dist/esm/testing/index.js +1 -0
- package/package.json +31 -3
- /package/dist/cjs/{event-contract-version.d.ts → event-contract/event-contract-version.d.ts} +0 -0
- /package/dist/cjs/{event-contract-version.js → event-contract/event-contract-version.js} +0 -0
- /package/dist/esm/{event-contract-version.d.ts → event-contract/event-contract-version.d.ts} +0 -0
- /package/dist/esm/{event-contract-version.js → event-contract/event-contract-version.js} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @application-kit/kafka
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- e9411eb: Спеки-гейты продукта переехали в пакеты: спека теперь — вызов помощника с параметрами продукта, а нарушение приходит ошибкой со списком.
|
|
8
|
+
|
|
9
|
+
`ui/testing`: `assertTokensDeclared`. `kafka/testing` (новый вход): `assertConsumerTopicsConsistent`. `authorization/testing` (новый вход): `assertAbilityRegistryConsistent`. `nestjs-vite/static-assets-gate` (новый вход, отдельно от `/testing`, чтобы спеки с фейком рендерера не грузили `vite`): `assertStaticAssetsLayout`. `lint-config` разрешает импорт нового входа `nestjs-vite/static-assets-gate`.
|
|
10
|
+
|
|
11
|
+
## 0.2.0
|
|
12
|
+
|
|
13
|
+
### Minor Changes
|
|
14
|
+
|
|
15
|
+
- 487f5e6: Новый вход `@application-kit/kafka/event-contract` — правила версии контракта события (`versionedQueueName`, `eventVersionOfTopic`, `eventVersionOfHeaders`, `DEFAULT_EVENT_VERSION`, `EVENT_VERSION_HEADER`) без `kafkajs` и `@nestjs/microservices`: производитель события (outbox) берёт имя очереди оттуда и не грузит транспорт в web-процесс. `kafkajs` и `@nestjs/microservices` стали опциональными peers — они нужны только корневому входу. Корень пакета экспортирует то же, что раньше.
|
|
16
|
+
|
|
3
17
|
## 0.1.1
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -12,6 +12,8 @@ yarn add @application-kit/kafka
|
|
|
12
12
|
|
|
13
13
|
Peer-зависимости: `@nestjs/common`, `@nestjs/core`, `@nestjs/microservices`, `kafkajs`, `rxjs`,
|
|
14
14
|
`@libs-for-dev/nestjs-config-module`, `class-transformer`, `class-validator`, `nestjs-pino`.
|
|
15
|
+
`@nestjs/microservices` и `kafkajs` нужны только корневому входу и объявлены опциональными: процессу,
|
|
16
|
+
который берёт лишь `@application-kit/kafka/event-contract`, ставить их не нужно.
|
|
15
17
|
|
|
16
18
|
Пакет грузится одним экземпляром на процесс: DI-токены — `Symbol()`, и ESM- и CJS-копии разом
|
|
17
19
|
дали бы два набора токенов.
|
|
@@ -95,3 +97,24 @@ export class UserConsumer extends ConsumerAbstract {
|
|
|
95
97
|
потребитель подписывается `eventTopics(queue, version)`, а труба сверяет версию топика
|
|
96
98
|
(`eventVersionOfTopic`) с заголовком `x-event-version` (`eventVersionOfHeaders`) до вызова
|
|
97
99
|
обработчика: сообщение чужой версии уходит в DLQ, не доходя до кода потребителя.
|
|
100
|
+
|
|
101
|
+
Правила версии живут и отдельным входом `@application-kit/kafka/event-contract` — без `kafkajs`,
|
|
102
|
+
`@nestjs/microservices` и побочных эффектов. Его берёт производитель события (web-процесс, outbox),
|
|
103
|
+
которому транспорт не нужен; корень пакета экспортирует то же самое.
|
|
104
|
+
|
|
105
|
+
## Гейт реестра топиков
|
|
106
|
+
|
|
107
|
+
Имя очереди живёт в манифесте модуля, статике `queueName` события, подписке консьюмера литералом и ключе
|
|
108
|
+
`kafka.eventQueues` values, и типом эти носители не связаны. `assertConsumerTopicsConsistent` из входа
|
|
109
|
+
`@application-kit/kafka/testing` читает их как текст и падает со списком расхождений:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
await assertConsumerTopicsConsistent({
|
|
113
|
+
chartValuesFile: 'ops/.helm/values-product.yaml',
|
|
114
|
+
consumerSources: '*/user-interface/consumers/*.ts',
|
|
115
|
+
eventSources: '**/domain/events/*.event.ts',
|
|
116
|
+
manifests: CONSUMER_MANIFESTS,
|
|
117
|
+
modulesDirectory: 'src/modules',
|
|
118
|
+
rootDirectory: process.cwd(),
|
|
119
|
+
})
|
|
120
|
+
```
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './event-contract-version.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./event-contract-version.js"), exports);
|
package/dist/cjs/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * from './configs/index.js';
|
|
2
|
+
export * from './event-contract/index.js';
|
|
2
3
|
export * from './libs/index.js';
|
|
3
4
|
export * from './constants.js';
|
|
4
|
-
export * from './event-contract-version.js';
|
|
5
5
|
export * from './kafka-commit-interceptor.js';
|
|
6
6
|
export * from './kafka-error-handler.js';
|
|
7
7
|
export * from './kafka.module.js';
|
package/dist/cjs/index.js
CHANGED
|
@@ -15,9 +15,9 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
__exportStar(require("./configs/index.js"), exports);
|
|
18
|
+
__exportStar(require("./event-contract/index.js"), exports);
|
|
18
19
|
__exportStar(require("./libs/index.js"), exports);
|
|
19
20
|
__exportStar(require("./constants.js"), exports);
|
|
20
|
-
__exportStar(require("./event-contract-version.js"), exports);
|
|
21
21
|
__exportStar(require("./kafka-commit-interceptor.js"), exports);
|
|
22
22
|
__exportStar(require("./kafka-error-handler.js"), exports);
|
|
23
23
|
__exportStar(require("./kafka.module.js"), exports);
|
|
@@ -16,7 +16,7 @@ exports.KafkaAutoCommit = exports.KafkaCommitInterceptor = void 0;
|
|
|
16
16
|
const common_1 = require("@nestjs/common");
|
|
17
17
|
const rxjs_1 = require("rxjs");
|
|
18
18
|
const constants_js_1 = require("./constants.js");
|
|
19
|
-
const event_contract_version_js_1 = require("./event-contract-version.js");
|
|
19
|
+
const event_contract_version_js_1 = require("./event-contract/event-contract-version.js");
|
|
20
20
|
const kafka_error_handler_js_1 = require("./kafka-error-handler.js");
|
|
21
21
|
let KafkaCommitInterceptor = class KafkaCommitInterceptor {
|
|
22
22
|
kafkaErrorHandler;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.eventTopics = void 0;
|
|
4
|
-
const event_contract_version_js_1 = require("../event-contract-version.js");
|
|
4
|
+
const event_contract_version_js_1 = require("../event-contract/event-contract-version.js");
|
|
5
5
|
const RETRY_TOPIC_SUFFIX = 'retry';
|
|
6
6
|
const eventTopics = (queueName, version = event_contract_version_js_1.DEFAULT_EVENT_VERSION) => {
|
|
7
7
|
const baseTopic = `events.${(0, event_contract_version_js_1.versionedQueueName)(queueName, version)}`;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ConsumerTopicsManifestInterface {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
readonly queues: readonly string[];
|
|
4
|
+
}
|
|
5
|
+
export interface ConsumerTopicsOptionsInterface {
|
|
6
|
+
readonly chartValuesFile: string;
|
|
7
|
+
readonly consumerSources: string;
|
|
8
|
+
readonly eventSources: string;
|
|
9
|
+
readonly manifests: readonly ConsumerTopicsManifestInterface[];
|
|
10
|
+
readonly modulesDirectory: string;
|
|
11
|
+
readonly rootDirectory: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const assertConsumerTopicsConsistent: (options: ConsumerTopicsOptionsInterface) => Promise<void>;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.assertConsumerTopicsConsistent = void 0;
|
|
7
|
+
const promises_1 = require("node:fs/promises");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const EVENT_QUEUE = /static readonly queueName\b[^\n]*'(?<queue>[^\n']+)'/u;
|
|
10
|
+
const SUBSCRIBED_QUEUE = /eventTopics\([^\n]*'(?<queue>[^\n']+)'/gu;
|
|
11
|
+
const CHART_QUEUES_BLOCK = /^ {2}eventQueues:\n(?<block>(?:(?: {4}.*)?\n)*)/mu;
|
|
12
|
+
const CHART_QUEUE = /^ {4}(?<queue>[\w\-.]+):/gmu;
|
|
13
|
+
const readSources = async (directory, pattern) => {
|
|
14
|
+
const files = await Array.fromAsync((0, promises_1.glob)(pattern, { cwd: directory }));
|
|
15
|
+
const sources = files.filter((file) => !file.endsWith('.spec.ts'));
|
|
16
|
+
return Promise.all(sources.map(async (file) => ({
|
|
17
|
+
file,
|
|
18
|
+
text: await (0, promises_1.readFile)(node_path_1.default.join(directory, file), 'utf8'),
|
|
19
|
+
})));
|
|
20
|
+
};
|
|
21
|
+
const missingFrom = (actual, expected) => {
|
|
22
|
+
const rest = [...actual];
|
|
23
|
+
return expected.filter((queue) => {
|
|
24
|
+
const index = rest.indexOf(queue);
|
|
25
|
+
if (index === -1) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
rest.splice(index, 1);
|
|
29
|
+
return false;
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
const chartViolations = (values, declared) => {
|
|
33
|
+
const block = CHART_QUEUES_BLOCK.exec(values)?.groups?.block ?? '';
|
|
34
|
+
const chartQueues = [...block.matchAll(CHART_QUEUE)].map((match) => match.groups?.queue ?? '');
|
|
35
|
+
return [
|
|
36
|
+
...missingFrom(chartQueues, declared).map((queue) => `очередь ${queue} из манифеста без ключа kafka.eventQueues: автосоздание топиков выключено, консьюмер упадёт`),
|
|
37
|
+
...missingFrom(declared, chartQueues).map((queue) => `ключ kafka.eventQueues.${queue} без очереди в манифесте: топик без производителя`),
|
|
38
|
+
];
|
|
39
|
+
};
|
|
40
|
+
const queueOf = ({ text }) => EVENT_QUEUE.exec(text)?.groups?.queue;
|
|
41
|
+
const ownerOf = ({ file }) => file.split('/', 1).join('');
|
|
42
|
+
const producedQueues = (events) => {
|
|
43
|
+
const produced = new Map();
|
|
44
|
+
for (const event of events) {
|
|
45
|
+
const queue = queueOf(event);
|
|
46
|
+
if (queue !== undefined) {
|
|
47
|
+
produced.set(ownerOf(event), [...produced.get(ownerOf(event)) ?? [], queue]);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return produced;
|
|
51
|
+
};
|
|
52
|
+
const ownerViolations = (events, manifests) => {
|
|
53
|
+
const produced = producedQueues(events);
|
|
54
|
+
const declared = new Map(manifests.map(({ name, queues }) => [name, queues]));
|
|
55
|
+
const owners = [...new Set([...produced.keys(), ...declared.keys()])];
|
|
56
|
+
return [
|
|
57
|
+
...events.filter((event) => queueOf(event) === undefined).map(({ file }) => `событие без queueName (${file})`),
|
|
58
|
+
...owners.flatMap((owner) => {
|
|
59
|
+
const ownQueues = produced.get(owner) ?? [];
|
|
60
|
+
const declaredQueues = declared.get(owner) ?? [];
|
|
61
|
+
return [
|
|
62
|
+
...missingFrom(declaredQueues, ownQueues).map((queue) => `модуль ${owner}: очередь ${queue} есть у события, но не в манифесте модуля`),
|
|
63
|
+
...missingFrom(ownQueues, declaredQueues).map((queue) => `модуль ${owner}: очередь ${queue} в манифесте без события модуля`),
|
|
64
|
+
];
|
|
65
|
+
}),
|
|
66
|
+
];
|
|
67
|
+
};
|
|
68
|
+
const subscriptionViolations = (consumers, declared) => consumers.flatMap(({ file, text }) => [...text.matchAll(SUBSCRIBED_QUEUE)]
|
|
69
|
+
.map((match) => match.groups?.queue ?? '')
|
|
70
|
+
.filter((queue) => !declared.includes(queue))
|
|
71
|
+
.map((queue) => `подписка на очередь ${queue} вне реестра: топик никто не производит (${file})`));
|
|
72
|
+
const assertConsumerTopicsConsistent = async (options) => {
|
|
73
|
+
const { chartValuesFile, consumerSources, eventSources, manifests, modulesDirectory, rootDirectory } = options;
|
|
74
|
+
const directory = node_path_1.default.join(rootDirectory, modulesDirectory);
|
|
75
|
+
const [values, events, consumers] = await Promise.all([
|
|
76
|
+
(0, promises_1.readFile)(node_path_1.default.join(rootDirectory, chartValuesFile), 'utf8'),
|
|
77
|
+
readSources(directory, eventSources),
|
|
78
|
+
readSources(directory, consumerSources),
|
|
79
|
+
]);
|
|
80
|
+
const declared = manifests.flatMap(({ queues }) => queues);
|
|
81
|
+
const emptyPatterns = [
|
|
82
|
+
...events.length === 0 ? [`шаблон событий ${eventSources} не нашёл файлов`] : [],
|
|
83
|
+
...consumers.length === 0 ? [`шаблон консьюмеров ${consumerSources} не нашёл файлов`] : [],
|
|
84
|
+
];
|
|
85
|
+
const violations = [
|
|
86
|
+
...emptyPatterns,
|
|
87
|
+
...chartViolations(values, declared),
|
|
88
|
+
...ownerViolations(events, manifests),
|
|
89
|
+
...subscriptionViolations(consumers, declared),
|
|
90
|
+
];
|
|
91
|
+
if (violations.length > 0) {
|
|
92
|
+
throw new Error(`Гейт реестра топиков Kafka:\n${violations.join('\n')}`);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
exports.assertConsumerTopicsConsistent = assertConsumerTopicsConsistent;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './assert-consumer-topics-consistent.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./assert-consumer-topics-consistent.js"), exports);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './event-contract-version.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './event-contract-version.js';
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * from './configs/index.js';
|
|
2
|
+
export * from './event-contract/index.js';
|
|
2
3
|
export * from './libs/index.js';
|
|
3
4
|
export * from './constants.js';
|
|
4
|
-
export * from './event-contract-version.js';
|
|
5
5
|
export * from './kafka-commit-interceptor.js';
|
|
6
6
|
export * from './kafka-error-handler.js';
|
|
7
7
|
export * from './kafka.module.js';
|
package/dist/esm/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export * from './configs/index.js';
|
|
2
|
+
export * from './event-contract/index.js';
|
|
2
3
|
export * from './libs/index.js';
|
|
3
4
|
export * from './constants.js';
|
|
4
|
-
export * from './event-contract-version.js';
|
|
5
5
|
export * from './kafka-commit-interceptor.js';
|
|
6
6
|
export * from './kafka-error-handler.js';
|
|
7
7
|
export * from './kafka.module.js';
|
|
@@ -13,7 +13,7 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
13
13
|
import { applyDecorators, BadRequestException, Inject, Injectable, UseInterceptors, } from '@nestjs/common';
|
|
14
14
|
import { catchError, of, tap, throwError } from 'rxjs';
|
|
15
15
|
import { KAFKA_LOGGER } from './constants.js';
|
|
16
|
-
import { eventVersionOfHeaders, eventVersionOfTopic } from './event-contract-version.js';
|
|
16
|
+
import { eventVersionOfHeaders, eventVersionOfTopic } from './event-contract/event-contract-version.js';
|
|
17
17
|
import { KafkaErrorHandler } from './kafka-error-handler.js';
|
|
18
18
|
let KafkaCommitInterceptor = class KafkaCommitInterceptor {
|
|
19
19
|
kafkaErrorHandler;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DEFAULT_EVENT_VERSION, versionedQueueName } from '../event-contract-version.js';
|
|
1
|
+
import { DEFAULT_EVENT_VERSION, versionedQueueName } from '../event-contract/event-contract-version.js';
|
|
2
2
|
const RETRY_TOPIC_SUFFIX = 'retry';
|
|
3
3
|
export const eventTopics = (queueName, version = DEFAULT_EVENT_VERSION) => {
|
|
4
4
|
const baseTopic = `events.${versionedQueueName(queueName, version)}`;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface ConsumerTopicsManifestInterface {
|
|
2
|
+
readonly name: string;
|
|
3
|
+
readonly queues: readonly string[];
|
|
4
|
+
}
|
|
5
|
+
export interface ConsumerTopicsOptionsInterface {
|
|
6
|
+
readonly chartValuesFile: string;
|
|
7
|
+
readonly consumerSources: string;
|
|
8
|
+
readonly eventSources: string;
|
|
9
|
+
readonly manifests: readonly ConsumerTopicsManifestInterface[];
|
|
10
|
+
readonly modulesDirectory: string;
|
|
11
|
+
readonly rootDirectory: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const assertConsumerTopicsConsistent: (options: ConsumerTopicsOptionsInterface) => Promise<void>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { glob, readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const EVENT_QUEUE = /static readonly queueName\b[^\n]*'(?<queue>[^\n']+)'/u;
|
|
4
|
+
const SUBSCRIBED_QUEUE = /eventTopics\([^\n]*'(?<queue>[^\n']+)'/gu;
|
|
5
|
+
const CHART_QUEUES_BLOCK = /^ {2}eventQueues:\n(?<block>(?:(?: {4}.*)?\n)*)/mu;
|
|
6
|
+
const CHART_QUEUE = /^ {4}(?<queue>[\w\-.]+):/gmu;
|
|
7
|
+
const readSources = async (directory, pattern) => {
|
|
8
|
+
const files = await Array.fromAsync(glob(pattern, { cwd: directory }));
|
|
9
|
+
const sources = files.filter((file) => !file.endsWith('.spec.ts'));
|
|
10
|
+
return Promise.all(sources.map(async (file) => ({
|
|
11
|
+
file,
|
|
12
|
+
text: await readFile(path.join(directory, file), 'utf8'),
|
|
13
|
+
})));
|
|
14
|
+
};
|
|
15
|
+
const missingFrom = (actual, expected) => {
|
|
16
|
+
const rest = [...actual];
|
|
17
|
+
return expected.filter((queue) => {
|
|
18
|
+
const index = rest.indexOf(queue);
|
|
19
|
+
if (index === -1) {
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
rest.splice(index, 1);
|
|
23
|
+
return false;
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
const chartViolations = (values, declared) => {
|
|
27
|
+
const block = CHART_QUEUES_BLOCK.exec(values)?.groups?.block ?? '';
|
|
28
|
+
const chartQueues = [...block.matchAll(CHART_QUEUE)].map((match) => match.groups?.queue ?? '');
|
|
29
|
+
return [
|
|
30
|
+
...missingFrom(chartQueues, declared).map((queue) => `очередь ${queue} из манифеста без ключа kafka.eventQueues: автосоздание топиков выключено, консьюмер упадёт`),
|
|
31
|
+
...missingFrom(declared, chartQueues).map((queue) => `ключ kafka.eventQueues.${queue} без очереди в манифесте: топик без производителя`),
|
|
32
|
+
];
|
|
33
|
+
};
|
|
34
|
+
const queueOf = ({ text }) => EVENT_QUEUE.exec(text)?.groups?.queue;
|
|
35
|
+
const ownerOf = ({ file }) => file.split('/', 1).join('');
|
|
36
|
+
const producedQueues = (events) => {
|
|
37
|
+
const produced = new Map();
|
|
38
|
+
for (const event of events) {
|
|
39
|
+
const queue = queueOf(event);
|
|
40
|
+
if (queue !== undefined) {
|
|
41
|
+
produced.set(ownerOf(event), [...produced.get(ownerOf(event)) ?? [], queue]);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return produced;
|
|
45
|
+
};
|
|
46
|
+
const ownerViolations = (events, manifests) => {
|
|
47
|
+
const produced = producedQueues(events);
|
|
48
|
+
const declared = new Map(manifests.map(({ name, queues }) => [name, queues]));
|
|
49
|
+
const owners = [...new Set([...produced.keys(), ...declared.keys()])];
|
|
50
|
+
return [
|
|
51
|
+
...events.filter((event) => queueOf(event) === undefined).map(({ file }) => `событие без queueName (${file})`),
|
|
52
|
+
...owners.flatMap((owner) => {
|
|
53
|
+
const ownQueues = produced.get(owner) ?? [];
|
|
54
|
+
const declaredQueues = declared.get(owner) ?? [];
|
|
55
|
+
return [
|
|
56
|
+
...missingFrom(declaredQueues, ownQueues).map((queue) => `модуль ${owner}: очередь ${queue} есть у события, но не в манифесте модуля`),
|
|
57
|
+
...missingFrom(ownQueues, declaredQueues).map((queue) => `модуль ${owner}: очередь ${queue} в манифесте без события модуля`),
|
|
58
|
+
];
|
|
59
|
+
}),
|
|
60
|
+
];
|
|
61
|
+
};
|
|
62
|
+
const subscriptionViolations = (consumers, declared) => consumers.flatMap(({ file, text }) => [...text.matchAll(SUBSCRIBED_QUEUE)]
|
|
63
|
+
.map((match) => match.groups?.queue ?? '')
|
|
64
|
+
.filter((queue) => !declared.includes(queue))
|
|
65
|
+
.map((queue) => `подписка на очередь ${queue} вне реестра: топик никто не производит (${file})`));
|
|
66
|
+
export const assertConsumerTopicsConsistent = async (options) => {
|
|
67
|
+
const { chartValuesFile, consumerSources, eventSources, manifests, modulesDirectory, rootDirectory } = options;
|
|
68
|
+
const directory = path.join(rootDirectory, modulesDirectory);
|
|
69
|
+
const [values, events, consumers] = await Promise.all([
|
|
70
|
+
readFile(path.join(rootDirectory, chartValuesFile), 'utf8'),
|
|
71
|
+
readSources(directory, eventSources),
|
|
72
|
+
readSources(directory, consumerSources),
|
|
73
|
+
]);
|
|
74
|
+
const declared = manifests.flatMap(({ queues }) => queues);
|
|
75
|
+
const emptyPatterns = [
|
|
76
|
+
...events.length === 0 ? [`шаблон событий ${eventSources} не нашёл файлов`] : [],
|
|
77
|
+
...consumers.length === 0 ? [`шаблон консьюмеров ${consumerSources} не нашёл файлов`] : [],
|
|
78
|
+
];
|
|
79
|
+
const violations = [
|
|
80
|
+
...emptyPatterns,
|
|
81
|
+
...chartViolations(values, declared),
|
|
82
|
+
...ownerViolations(events, manifests),
|
|
83
|
+
...subscriptionViolations(consumers, declared),
|
|
84
|
+
];
|
|
85
|
+
if (violations.length > 0) {
|
|
86
|
+
throw new Error(`Гейт реестра топиков Kafka:\n${violations.join('\n')}`);
|
|
87
|
+
}
|
|
88
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './assert-consumer-topics-consistent.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './assert-consumer-topics-consistent.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@application-kit/kafka",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Kafka-транспорт для NestJS: консьюмер-процесс с heartbeat, auto-commit, retry/DLQ и контракт версии события",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"consumer",
|
|
@@ -32,7 +32,27 @@
|
|
|
32
32
|
"default": "./dist/cjs/index.js"
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
|
-
"./
|
|
35
|
+
"./event-contract": {
|
|
36
|
+
"import": {
|
|
37
|
+
"types": "./dist/esm/event-contract/index.d.ts",
|
|
38
|
+
"default": "./dist/esm/event-contract/index.js"
|
|
39
|
+
},
|
|
40
|
+
"require": {
|
|
41
|
+
"types": "./dist/cjs/event-contract/index.d.ts",
|
|
42
|
+
"default": "./dist/cjs/event-contract/index.js"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"./package.json": "./package.json",
|
|
46
|
+
"./testing": {
|
|
47
|
+
"import": {
|
|
48
|
+
"types": "./dist/esm/testing/index.d.ts",
|
|
49
|
+
"default": "./dist/esm/testing/index.js"
|
|
50
|
+
},
|
|
51
|
+
"require": {
|
|
52
|
+
"types": "./dist/cjs/testing/index.d.ts",
|
|
53
|
+
"default": "./dist/cjs/testing/index.js"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
36
56
|
},
|
|
37
57
|
"main": "./dist/cjs/index.js",
|
|
38
58
|
"module": "./dist/esm/index.js",
|
|
@@ -41,7 +61,7 @@
|
|
|
41
61
|
"dist"
|
|
42
62
|
],
|
|
43
63
|
"scripts": {
|
|
44
|
-
"build": "rm -rf ./dist && node ../../scripts/barrels.mjs create ./src && tsc --project tsconfig.build.json && tsc --project tsconfig.build.cjs.json && node ../../scripts/barrels.mjs clean ./src && node ../../scripts/mark-cjs.mjs ./dist/cjs",
|
|
64
|
+
"build": "rm -rf ./dist && node ../../scripts/barrels.mjs create ./src --entry-only=testing && tsc --project tsconfig.build.json && tsc --project tsconfig.build.cjs.json && node ../../scripts/barrels.mjs clean ./src && node ../../scripts/mark-cjs.mjs ./dist/cjs",
|
|
45
65
|
"lint:tsc": "tsc --noEmit --pretty --project .",
|
|
46
66
|
"test:mutation": "stryker run",
|
|
47
67
|
"test:unit": "vitest run"
|
|
@@ -76,6 +96,14 @@
|
|
|
76
96
|
"nestjs-pino": "^4.6.1",
|
|
77
97
|
"rxjs": "^7.8.0"
|
|
78
98
|
},
|
|
99
|
+
"peerDependenciesMeta": {
|
|
100
|
+
"@nestjs/microservices": {
|
|
101
|
+
"optional": true
|
|
102
|
+
},
|
|
103
|
+
"kafkajs": {
|
|
104
|
+
"optional": true
|
|
105
|
+
}
|
|
106
|
+
},
|
|
79
107
|
"engines": {
|
|
80
108
|
"node": ">=24"
|
|
81
109
|
},
|
/package/dist/cjs/{event-contract-version.d.ts → event-contract/event-contract-version.d.ts}
RENAMED
|
File without changes
|
|
File without changes
|
/package/dist/esm/{event-contract-version.d.ts → event-contract/event-contract-version.d.ts}
RENAMED
|
File without changes
|
|
File without changes
|