Compare commits

1 Commits
review ... main

Author SHA1 Message Date
Первов Артем
51697997ab add translation for tags 2026-06-17 11:22:05 +03:00
15 changed files with 315 additions and 134 deletions

View File

@@ -0,0 +1,145 @@
CREATE TABLE IF NOT EXISTS tag_translation (
edge_id text NOT NULL,
tag_id text NOT NULL,
locale text NOT NULL DEFAULT 'ru',
display_name text NOT NULL,
source text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (edge_id, tag_id, locale)
);
CREATE INDEX IF NOT EXISTS tag_translation_edge_locale_idx
ON tag_translation (edge_id, locale, tag_id);
INSERT INTO tag_translation (edge_id, tag_id, locale, display_name, source)
VALUES
('edge5', 'hook_weight_1', 'ru', 'Вес на крюке 1', 'edge5-modbus_rus.json'),
('edge5', 'hookblock_position', 'ru', 'Положение крюкоблока', 'edge5-modbus_rus.json'),
('edge5', 'manifold_pressure_1', 'ru', 'Давление в манифольде 1', 'edge5-modbus_rus.json'),
('edge5', 'hydraulic_tong_torque', 'ru', 'Момент на гидроключе', 'edge5-modbus_rus.json'),
('edge5', 'hook_weight_2', 'ru', 'Вес на крюке 2', 'edge5-modbus_rus.json'),
('edge5', 'outlet_flow', 'ru', 'Расход на выходе', 'edge5-modbus_rus.json'),
('edge5', 'gas_1_lfl', 'ru', 'Загазованность 1 (НКПР)', 'edge5-modbus_rus.json'),
('edge5', 'gas_1_h2s', 'ru', 'Загазованность 1 (H2S)', 'edge5-modbus_rus.json'),
('edge5', 'gas_2_h2s', 'ru', 'Загазованность 2 (H2S)', 'edge5-modbus_rus.json'),
('edge5', 'gas_3_h2s', 'ru', 'Загазованность 3 (H2S)', 'edge5-modbus_rus.json'),
('edge5', 'outlet_temperature', 'ru', 'Температура на выходе', 'edge5-modbus_rus.json'),
('edge5', 'inlet_flow_1', 'ru', 'Расход на входе 1', 'edge5-modbus_rus.json'),
('edge5', 'akb_tong_torque', 'ru', 'Момент на ключе АКБ', 'edge5-modbus_rus.json'),
('edge5', 'rotary_torque', 'ru', 'Момент на роторе', 'edge5-modbus_rus.json'),
('edge5', 'rotary_rpm', 'ru', 'Обороты ротора', 'edge5-modbus_rus.json'),
('edge5', 'manifold_pressure_2', 'ru', 'Давление в манифольде 2', 'edge5-modbus_rus.json'),
('edge5', 'machine_tong_torque', 'ru', 'Момент на машинном ключе', 'edge5-modbus_rus.json'),
('edge5', 'inlet_temperature', 'ru', 'Температура на входе', 'edge5-modbus_rus.json'),
('edge5', 'gas_4_h2s', 'ru', 'Загазованность 4 (H2S)', 'edge5-modbus_rus.json'),
('edge5', 'density_1', 'ru', 'Плотность 1', 'edge5-modbus_rus.json'),
('edge5', 'density_2', 'ru', 'Плотность 2', 'edge5-modbus_rus.json'),
('edge5', 'density_3', 'ru', 'Плотность 3', 'edge5-modbus_rus.json'),
('edge5', 'density_4', 'ru', 'Плотность 4', 'edge5-modbus_rus.json'),
('edge5', 'density_5', 'ru', 'Плотность 5', 'edge5-modbus_rus.json'),
('edge5', 'density_6', 'ru', 'Плотность 6', 'edge5-modbus_rus.json'),
('edge5', 'level_1', 'ru', 'Уровень 1', 'edge5-modbus_rus.json'),
('edge5', 'level_2', 'ru', 'Уровень 2', 'edge5-modbus_rus.json'),
('edge5', 'level_3', 'ru', 'Уровень 3', 'edge5-modbus_rus.json'),
('edge5', 'level_4', 'ru', 'Уровень 4', 'edge5-modbus_rus.json'),
('edge5', 'level_5', 'ru', 'Уровень 5', 'edge5-modbus_rus.json'),
('edge5', 'level_6', 'ru', 'Уровень 6', 'edge5-modbus_rus.json'),
('edge5', 'volume_1', 'ru', 'Объем 1', 'edge5-modbus_rus.json'),
('edge5', 'volume_2', 'ru', 'Объем 2', 'edge5-modbus_rus.json'),
('edge5', 'volume_3', 'ru', 'Объем 3', 'edge5-modbus_rus.json'),
('edge5', 'volume_4', 'ru', 'Объем 4', 'edge5-modbus_rus.json'),
('edge5', 'volume_5', 'ru', 'Объем 5', 'edge5-modbus_rus.json'),
('edge5', 'volume_6', 'ru', 'Объем 6', 'edge5-modbus_rus.json'),
('edge5', 'total_volume', 'ru', 'Объем суммарный', 'edge5-modbus_rus.json'),
('edge5', 'weight_on_bit', 'ru', 'Нагрузка на долото', 'edge5-modbus_rus.json'),
('edge5', 'penetration_rate', 'ru', 'Скорость проходки', 'edge5-modbus_rus.json'),
('edge5', 'tripping_speed', 'ru', 'Скорость СПО', 'edge5-modbus_rus.json'),
('edge5', 'tool_depth', 'ru', 'Глубина инструмента', 'edge5-modbus_rus.json'),
('edge5', 'bottom_depth', 'ru', 'Глубина забоя', 'edge5-modbus_rus.json'),
('edge5', 'outlet_flow_change', 'ru', 'Изм. расхода на выходе', 'edge5-modbus_rus.json'),
('edge5', 'stand_feed', 'ru', 'Подача свечи', 'edge5-modbus_rus.json'),
('edge5', 'lowered_stands_count', 'ru', 'Количество опущенных свеч', 'edge5-modbus_rus.json'),
('edge5', 'inlet_flow_2', 'ru', 'Расход на входе 2', 'edge5-modbus_rus.json'),
('edge5', 'slips_position', 'ru', 'Положение клиньев', 'edge5-modbus_rus.json'),
('edge5', 'gas_2_lfl', 'ru', 'Загазованность 2 (НКПР)', 'edge5-modbus_rus.json'),
('edge5', 'level_7', 'ru', 'Уровень 7', 'edge5-modbus_rus.json'),
('edge5', 'volume_7', 'ru', 'Объем 7', 'edge5-modbus_rus.json'),
('edge5', 'top_drive_rpm', 'ru', 'Обороты СВП', 'edge5-modbus_rus.json'),
('edge5', 'top_drive_torque', 'ru', 'Крутящий момент СВП', 'edge5-modbus_rus.json'),
('edge5', 'level_8', 'ru', 'Уровень 8', 'edge5-modbus_rus.json'),
('edge5', 'volume_8', 'ru', 'Объем 8', 'edge5-modbus_rus.json'),
('edge5', 'pump_1_strokes', 'ru', 'Ходы насоса 1', 'edge5-modbus_rus.json'),
('edge5', 'pump_2_strokes', 'ru', 'Ходы насоса 2', 'edge5-modbus_rus.json'),
('edge5', 'pump_strokes_total', 'ru', 'Сумма ходов насосов', 'edge5-modbus_rus.json'),
('edge5', 'gas_3_lfl', 'ru', 'Загазованность 3 (НКПР)', 'edge5-modbus_rus.json'),
('edge5', 'pump_3_strokes', 'ru', 'Ходы насоса 3', 'edge5-modbus_rus.json'),
('edge5', 'level_9', 'ru', 'Уровень 9', 'edge5-modbus_rus.json'),
('edge5', 'volume_9', 'ru', 'Объем 9', 'edge5-modbus_rus.json'),
('edge5', 'level_10', 'ru', 'Уровень 10', 'edge5-modbus_rus.json'),
('edge5', 'volume_10', 'ru', 'Объем 10', 'edge5-modbus_rus.json'),
('edge5', 'level_11', 'ru', 'Уровень 11', 'edge5-modbus_rus.json'),
('edge5', 'volume_11', 'ru', 'Объем 11', 'edge5-modbus_rus.json'),
('edge5', 'level_12', 'ru', 'Уровень 12', 'edge5-modbus_rus.json'),
('edge5', 'volume_12', 'ru', 'Объем 12', 'edge5-modbus_rus.json'),
('edge5', 'level_13', 'ru', 'Уровень 13', 'edge5-modbus_rus.json'),
('edge5', 'volume_13', 'ru', 'Объем 13', 'edge5-modbus_rus.json'),
('edge5', 'level_14', 'ru', 'Уровень 14', 'edge5-modbus_rus.json'),
('edge5', 'volume_14', 'ru', 'Объем 14', 'edge5-modbus_rus.json'),
('edge5', 'total_volume_change', 'ru', 'Изменение суммарного объема (±)', 'edge5-modbus_rus.json'),
('edge5', 'casing_tong_1_torque', 'ru', 'Момент на ключе 1 обсадных труб', 'edge5-modbus_rus.json'),
('edge5', 'casing_tong_2_torque', 'ru', 'Момент на ключе 2 обсадных труб', 'edge5-modbus_rus.json'),
('edge5', 'drill_pipe_tong_hydraulic_pressure', 'ru', 'Давление в гидросистеме ключа для бурильных труб', 'edge5-modbus_rus.json'),
('edge5', 'drill_pipe_tong_high_low_speed', 'ru', 'Высокая/низкая скорость ключа бурильных труб', 'edge5-modbus_rus.json'),
('edge5', 'hydraulic_distributor_section_1', 'ru', 'Секция 1 гидрораспределителя', 'edge5-modbus_rus.json'),
('edge5', 'hydraulic_distributor_section_2', 'ru', 'Секция 2 гидрораспределителя', 'edge5-modbus_rus.json'),
('edge5', 'tong_type', 'ru', 'Тип ключа', 'edge5-modbus_rus.json'),
('edge5', 'gas_4_lfl', 'ru', 'Загазованность 4 (НКПР)', 'edge5-modbus_rus.json'),
('edge5', 'gas_5_lfl', 'ru', 'Загазованность 5 (НКПР)', 'edge5-modbus_rus.json'),
('edge5', 'gas_6_lfl', 'ru', 'Загазованность 6 (НКПР)', 'edge5-modbus_rus.json'),
('edge5', 'gas_7_lfl', 'ru', 'Загазованность 7 (НКПР)', 'edge5-modbus_rus.json'),
('edge5', 'gas_8_lfl', 'ru', 'Загазованность 8 (НКПР)', 'edge5-modbus_rus.json'),
('edge5', 'guyline_tension_1', 'ru', 'Усилие в растяжке 1', 'edge5-modbus_rus.json'),
('edge5', 'guyline_tension_2', 'ru', 'Усилие в растяжке 2', 'edge5-modbus_rus.json'),
('edge5', 'guyline_tension_3', 'ru', 'Усилие в растяжке 3', 'edge5-modbus_rus.json'),
('edge5', 'guyline_tension_4', 'ru', 'Усилие в растяжке 4', 'edge5-modbus_rus.json'),
('edge5', 'gas_9_lfl_ch4', 'ru', 'Загазованность 9 (НКПР) CH4', 'edge5-modbus_rus.json'),
('edge5', 'gas_10_lfl_ch4', 'ru', 'Загазованность 10 (НКПР) CH4', 'edge5-modbus_rus.json'),
('edge5', 'gas_1_lfl_c3h8', 'ru', 'Загазованность 1 (НКПР) C3H8', 'edge5-modbus_rus.json'),
('edge5', 'gas_2_lfl_c3h8', 'ru', 'Загазованность 2 (НКПР) C3H8', 'edge5-modbus_rus.json'),
('edge5', 'gas_3_lfl_c3h8', 'ru', 'Загазованность 3 (НКПР) C3H8', 'edge5-modbus_rus.json'),
('edge5', 'gas_4_lfl_c3h8', 'ru', 'Загазованность 4 (НКПР) C3H8', 'edge5-modbus_rus.json'),
('edge5', 'gas_5_lfl_c3h8', 'ru', 'Загазованность 5 (НКПР) C3H8', 'edge5-modbus_rus.json'),
('edge5', 'gas_6_lfl_c3h8', 'ru', 'Загазованность 6 (НКПР) C3H8', 'edge5-modbus_rus.json'),
('edge5', 'gas_7_lfl_c3h8', 'ru', 'Загазованность 7 (НКПР) C3H8', 'edge5-modbus_rus.json'),
('edge5', 'gas_8_lfl_c3h8', 'ru', 'Загазованность 8 (НКПР) C3H8', 'edge5-modbus_rus.json'),
('edge5', 'gas_9_lfl_c3h8', 'ru', 'Загазованность 9 (НКПР) C3H8', 'edge5-modbus_rus.json'),
('edge5', 'gas_10_lfl_c3h8', 'ru', 'Загазованность 10 (НКПР) C3H8', 'edge5-modbus_rus.json'),
('edge5', 'rotary_torque_2', 'ru', 'Момент на роторе 2', 'edge5-modbus_rus.json'),
('edge5', 'density_7', 'ru', 'Плотность 7', 'edge5-modbus_rus.json'),
('edge5', 'density_8', 'ru', 'Плотность 8', 'edge5-modbus_rus.json'),
('edge5', 'density_9', 'ru', 'Плотность 9', 'edge5-modbus_rus.json'),
('edge5', 'density_10', 'ru', 'Плотность 10', 'edge5-modbus_rus.json'),
('edge5', 'bit_position_above_bottom', 'ru', 'Положение долота над забоем', 'edge5-modbus_rus.json'),
('edge5', 'disperser_pump_pressure', 'ru', 'Давление насоса диспергации', 'edge5-modbus_rus.json'),
('edge5', 'disperser_pump_strokes', 'ru', 'Число ходов насоса диспергации', 'edge5-modbus_rus.json'),
('edge5', 'disperser_pump_outlet_flow', 'ru', 'Расход на выходе насоса диспергации', 'edge5-modbus_rus.json'),
('edge5', 'pump_1_outlet_flow', 'ru', 'Расход на выходе насоса 1', 'edge5-modbus_rus.json'),
('edge5', 'pump_2_outlet_flow', 'ru', 'Расход на выходе насоса 2', 'edge5-modbus_rus.json'),
('edge5', 'drilling_line_runtime', 'ru', 'Наработка талевого каната (в реальном времени)', 'edge5-modbus_rus.json'),
('edge5', 'tripping_fill_volume_total', 'ru', 'Суммарный объем долива скважины при СПО', 'edge5-modbus_rus.json'),
('edge5', 'pump_1_outlet_pressure', 'ru', 'Давление на выходе насоса 1', 'edge5-modbus_rus.json'),
('edge5', 'pump_2_outlet_pressure', 'ru', 'Давление на выходе насоса 2', 'edge5-modbus_rus.json'),
('edge5', 'drilling_time_per_meter', 'ru', 'Время бурения 1м проходки', 'edge5-modbus_rus.json'),
('edge5', 'gas_11_lfl_ch4', 'ru', 'Загазованность 11 (НКПР) CH4', 'edge5-modbus_rus.json'),
('edge5', 'gas_12_lfl_ch4', 'ru', 'Загазованность 12 (НКПР) CH4', 'edge5-modbus_rus.json'),
('edge5', 'gas_13_lfl_ch4', 'ru', 'Загазованность 13 (НКПР) CH4', 'edge5-modbus_rus.json'),
('edge5', 'pump_3_outlet_flow', 'ru', 'Расход на выходе насоса 3', 'edge5-modbus_rus.json'),
('edge5', 'pump_3_outlet_pressure', 'ru', 'Давление на выходе насоса 3', 'edge5-modbus_rus.json'),
('edge5', 'gas_5_h2s', 'ru', 'Загазованность 5 (H2S)', 'edge5-modbus_rus.json'),
('edge5', 'gas_6_h2s', 'ru', 'Загазованность 6 (H2S)', 'edge5-modbus_rus.json'),
('edge5', 'gas_7_h2s', 'ru', 'Загазованность 7 (H2S)', 'edge5-modbus_rus.json')
ON CONFLICT (edge_id, tag_id, locale) DO UPDATE SET
display_name = EXCLUDED.display_name,
source = EXCLUDED.source,
updated_at = now();

