Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f3e3280967 |
@@ -1,145 +0,0 @@
|
|||||||
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
73
package-lock.json
generated
@@ -16,6 +16,7 @@
|
|||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.2",
|
"class-validator": "^0.14.2",
|
||||||
"compression": "^1.8.1",
|
"compression": "^1.8.1",
|
||||||
|
"joi": "^18.2.1",
|
||||||
"pg": "^8.13.1",
|
"pg": "^8.13.1",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
@@ -1035,6 +1036,54 @@
|
|||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"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": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.2",
|
"version": "0.19.2",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
||||||
@@ -2565,6 +2614,12 @@
|
|||||||
"@sinonjs/commons": "^3.0.1"
|
"@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": {
|
"node_modules/@tokenizer/inflate": {
|
||||||
"version": "0.4.1",
|
"version": "0.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz",
|
||||||
@@ -7171,6 +7226,24 @@
|
|||||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
"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": {
|
"node_modules/js-tokens": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
|||||||
@@ -21,27 +21,28 @@
|
|||||||
"class-transformer": "^0.5.1",
|
"class-transformer": "^0.5.1",
|
||||||
"class-validator": "^0.14.2",
|
"class-validator": "^0.14.2",
|
||||||
"compression": "^1.8.1",
|
"compression": "^1.8.1",
|
||||||
|
"joi": "^18.2.1",
|
||||||
"pg": "^8.13.1",
|
"pg": "^8.13.1",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1"
|
"rxjs": "^7.8.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.18.0",
|
||||||
"@nestjs/cli": "^11.0.0",
|
"@nestjs/cli": "^11.0.0",
|
||||||
"@types/compression": "^1.8.1",
|
"@types/compression": "^1.8.1",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/jest": "^30.0.0",
|
"@types/jest": "^30.0.0",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
"@types/pg": "^8.11.10",
|
"@types/pg": "^8.11.10",
|
||||||
|
"eslint": "^9.18.0",
|
||||||
|
"globals": "^16.0.0",
|
||||||
"jest": "^30.0.0",
|
"jest": "^30.0.0",
|
||||||
"ts-jest": "^29.2.5",
|
"ts-jest": "^29.2.5",
|
||||||
"ts-loader": "^9.5.2",
|
"ts-loader": "^9.5.2",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
"typescript": "^5.7.3",
|
"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": {
|
"jest": {
|
||||||
"moduleFileExtensions": [
|
"moduleFileExtensions": [
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { envValidationSchema } from './common/env.validation';
|
||||||
import { CurrentModule } from './current/current.module';
|
import { CurrentModule } from './current/current.module';
|
||||||
import { DbModule } from './db/db.module';
|
import { DbModule } from './db/db.module';
|
||||||
import { EdgeModule } from './edge/edge.module';
|
import { EdgeModule } from './edge/edge.module';
|
||||||
import { HealthController } from './health.controller';
|
import { HealthController } from './health.controller';
|
||||||
import { HistoryModule } from './history/history.module';
|
import { HistoryModule } from './history/history.module';
|
||||||
import { IngestModule } from './ingest/ingest.module';
|
import { IngestModule } from './ingest/ingest.module';
|
||||||
import { TagTranslationModule } from './tag-translation/tag-translation.module';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({ isGlobal: true }),
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
validationSchema: envValidationSchema,
|
||||||
|
}),
|
||||||
DbModule,
|
DbModule,
|
||||||
IngestModule,
|
IngestModule,
|
||||||
EdgeModule,
|
EdgeModule,
|
||||||
CurrentModule,
|
CurrentModule,
|
||||||
HistoryModule,
|
HistoryModule,
|
||||||
TagTranslationModule,
|
|
||||||
],
|
],
|
||||||
controllers: [HealthController],
|
controllers: [HealthController],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
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));
|
|
||||||
}
|
|
||||||
30
src/common/env.validation.ts
Normal file
30
src/common/env.validation.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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),
|
||||||
|
});
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import type { Request } from 'express';
|
import type { Request } from 'express';
|
||||||
|
import { EnvironmentVariables } from './env.validation';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class IngestApiKeyGuard implements CanActivate {
|
export class IngestApiKeyGuard implements CanActivate {
|
||||||
constructor(private readonly config: ConfigService) {}
|
constructor(private readonly config: ConfigService<EnvironmentVariables, true>) {}
|
||||||
|
|
||||||
canActivate(context: ExecutionContext): boolean {
|
canActivate(context: ExecutionContext): boolean {
|
||||||
const expectedKey = this.config.get<string>('INGEST_API_KEY');
|
const expectedKey = this.config.get('INGEST_API_KEY');
|
||||||
if (!expectedKey) {
|
if (!expectedKey) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { Pool, PoolClient, QueryResult, QueryResultRow } from 'pg';
|
import { Pool, PoolClient, QueryResult, QueryResultRow } from 'pg';
|
||||||
import { getIntegerConfig } from '../common/config-number';
|
import { EnvironmentVariables } from '../common/env.validation';
|
||||||
|
|
||||||
type DatabaseHealth = {
|
type DatabaseHealth = {
|
||||||
now: Date;
|
now: Date;
|
||||||
timescaledb_installed: boolean;
|
timescaledb_installed: boolean;
|
||||||
@@ -13,18 +12,13 @@ type DatabaseHealth = {
|
|||||||
export class DbService implements OnModuleInit, OnModuleDestroy {
|
export class DbService implements OnModuleInit, OnModuleDestroy {
|
||||||
private pool!: Pool;
|
private pool!: Pool;
|
||||||
|
|
||||||
constructor(private readonly config: ConfigService) {}
|
constructor(private readonly config: ConfigService<EnvironmentVariables, true>) {}
|
||||||
|
|
||||||
// Инициализируем пул один раз на модуль и сразу проверяем доступность БД.
|
// Инициализируем пул один раз на модуль и сразу проверяем доступность БД.
|
||||||
async onModuleInit(): Promise<void> {
|
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({
|
this.pool = new Pool({
|
||||||
connectionString,
|
connectionString: this.config.get('DATABASE_URL'),
|
||||||
max: getIntegerConfig(this.config, 'PG_POOL_MAX', 20),
|
max: this.config.get('PG_POOL_MAX'),
|
||||||
idleTimeoutMillis: 30_000,
|
idleTimeoutMillis: 30_000,
|
||||||
connectionTimeoutMillis: 5_000,
|
connectionTimeoutMillis: 5_000,
|
||||||
statement_timeout: 60_000,
|
statement_timeout: 60_000,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { getIntegerConfig } from '../common/config-number';
|
import { EnvironmentVariables } from '../common/env.validation';
|
||||||
import { normalizeRequiredText } from '../common/normalize-text';
|
import { normalizeRequiredText } from '../common/normalize-text';
|
||||||
import { GetHistoryDto } from './dto/get-history.dto';
|
import { GetHistoryDto } from './dto/get-history.dto';
|
||||||
import { chooseHistorySource } from './history-source';
|
import { chooseHistorySource } from './history-source';
|
||||||
@@ -12,7 +12,7 @@ import { HistoryResponse } from './history.types';
|
|||||||
export class HistoryService {
|
export class HistoryService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly repository: HistoryRepository,
|
private readonly repository: HistoryRepository,
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService<EnvironmentVariables, true>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// Use-case для графиков: выбирает latest/raw/aggregate и возвращает единый API-формат.
|
// Use-case для графиков: выбирает latest/raw/aggregate и возвращает единый API-формат.
|
||||||
@@ -84,8 +84,8 @@ export class HistoryService {
|
|||||||
|
|
||||||
// Ограничивает плотность ответа, чтобы API не возвращал больше точек, чем способен отрисовать UI.
|
// Ограничивает плотность ответа, чтобы API не возвращал больше точек, чем способен отрисовать UI.
|
||||||
private getTargetPoints(requested?: number): number {
|
private getTargetPoints(requested?: number): number {
|
||||||
const defaultValue = getIntegerConfig(this.config, 'HISTORY_TARGET_POINTS', 2000);
|
const defaultValue = this.config.get('HISTORY_TARGET_POINTS');
|
||||||
const maxValue = getIntegerConfig(this.config, 'HISTORY_MAX_TARGET_POINTS', 2000);
|
const maxValue = this.config.get('HISTORY_MAX_TARGET_POINTS');
|
||||||
const value = requested ?? defaultValue;
|
const value = requested ?? defaultValue;
|
||||||
return Math.min(Math.max(Math.floor(value), 100), maxValue);
|
return Math.min(Math.max(Math.floor(value), 100), maxValue);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { getIntegerConfig } from '../common/config-number';
|
import { EnvironmentVariables } from '../common/env.validation';
|
||||||
import { normalizeRequiredText } from '../common/normalize-text';
|
import { normalizeRequiredText } from '../common/normalize-text';
|
||||||
import { CurrentEventsService } from '../current/current-events.service';
|
import { CurrentEventsService } from '../current/current-events.service';
|
||||||
import { DbService } from '../db/db.service';
|
import { DbService } from '../db/db.service';
|
||||||
import { IngestPointDto } from './dto/ingest-point.dto';
|
import { IngestPointDto } from './dto/ingest-point.dto';
|
||||||
|
|
||||||
const DEFAULT_MAX_BATCH_SIZE = 10_000;
|
|
||||||
|
|
||||||
type NormalizedPoint = {
|
type NormalizedPoint = {
|
||||||
time: Date;
|
time: Date;
|
||||||
edge: string;
|
edge: string;
|
||||||
@@ -33,7 +31,7 @@ type CurrentValueRow = {
|
|||||||
export class IngestService {
|
export class IngestService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly db: DbService,
|
private readonly db: DbService,
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService<EnvironmentVariables, true>,
|
||||||
private readonly currentEvents: CurrentEventsService,
|
private readonly currentEvents: CurrentEventsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -185,10 +183,7 @@ export class IngestService {
|
|||||||
|
|
||||||
// Ограничивает размер одного запроса, чтобы один producer не занял память и соединение надолго.
|
// Ограничивает размер одного запроса, чтобы один producer не занял память и соединение надолго.
|
||||||
private assertBatchSize(size: number): void {
|
private assertBatchSize(size: number): void {
|
||||||
const maxBatchSize = Math.max(
|
const maxBatchSize = this.config.get('INGEST_MAX_BATCH_SIZE');
|
||||||
getIntegerConfig(this.config, 'INGEST_MAX_BATCH_SIZE', DEFAULT_MAX_BATCH_SIZE),
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (size > maxBatchSize) {
|
if (size > maxBatchSize) {
|
||||||
throw new BadRequestException(`Batch size must not exceed ${maxBatchSize} points.`);
|
throw new BadRequestException(`Batch size must not exceed ${maxBatchSize} points.`);
|
||||||
|
|||||||
10
src/main.ts
10
src/main.ts
@@ -1,14 +1,18 @@
|
|||||||
import { ValidationPipe } from '@nestjs/common';
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { NestFactory } from '@nestjs/core';
|
import { NestFactory } from '@nestjs/core';
|
||||||
import compression from 'compression';
|
import compression from 'compression';
|
||||||
import { AppModule } from './app.module';
|
import { AppModule } from './app.module';
|
||||||
|
import { EnvironmentVariables } from './common/env.validation';
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule);
|
||||||
app.use(compression());
|
app.use(compression());
|
||||||
|
|
||||||
const corsAllowedOrigins = process.env.CORS_ALLOWED_ORIGINS
|
const config = app.get(ConfigService<EnvironmentVariables, true>);
|
||||||
? process.env.CORS_ALLOWED_ORIGINS.split(',').map((origin) => origin.trim()).filter(Boolean)
|
const corsOrigins = config.get('CORS_ALLOWED_ORIGINS');
|
||||||
|
const corsAllowedOrigins = corsOrigins
|
||||||
|
? corsOrigins.split(',').map((origin: string) => origin.trim()).filter(Boolean)
|
||||||
: true;
|
: true;
|
||||||
|
|
||||||
// CORS настраивается через env, чтобы один build работал локально и в окружениях деплоя.
|
// CORS настраивается через env, чтобы один build работал локально и в окружениях деплоя.
|
||||||
@@ -28,7 +32,7 @@ async function bootstrap() {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
await app.listen(process.env.PORT ?? 3100);
|
await app.listen(config.get('PORT'));
|
||||||
}
|
}
|
||||||
|
|
||||||
void bootstrap();
|
void bootstrap();
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
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[];
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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 {}
|
|
||||||
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
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,
|
|
||||||
})),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user