Backend: SQLite storage, EvoBGP integration, filters in SQL
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,257 @@
|
||||
/**
|
||||
* GET/POST для справочников из EvoBGP (ранее textDataRoutes + S3).
|
||||
*/
|
||||
|
||||
const crypto = require('crypto');
|
||||
const evobgpClient = require('../services/evobgpClient');
|
||||
const { sendError, sendOk, checkIfNoneMatch } = require('../middleware/errorHandler');
|
||||
|
||||
const countOnlyCache = { map: new Map(), ttlMs: 10_000 };
|
||||
|
||||
function getCountOnlyCache(cacheKey) {
|
||||
const v = countOnlyCache.map.get(cacheKey);
|
||||
if (!v) return null;
|
||||
if (Date.now() > v.at + countOnlyCache.ttlMs) {
|
||||
countOnlyCache.map.delete(cacheKey);
|
||||
return null;
|
||||
}
|
||||
return v.value;
|
||||
}
|
||||
|
||||
function setCountOnlyCache(cacheKey, value) {
|
||||
countOnlyCache.map.set(cacheKey, { value, at: Date.now() });
|
||||
}
|
||||
|
||||
function etagForJson(obj) {
|
||||
const s = JSON.stringify(obj);
|
||||
const h = crypto.createHash('sha256').update(Buffer.from(s, 'utf8')).digest('hex');
|
||||
return `"${h}"`;
|
||||
}
|
||||
|
||||
function filterPage(items, q, offset, limit) {
|
||||
const qstr = q ? String(q).toLowerCase() : '';
|
||||
const filtered = !qstr
|
||||
? items
|
||||
: items.filter((it) => JSON.stringify(it).toLowerCase().includes(qstr));
|
||||
const total = filtered.length;
|
||||
if (limit > 0) {
|
||||
return { items: filtered.slice(offset, offset + limit), total };
|
||||
}
|
||||
return { items: filtered, total };
|
||||
}
|
||||
|
||||
function createEvobgpListGET({ fetchItems, cachePrefix, validate }) {
|
||||
return async (req, res) => {
|
||||
const { q = '', offset, limit, countOnly, format } = req.query || {};
|
||||
if (!evobgpClient.isConfigured()) {
|
||||
return sendError(res, 503, 'EvoBGP не настроен', 'E_EVOBGP_NOT_CONFIGURED');
|
||||
}
|
||||
try {
|
||||
if (countOnly === 'true') {
|
||||
const cacheKey = `${cachePrefix}:count:${q}`;
|
||||
const cached = getCountOnlyCache(cacheKey);
|
||||
if (cached != null) {
|
||||
return res.json(format === 'std' ? { items: [], total: cached, meta: {} } : { total: cached });
|
||||
}
|
||||
const all = await fetchItems();
|
||||
const { total } = filterPage(all, q, 0, 0);
|
||||
setCountOnlyCache(cacheKey, total);
|
||||
return res.json(format === 'std' ? { items: [], total, meta: {} } : { total });
|
||||
}
|
||||
|
||||
if (Number(limit) > 0) {
|
||||
const all = await fetchItems();
|
||||
if (!validate(all)) {
|
||||
return sendError(res, 500, 'Invalid data format', 'E_SCHEMA');
|
||||
}
|
||||
const { items, total } = filterPage(all, q, Number(offset) || 0, Number(limit) || 0);
|
||||
return res.json(format === 'std' ? { items, total, meta: {} } : { items, total });
|
||||
}
|
||||
|
||||
const all = await fetchItems();
|
||||
if (!validate(all)) {
|
||||
return sendError(res, 500, 'Invalid data format', 'E_SCHEMA');
|
||||
}
|
||||
const etag = etagForJson(all);
|
||||
res.set('ETag', etag);
|
||||
res.set('Last-Modified', new Date().toUTCString());
|
||||
res.set('Content-Length-Source', String(Buffer.byteLength(JSON.stringify(all), 'utf8')));
|
||||
if (checkIfNoneMatch(req, res, etag)) return;
|
||||
if (format === 'std') {
|
||||
return res.json({ items: all, total: all.length, meta: {} });
|
||||
}
|
||||
return res.json(all);
|
||||
} catch (e) {
|
||||
console.error(cachePrefix, e);
|
||||
return sendError(res, 500, e.message || 'EvoBGP error', 'E_EVOBGP');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function readIfMatch(req) {
|
||||
const raw = req.headers['if-match'] || req.headers['If-Match'];
|
||||
if (!raw) return null;
|
||||
return String(raw).trim();
|
||||
}
|
||||
|
||||
function createEvobgpDomainsNewPOST({ validate, validateItem }) {
|
||||
return async (req, res) => {
|
||||
const { domains: items, etag: bodyEtag } = req.body || {};
|
||||
if (!Array.isArray(items)) {
|
||||
return sendError(res, 400, 'Data must be an array', 'E_BAD_REQUEST');
|
||||
}
|
||||
if (!validate(items)) {
|
||||
return sendError(res, 400, 'Invalid payload format', 'E_SCHEMA');
|
||||
}
|
||||
if (validateItem) {
|
||||
const validationErrors = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const errors = validateItem(items[i], i);
|
||||
if (errors.length > 0) validationErrors.push(...errors);
|
||||
}
|
||||
if (validationErrors.length > 0) {
|
||||
return sendError(res, 400, 'Validation errors', 'E_VALIDATION', {
|
||||
errors: validationErrors.slice(0, 10),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!evobgpClient.isConfigured()) {
|
||||
return sendError(res, 503, 'EvoBGP не настроен', 'E_EVOBGP_NOT_CONFIGURED');
|
||||
}
|
||||
try {
|
||||
const current = await evobgpClient.fetchDomainsData();
|
||||
const currentEtag = etagForJson(current);
|
||||
const ifMatch = readIfMatch(req);
|
||||
const expected = ifMatch || bodyEtag;
|
||||
if (expected && String(expected).trim() !== currentEtag) {
|
||||
return sendError(res, 412, 'ETag mismatch', 'E_ETAG_MISMATCH');
|
||||
}
|
||||
const normalized = items.map((d) => ({
|
||||
domain: String(d.domain || '').trim().toLowerCase(),
|
||||
community: String(d.community || '').trim(),
|
||||
}));
|
||||
await evobgpClient.saveDomainsData(normalized, current);
|
||||
return sendOk(res, { etag: null, lastModified: new Date().toISOString(), contentLength: null });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return sendError(res, 500, e.message || 'EvoBGP write failed', 'E_EVOBGP');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createEvobgpAsnsPOST({ validate, validateItem }) {
|
||||
return async (req, res) => {
|
||||
const { domains: items, etag: bodyEtag } = req.body || {};
|
||||
if (!Array.isArray(items)) {
|
||||
return sendError(res, 400, 'Data must be an array', 'E_BAD_REQUEST');
|
||||
}
|
||||
if (!validate(items)) {
|
||||
return sendError(res, 400, 'Invalid payload format', 'E_SCHEMA');
|
||||
}
|
||||
if (validateItem) {
|
||||
const validationErrors = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const errors = validateItem(items[i], i);
|
||||
if (errors.length > 0) validationErrors.push(...errors);
|
||||
}
|
||||
if (validationErrors.length > 0) {
|
||||
return sendError(res, 400, 'Validation errors', 'E_VALIDATION', {
|
||||
errors: validationErrors.slice(0, 10),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!evobgpClient.isConfigured()) {
|
||||
return sendError(res, 503, 'EvoBGP не настроен', 'E_EVOBGP_NOT_CONFIGURED');
|
||||
}
|
||||
try {
|
||||
const current = await evobgpClient.fetchAsnsData();
|
||||
const currentEtag = etagForJson(
|
||||
current.map((a) => ({ domain: a.asn, type: a.community })),
|
||||
);
|
||||
const ifMatch = readIfMatch(req);
|
||||
const expected = ifMatch || bodyEtag;
|
||||
if (expected && String(expected).trim() !== currentEtag) {
|
||||
return sendError(res, 412, 'ETag mismatch', 'E_ETAG_MISMATCH');
|
||||
}
|
||||
const normalized = items.map((a) => ({
|
||||
asn: String(a.domain || a.asn || '').trim(),
|
||||
community: String(a.type || a.community || '').trim(),
|
||||
}));
|
||||
await evobgpClient.saveAsnsData(normalized, current);
|
||||
return sendOk(res, { etag: null, lastModified: new Date().toISOString(), contentLength: null });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return sendError(res, 500, e.message || 'EvoBGP write failed', 'E_EVOBGP');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createEvobgpIpRangesPOST({ validate, validateItem }) {
|
||||
return async (req, res) => {
|
||||
const payload = Array.isArray(req.body?.domains)
|
||||
? req.body.domains
|
||||
: Array.isArray(req.body?.ipRanges)
|
||||
? req.body.ipRanges
|
||||
: null;
|
||||
const { etag: bodyEtag } = req.body || {};
|
||||
if (!Array.isArray(payload)) {
|
||||
return sendError(res, 400, 'Data must be an array', 'E_BAD_REQUEST');
|
||||
}
|
||||
if (!validate(payload)) {
|
||||
return sendError(res, 400, 'Invalid payload format', 'E_SCHEMA');
|
||||
}
|
||||
if (validateItem) {
|
||||
const validationErrors = [];
|
||||
for (let i = 0; i < payload.length; i++) {
|
||||
const errors = validateItem(payload[i], i);
|
||||
if (errors.length > 0) validationErrors.push(...errors);
|
||||
}
|
||||
if (validationErrors.length > 0) {
|
||||
return sendError(res, 400, 'Validation errors', 'E_VALIDATION', {
|
||||
errors: validationErrors.slice(0, 10),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!evobgpClient.isConfigured()) {
|
||||
return sendError(res, 503, 'EvoBGP не настроен', 'E_EVOBGP_NOT_CONFIGURED');
|
||||
}
|
||||
try {
|
||||
const current = await evobgpClient.fetchIpRangesData();
|
||||
const currentEtag = etagForJson(current);
|
||||
const ifMatch = readIfMatch(req);
|
||||
const expected = ifMatch || bodyEtag;
|
||||
if (expected && String(expected).trim() !== currentEtag) {
|
||||
return sendError(res, 412, 'ETag mismatch', 'E_ETAG_MISMATCH');
|
||||
}
|
||||
const normalized = payload.map((ip) => ({
|
||||
ipRange: String(ip.ipRange || '').trim(),
|
||||
community: String(ip.community || '').trim(),
|
||||
}));
|
||||
await evobgpClient.saveIpRangesData(normalized, current);
|
||||
return sendOk(res, { etag: null, lastModified: new Date().toISOString(), contentLength: null });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return sendError(res, 500, e.message || 'EvoBGP write failed', 'E_EVOBGP');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** Legacy /api/domains: тот же модуль ASN, ответ в полях domain + type. */
|
||||
function createEvobgpLegacyDomainsGET({ validate }) {
|
||||
return createEvobgpListGET({
|
||||
cachePrefix: 'domains',
|
||||
validate,
|
||||
fetchItems: async () => {
|
||||
const rows = await evobgpClient.fetchAsnsData();
|
||||
return rows.map((r) => ({ domain: r.asn, type: r.community }));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createEvobgpListGET,
|
||||
createEvobgpDomainsNewPOST,
|
||||
createEvobgpAsnsPOST,
|
||||
createEvobgpIpRangesPOST,
|
||||
createEvobgpLegacyDomainsGET,
|
||||
};
|
||||
Reference in New Issue
Block a user