73
package-lock.json generated
View File

@@ -16,7 +16,6 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"compression": "^1.8.1",
"joi": "^18.2.1",
"pg": "^8.13.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
@@ -1036,54 +1035,6 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@hapi/address": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz",
"integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^11.0.2"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@hapi/formula": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz",
"integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==",
"license": "BSD-3-Clause"
},
"node_modules/@hapi/hoek": {
"version": "11.0.7",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz",
"integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==",
"license": "BSD-3-Clause"
},
"node_modules/@hapi/pinpoint": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz",
"integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==",
"license": "BSD-3-Clause"
},
"node_modules/@hapi/tlds": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.7.tgz",
"integrity": "sha512-MgNjRwy9Ti92yVAixLmDc8dd1bJIKwO9qlWCfFQRwRmUEDPQHYn4G6hwPFvFGUTzAa0FsS+inMjLin7GnyBRhA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/@hapi/topo": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz",
"integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/hoek": "^11.0.2"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
@@ -2614,12 +2565,6 @@
"@sinonjs/commons": "^3.0.1"
}
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT"
},
"node_modules/@tokenizer/inflate": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz",
@@ -7226,24 +7171,6 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
"node_modules/joi": {
"version": "18.2.1",
"resolved": "https://registry.npmjs.org/joi/-/joi-18.2.1.tgz",
"integrity": "sha512-2/OKlogiESf2Nh3TFCrRjrr9z1DRHeW0I+KReF67+4J0Ns+8hBtHRmoWAZ2OFU6I5+TWLEe6sVlSdXPjHm5UbQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@hapi/address": "^5.1.1",
"@hapi/formula": "^3.0.2",
"@hapi/hoek": "^11.0.7",
"@hapi/pinpoint": "^2.0.1",
"@hapi/tlds": "^1.1.1",
"@hapi/topo": "^6.0.2",
"@standard-schema/spec": "^1.1.0"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",

View File

@@ -21,28 +21,27 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.14.2",
"compression": "^1.8.1",
"joi": "^18.2.1",
"pg": "^8.13.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@eslint/js": "^9.18.0",
"@nestjs/cli": "^11.0.0",
"@types/compression": "^1.8.1",
"@types/express": "^5.0.0",
"@types/jest": "^30.0.0",
"@types/node": "^22.10.7",
"@types/pg": "^8.11.10",
"eslint": "^9.18.0",
"globals": "^16.0.0",
"jest": "^30.0.0",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.2",
"ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.7.3",
"typescript-eslint": "^8.20.0"
"typescript-eslint": "^8.20.0",
"@eslint/js": "^9.18.0",
"eslint": "^9.18.0",
"globals": "^16.0.0"
},
"jest": {
"moduleFileExtensions": [

View File

@@ -1,24 +1,22 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { envValidationSchema } from './common/env.validation';
import { CurrentModule } from './current/current.module';
import { DbModule } from './db/db.module';
import { EdgeModule } from './edge/edge.module';
import { HealthController } from './health.controller';
import { HistoryModule } from './history/history.module';
import { IngestModule } from './ingest/ingest.module';
import { TagTranslationModule } from './tag-translation/tag-translation.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
validationSchema: envValidationSchema,
}),
ConfigModule.forRoot({ isGlobal: true }),
DbModule,
IngestModule,
EdgeModule,
CurrentModule,
HistoryModule,
TagTranslationModule,
],
controllers: [HealthController],
})

