Backend: SQLite storage, EvoBGP integration, filters in SQL

Made-with: Cursor
This commit is contained in:
2026-04-20 01:17:00 +07:00
parent 7f3eaea40a
commit 1c6e6ab24a
27 changed files with 2406 additions and 2542 deletions
+6 -5
View File
@@ -39,14 +39,15 @@ function mapAjvErrors(errors) {
}
/**
* Маппинг UI ресурсов на S3 ключи (для истории версий)
* Маппинг UI ресурсов на ключи в SQLite blobs (только локально версионируемые объекты).
* Справочники EvoBGP (domains-new / asns / ip-ranges) в истории не участвуют.
*/
function resourceToKey(resource) {
switch (resource) {
case 'domains-new': return 'bgp_data/domains_community.txt';
case 'ip-ranges': return 'bgp_data/ips.txt';
case 'asns': return 'bgp_data/asns.txt';
default: return null;
case 'servers':
return 'servers.json';
default:
return null;
}
}
+45 -59
View File
@@ -1,42 +1,37 @@
/**
* Универсальные хелперы для работы с S3
* Устраняют дублирование кода в routes
* Универсальные хелперы для JSON/текста из локального storage (SQLite)
*/
const { s3, BUCKET_NAME } = require('../services/s3Service');
const { GetObjectCommand, HeadObjectCommand } = require('@aws-sdk/client-s3');
const { streamToString } = require('../services/s3Service');
const { headMeta, readS3TextObject } = require('../services/s3Service');
const { sendError, checkIfNoneMatch } = require('../middleware/errorHandler');
/**
* Универсальный GET для JSON объектов из S3 с заголовками
* @param {string} s3Key - Ключ S3
* @param {object} req - Express request
* @param {object} res - Express response
* @param {object} options - Опции
* @param {function} options.transform - Функция трансформации данных
* @param {*} options.defaultValue - Значение по умолчанию при отсутствии файла
* Универсальный GET для JSON объектов с заголовками
*/
async function getS3JsonWithHeaders(s3Key, req, res, options = {}) {
const { transform = (data) => data, defaultValue = [] } = options;
try {
// Получаем HEAD для установки заголовков
const head = await s3.send(new HeadObjectCommand({ Bucket: BUCKET_NAME, Key: s3Key })).catch(() => null);
const etag = head?.ETag || null;
// Устанавливаем заголовки
const head = await headMeta(s3Key);
const etag = head?.etag || null;
if (etag) res.set('ETag', String(etag));
if (head?.LastModified) res.set('Last-Modified', new Date(head.LastModified).toUTCString());
if (typeof head?.ContentLength === 'number') res.set('Content-Length-Source', String(head.ContentLength));
// Проверяем If-None-Match
if (head?.lastModified) res.set('Last-Modified', new Date(head.lastModified).toUTCString());
if (typeof head?.contentLength === 'number') res.set('Content-Length-Source', String(head.contentLength));
if (checkIfNoneMatch(req, res, etag)) return;
// Получаем объект
const data = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: s3Key }));
const fileContent = await streamToString(data.Body);
let fileContent;
try {
const data = await readS3TextObject(s3Key);
fileContent = data.body;
} catch (e) {
if (e?.code === 'NoSuchKey') {
return res.json(transform(defaultValue));
}
throw e;
}
let parsed;
try {
parsed = JSON.parse(fileContent);
@@ -47,55 +42,47 @@ async function getS3JsonWithHeaders(s3Key, req, res, options = {}) {
console.error(`Error parsing ${s3Key}:`, parseError);
parsed = defaultValue;
}
// Применяем трансформацию
const result = transform(parsed);
res.json(result);
} catch (error) {
if (error?.code === 'NoSuchKey' || error?.$metadata?.httpStatusCode === 404) {
res.json(defaultValue);
} else {
console.error(error);
return sendError(res, 500, 'Error reading from S3', 'E_S3');
}
console.error(error);
return sendError(res, 500, 'Error reading storage', 'E_STORAGE');
}
}
/**
* Универсальный GET для текстовых объектов из S3 с заголовками
* @param {string} s3Key - Ключ S3
* @param {object} req - Express request
* @param {object} res - Express response
* @param {object} options - Опции
* @param {function} options.transform - Функция трансформации текста
* @param {string} options.defaultValue - Значение по умолчанию при отсутствии файла
* Универсальный GET для текстовых объектов с заголовками
*/
async function getS3TextWithHeaders(s3Key, req, res, options = {}) {
const { transform = (text) => ({ config: text }), defaultValue = '' } = options;
try {
const head = await s3.send(new HeadObjectCommand({ Bucket: BUCKET_NAME, Key: s3Key })).catch(() => null);
const etag = head?.ETag || null;
const head = await headMeta(s3Key);
const etag = head?.etag || null;
if (etag) res.set('ETag', String(etag));
if (head?.LastModified) res.set('Last-Modified', new Date(head.LastModified).toUTCString());
if (typeof head?.ContentLength === 'number') res.set('Content-Length-Source', String(head.ContentLength));
if (head?.lastModified) res.set('Last-Modified', new Date(head.lastModified).toUTCString());
if (typeof head?.contentLength === 'number') res.set('Content-Length-Source', String(head.contentLength));
if (checkIfNoneMatch(req, res, etag)) return;
const data = await s3.send(new GetObjectCommand({ Bucket: BUCKET_NAME, Key: s3Key }));
const text = await streamToString(data.Body);
let text;
try {
const data = await readS3TextObject(s3Key);
text = data.body;
} catch (e) {
if (e?.code === 'NoSuchKey') {
return res.json(transform(defaultValue));
}
throw e;
}
const result = transform(text);
res.json(result);
} catch (error) {
if (error?.code === 'NoSuchKey' || error?.$metadata?.httpStatusCode === 404) {
res.json(transform(defaultValue));
} else {
console.error(error);
return sendError(res, 500, 'Error reading from S3', 'E_S3');
}
console.error(error);
return sendError(res, 500, 'Error reading storage', 'E_STORAGE');
}
}
@@ -103,4 +90,3 @@ module.exports = {
getS3JsonWithHeaders,
getS3TextWithHeaders,
};