Skip to content

Commit 4419cf4

Browse files
committed
aimsora-0010 // processing-worker // Инициализирован worker-сервис: структура TypeScript, конфиги и контейнеризация
0 parents  commit 4419cf4

15 files changed

Lines changed: 393 additions & 0 deletions

File tree

.dockerignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
node_modules
2+
dist
3+
.git
4+
.github

.env.example

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
NODE_ENV=development
2+
RABBITMQ_URL=amqp://app:app@localhost:5672
3+
REDIS_URL=redis://localhost:6379
4+
API_GRAPHQL_URL=http://localhost:3000/graphql
5+
6+
QUEUE_RAW_EVENT=source.raw.v1
7+
QUEUE_NORMALIZED_EVENT=source.normalized.v1
8+
SHARED_CONTRACTS_DIR=../shared-contracts

.github/workflows/ci.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: ["main", "master"]
6+
pull_request:
7+
8+
jobs:
9+
build:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- name: Checkout
13+
uses: actions/checkout@v4
14+
15+
- name: Setup Node.js
16+
uses: actions/setup-node@v4
17+
with:
18+
node-version: 20
19+
cache: npm
20+
21+
- name: Install dependencies
22+
run: npm install
23+
24+
- name: Type check
25+
run: npm run check
26+
27+
- name: Build
28+
run: npm run build

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules/
2+
dist/
3+
coverage/
4+
.env
5+
.DS_Store