View File

@@ -0,0 +1,25 @@
import { ConfigService } from '@nestjs/config';
// Читает числовой env-параметр с безопасным fallback, чтобы конфиг не ломал старт сервиса.
export function getNumberConfig(
config: ConfigService,
key: string,
fallback: number,
): number {
const value = config.get<string>(key);
if (value === undefined || value === '') {
return fallback;
}
const parsed = Number(value);
return Number.isFinite(parsed) ? parsed : fallback;
}
// Используется для лимитов и размеров пулов, где дробные значения не имеют смысла.
export function getIntegerConfig(
config: ConfigService,
key: string,
fallback: number,
): number {
return Math.floor(getNumberConfig(config, key, fallback));
}

View File

@@ -1,30 +0,0 @@
import * as Joi from 'joi';
export interface EnvironmentVariables {
PORT: number;
DATABASE_URL: string;
CORS_ALLOWED_ORIGINS: string;
INGEST_API_KEY: string;
PG_POOL_MAX: number;
INGEST_MAX_BATCH_SIZE: number;
HISTORY_TARGET_POINTS: number;
HISTORY_MAX_TARGET_POINTS: number;
}
export const envValidationSchema = Joi.object({
PORT: Joi.number().port().default(3100),
DATABASE_URL: Joi.string().required(),
CORS_ALLOWED_ORIGINS: Joi.string().allow('').default(''),
INGEST_API_KEY: Joi.string().allow('').default(''),
PG_POOL_MAX: Joi.number().integer().min(1).default(20),
INGEST_MAX_BATCH_SIZE: Joi.number().integer().min(1).default(10_000),
HISTORY_TARGET_POINTS: Joi.number().integer().min(100).default(2000),
HISTORY_MAX_TARGET_POINTS: Joi.number().integer().min(100).default(2000),
});