Dockerfile

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
FROM node:20-alpine AS deps
2+
WORKDIR /app
3+
COPY package.json package-lock.json* ./
4+
RUN npm install
5+
6+
FROM deps AS build
7+
WORKDIR /app
8+
COPY tsconfig.json ./
9+
COPY src ./src
10+
RUN npm run build
11+
12+
FROM node:20-alpine AS runtime
13+
WORKDIR /app
14+
ENV NODE_ENV=production
15+
COPY --from=deps /app/node_modules ./node_modules
16+
COPY --from=build /app/dist ./dist
17+
CMD ["node", "dist/main.js"]

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# processing-worker
2+
3+
![CI](https://img.shields.io/badge/CI-GitHub_Actions-2088FF?logo=githubactions&logoColor=white)
4+
![CD](https://img.shields.io/badge/CD-GitHub_Deploy-2ea44f?logo=github&logoColor=white)
5+
![Container](https://img.shields.io/badge/Container-GHCR-2496ED?logo=docker&logoColor=white)
6+
7+
Сервис асинхронной обработки и нормализации собранных данных.
8+
9+
## Что делает этот репозиторий
10+
11+
- читает `source.raw.v1` из RabbitMQ;
12+
- валидирует raw-события;
13+
- нормализует payload до формата `source.normalized.v1`;
14+
- публикует нормализованные события и отправляет ingest mutation в `backend-api`.
15+
16+
## Черновая реализация
17+
18+
- consumer RabbitMQ (`src/messaging/queue-client.ts`);
19+
- нормализатор (`src/normalize.ts`);
20+
- валидация raw/normalized схем через Ajv;
21+
- GraphQL-клиент отправки в backend (`src/backend-client.ts`);
22+
- Dockerfile и CI workflow.
23+
24+
## Локальный запуск
25+
26+
```bash
27+
cp .env.example .env
28+
npm install
29+
npm run start:dev
30+
```
31+
32+
## Важные переменные
33+
34+
- `RABBITMQ_URL`
35+
- `QUEUE_RAW_EVENT`
36+
- `QUEUE_NORMALIZED_EVENT`
37+
- `API_GRAPHQL_URL`
38+
- `SHARED_CONTRACTS_DIR`
39+
40+
## Связи с другими репозиториями
41+
42+
- получает события от `scraper-service`;
43+
- использует схемы из `shared-contracts`;
44+
- отправляет результаты в `backend-api`.

package.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "processing-worker",
3+
"version": "0.1.0",
4+
"private": true,
5+
"description": "Черновой worker для нормализации и передачи данных в backend-api.",
6+
"scripts": {
7+
"build": "tsc -p tsconfig.json",
8+
"start": "node dist/main.js",
9+
"start:dev": "ts-node-dev --respawn --transpile-only src/main.ts",
10+
"check": "tsc --noEmit"
11+
},
12+
"dependencies": {
13+
"ajv": "^8.17.1",
14+
"amqplib": "^0.10.5",
15+
"dotenv": "^16.4.7"
16+
},
17+
"devDependencies": {
18+
"@types/amqplib": "^0.10.7",
19+
"@types/node": "^22.10.5",
20+
"ts-node-dev": "^2.0.0",
21+
"typescript": "^5.7.2"
22+
},
23+
"engines": {
24+
"node": ">=20"
25+
}
26+
}

src/backend-client.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { NormalizedSourceEvent } from "./types";
2+
3+
const INGEST_MUTATION = `
4+
mutation IngestNormalizedItem($input: IngestNormalizedItemInput!) {
5+
ingestNormalizedItem(input: $input) {
6+
accepted
7+
idempotencyKey
8+
}
9+
}
10+
`;
11+
12+
export async function sendToBackend(
13+
graphqlUrl: string,
14+
event: NormalizedSourceEvent
15+
): Promise<void> {
16+
const response = await fetch(graphqlUrl, {
17+
method: "POST",
18+
headers: {
19+
"content-type": "application/json"
20+
},
21+
body: JSON.stringify({
22+
query: INGEST_MUTATION,
23+
variables: {
24+
input: {
25+
externalId: event.externalId,
26+
source: event.source,
27+
title: event.title,
28+
customer: event.customer,
29+
supplier: event.supplier,
30+
amount: event.amount,
31+
currency: event.currency,
32+
publishedAt: event.publishedAt,
33+
payloadVersion: event.payloadVersion,
34+
rawPayload: { rawRef: event.rawRef, eventId: event.eventId }
35+
}
36+
}
37+
})
38+
});
39+
40+
if (!response.ok) {
41+
throw new Error(`Backend API ответил с кодом ${response.status}`);
42+
}
43+
44+
const payload = (await response.json()) as {
45+
errors?: Array<{ message: string }>;
46+
};
47+
48+
if (payload.errors?.length) {
49+
throw new Error(`Ошибка GraphQL ingest: ${payload.errors.map((e) => e.message).join("; ")}`);
50+
}
51+
}

src/config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import "dotenv/config";
2+
3+
export const config = {
4+
rabbitmqUrl: process.env.RABBITMQ_URL ?? "amqp://app:app@localhost:5672",
5+
apiGraphqlUrl: process.env.API_GRAPHQL_URL ?? "http://localhost:3000/graphql",
6+
queueRaw: process.env.QUEUE_RAW_EVENT ?? "source.raw.v1",
7+
queueNormalized: process.env.QUEUE_NORMALIZED_EVENT ?? "source.normalized.v1",
8+
sharedContractsDir: process.env.SHARED_CONTRACTS_DIR ?? "../shared-contracts"
9+
};

src/contracts/schema-validator.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { readFileSync } from "node:fs";
2+
import { join } from "node:path";
3+
import Ajv from "ajv";
4+
import type { NormalizedSourceEvent, RawSourceEvent } from "../types";
5+
6+
export function createSchemaValidators(sharedContractsDir: string) {
7+
const rawSchema = JSON.parse(
8+
readFileSync(join(sharedContractsDir, "events", "source-raw.v1.json"), "utf-8")
9+
);
10+
const normalizedSchema = JSON.parse(
11+
readFileSync(join(sharedContractsDir, "events", "source-normalized.v1.json"), "utf-8")
12+
);
13+
14+
const ajv = new Ajv({ allErrors: true });
15+
const rawValidate = ajv.compile<RawSourceEvent>(rawSchema);
16+
const normalizedValidate = ajv.compile<NormalizedSourceEvent>(normalizedSchema);
17+
18+
return {
19+
validateRaw(event: RawSourceEvent): void {
20+
if (rawValidate(event)) {
21+
return;
22+
}
23+
throw new Error(
24+
`Raw schema validation failed: ${(rawValidate.errors ?? [])
25+
.map((err) => `${err.instancePath || "/"} ${err.message}`)
26+
.join("; ")}`
27+
);
28+
},
29+
validateNormalized(event: NormalizedSourceEvent): void {
30+
if (normalizedValidate(event)) {
31+
return;
32+
}
33+
throw new Error(
34+
`Normalized schema validation failed: ${(normalizedValidate.errors ?? [])
35+
.map((err) => `${err.instancePath || "/"} ${err.message}`)
36+
.join("; ")}`
37+
);
38+
}
39+
};
40+
}

0 commit comments

Comments
 (0)