View File

@@ -1,14 +1,13 @@
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import type { Request } from 'express';
import { EnvironmentVariables } from './env.validation';
@Injectable()
export class IngestApiKeyGuard implements CanActivate {
constructor(private readonly config: ConfigService<EnvironmentVariables, true>) {}
constructor(private readonly config: ConfigService) {}
canActivate(context: ExecutionContext): boolean {
const expectedKey = this.config.get('INGEST_API_KEY');
const expectedKey = this.config.get<string>('INGEST_API_KEY');
if (!expectedKey) {
return true;
}

View File

@@ -1,7 +1,8 @@
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Pool, PoolClient, QueryResult, QueryResultRow } from 'pg';
import { EnvironmentVariables } from '../common/env.validation';
import { getIntegerConfig } from '../common/config-number';
type DatabaseHealth = {
now: Date;
timescaledb_installed: boolean;
@@ -12,13 +13,18 @@ type DatabaseHealth = {
export class DbService implements OnModuleInit, OnModuleDestroy {
private pool!: Pool;
constructor(private readonly config: ConfigService<EnvironmentVariables, true>) {}
constructor(private readonly config: ConfigService) {}
// Инициализируем пул один раз на модуль и сразу проверяем доступность БД.
async onModuleInit(): Promise<void> {
const connectionString = this.config.get<string>('DATABASE_URL');
if (!connectionString) {
throw new Error('DATABASE_URL is required.');
}
this.pool = new Pool({
connectionString: this.config.get('DATABASE_URL'),
max: this.config.get('PG_POOL_MAX'),
connectionString,
max: getIntegerConfig(this.config, 'PG_POOL_MAX', 20),
idleTimeoutMillis: 30_000,
connectionTimeoutMillis: 5_000,
statement_timeout: 60_000,

View File

@@ -1,6 +1,6 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { EnvironmentVariables } from '../common/env.validation';
import { getIntegerConfig } from '../common/config-number';
import { normalizeRequiredText } from '../common/normalize-text';
import { GetHistoryDto } from './dto/get-history.dto';
import { chooseHistorySource } from './history-source';
@@ -12,7 +12,7 @@ import { HistoryResponse } from './history.types';
export class HistoryService {
constructor(
private readonly repository: HistoryRepository,
private readonly config: ConfigService<EnvironmentVariables, true>,
private readonly config: ConfigService,
) {}
// Use-case для графиков: выбирает latest/raw/aggregate и возвращает единый API-формат.
@@ -84,8 +84,8 @@ export class HistoryService {
// Ограничивает плотность ответа, чтобы API не возвращал больше точек, чем способен отрисовать UI.
private getTargetPoints(requested?: number): number {
const defaultValue = this.config.get('HISTORY_TARGET_POINTS');
const maxValue = this.config.get('HISTORY_MAX_TARGET_POINTS');
const defaultValue = getIntegerConfig(this.config, 'HISTORY_TARGET_POINTS', 2000);
const maxValue = getIntegerConfig(this.config, 'HISTORY_MAX_TARGET_POINTS', 2000);
const value = requested ?? defaultValue;
return Math.min(Math.max(Math.floor(value), 100), maxValue);
}

View File

@@ -1,11 +1,13 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { EnvironmentVariables } from '../common/env.validation';
import { getIntegerConfig } from '../common/config-number';
import { normalizeRequiredText } from '../common/normalize-text';
import { CurrentEventsService } from '../current/current-events.service';
import { DbService } from '../db/db.service';
import { IngestPointDto } from './dto/ingest-point.dto';
const DEFAULT_MAX_BATCH_SIZE = 10_000;
type NormalizedPoint = {
time: Date;
edge: string;
@@ -31,7 +33,7 @@ type CurrentValueRow = {
export class IngestService {
constructor(
private readonly db: DbService,
private readonly config: ConfigService<EnvironmentVariables, true>,
private readonly config: ConfigService,
private readonly currentEvents: CurrentEventsService,
) {}
@@ -183,7 +185,10 @@ export class IngestService {
// Ограничивает размер одного запроса, чтобы один producer не занял память и соединение надолго.
private assertBatchSize(size: number): void {
const maxBatchSize = this.config.get('INGEST_MAX_BATCH_SIZE');
const maxBatchSize = Math.max(
getIntegerConfig(this.config, 'INGEST_MAX_BATCH_SIZE', DEFAULT_MAX_BATCH_SIZE),
1,
);
if (size > maxBatchSize) {
throw new BadRequestException(`Batch size must not exceed ${maxBatchSize} points.`);

View File

@@ -1,18 +1,14 @@
import { ValidationPipe } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import compression from 'compression';
import { AppModule } from './app.module';
import { EnvironmentVariables } from './common/env.validation';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.use(compression());
const config = app.get(ConfigService<EnvironmentVariables, true>);
const corsOrigins = config.get('CORS_ALLOWED_ORIGINS');
const corsAllowedOrigins = corsOrigins
? corsOrigins.split(',').map((origin: string) => origin.trim()).filter(Boolean)
const corsAllowedOrigins = process.env.CORS_ALLOWED_ORIGINS
? process.env.CORS_ALLOWED_ORIGINS.split(',').map((origin) => origin.trim()).filter(Boolean)
: true;
// CORS настраивается через env, чтобы один build работал локально и в окружениях деплоя.
@@ -32,7 +28,7 @@ async function bootstrap() {
}),
);
await app.listen(config.get('PORT'));
await app.listen(process.env.PORT ?? 3100);
}
void bootstrap();

View File

@@ -0,0 +1,19 @@
import { Transform } from 'class-transformer';
import { IsArray, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { parseCommaSeparatedList } from '../../common/query-list';
export class GetTagTranslationsDto {
@IsNotEmpty()
@IsString()
edge!: string;
@IsOptional()
@IsString()
locale?: string;
@IsOptional()
@Transform(({ value }) => parseCommaSeparatedList(value))
@IsArray()
@IsString({ each: true })
tags?: string[];
}

View File

@@ -0,0 +1,15 @@
import { Controller, Get, Header, Query } from '@nestjs/common';
import { GetTagTranslationsDto } from './dto/get-tag-translations.dto';
import { TagTranslationService } from './tag-translation.service';
@Controller('tag-translations')
export class TagTranslationController {
constructor(private readonly translations: TagTranslationService) {}
@Get()
@Header('Cache-Control', 'no-store')
findByEdge(@Query() query: GetTagTranslationsDto) {
return this.translations.findByEdge(query);
}
}

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { TagTranslationController } from './tag-translation.controller';
import { TagTranslationService } from './tag-translation.service';
@Module({
controllers: [TagTranslationController],
providers: [TagTranslationService],
})
export class TagTranslationModule {}

View File

@@ -0,0 +1,67 @@
import { Injectable } from '@nestjs/common';
import { parseCommaSeparatedList } from '../common/query-list';
import { normalizeRequiredText } from '../common/normalize-text';
import { DbService } from '../db/db.service';
import { GetTagTranslationsDto } from './dto/get-tag-translations.dto';
type TagTranslationRow = {
edge_id: string;
tag_id: string;
locale: string;
display_name: string;
source: string | null;
updated_at: Date;
};
type TagTranslationItem = {
edge: string;
tag: string;
locale: string;
displayName: string;
source: string | null;
updatedAt: Date;
};
type TagTranslationResponse = {
edge: string;
locale: string;
items: TagTranslationItem[];
};
@Injectable()
export class TagTranslationService {
constructor(private readonly db: DbService) {}
// Возвращает человекочитаемые названия тегов для выбранного edge и языка.
async findByEdge(query: GetTagTranslationsDto): Promise<TagTranslationResponse> {
const edge = normalizeRequiredText(query.edge, 'edge');
const locale = (query.locale?.trim() || 'ru').toLowerCase();
const tags = parseCommaSeparatedList(query.tags) ?? null;
const result = await this.db.query<TagTranslationRow>(
`
SELECT edge_id, tag_id, locale, display_name, source, updated_at
FROM tag_translation
WHERE edge_id = $1
AND locale = $2
AND ($3::text[] IS NULL OR tag_id = ANY($3::text[]))
ORDER BY tag_id ASC
`,
[edge, locale, tags],
);
return {
edge,
locale,
items: result.rows.map((row) => ({
edge: row.edge_id,
tag: row.tag_id,
locale: row.locale,
displayName: row.display_name,
source: row.source,
updatedAt: row.updated_at,
})),
};
}
}