131 lines
4.1 KiB
JavaScript
131 lines
4.1 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const configPath = path.join(__dirname, 'APIinvima.json');
|
|
const config = JSON.parse(fs.readFileSync(configPath, 'utf8').replace(/^\uFEFF/, ''));
|
|
|
|
const INVIMA_DATASETS = Object.fromEntries(config.datasets.map((dataset) => [dataset.key, dataset]));
|
|
const INVIMA_DATASET_IDS = Object.fromEntries(config.datasets.map((dataset) => [dataset.id, dataset]));
|
|
|
|
const ALLOWED_SODA_PARAMS = ['$limit', '$offset', '$select', '$where', '$order'];
|
|
const RETRYABLE_STATUS_CODES = new Set([408, 425, 429, 500, 502, 503, 504]);
|
|
|
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
|
|
const clampInt = (value, fallback, min, max) => {
|
|
const parsed = Number.parseInt(String(value ?? ''), 10);
|
|
if (Number.isNaN(parsed)) return fallback;
|
|
return Math.max(min, Math.min(max, parsed));
|
|
};
|
|
|
|
const computeBackoffMs = (attempt, baseMs = 800, maxMs = 8000) => {
|
|
const exp = baseMs * 2 ** attempt;
|
|
const jitter = Math.floor(Math.random() * 250);
|
|
return Math.min(maxMs, exp + jitter);
|
|
};
|
|
|
|
const isNetworkRetryableError = (error) => {
|
|
const code = String(error?.cause?.code || error?.code || '').toUpperCase();
|
|
if (code.includes('TIMEOUT') || code.includes('ECONNRESET') || code.includes('EAI_AGAIN')) return true;
|
|
const message = String(error?.message || '').toLowerCase();
|
|
return (
|
|
message.includes('fetch failed') ||
|
|
message.includes('timeout') ||
|
|
message.includes('socket hang up') ||
|
|
message.includes('temporar')
|
|
);
|
|
};
|
|
|
|
const cleanParamValue = (value) => {
|
|
if (value === undefined || value === null) return null;
|
|
const text = String(value).trim();
|
|
return text ? text : null;
|
|
};
|
|
|
|
const buildInvimaUrl = (datasetId, params = {}) => {
|
|
const url = new URL(`${config.baseUrl}/${datasetId}.json`);
|
|
|
|
for (const key of ALLOWED_SODA_PARAMS) {
|
|
const value = cleanParamValue(params[key]);
|
|
if (value !== null) {
|
|
url.searchParams.set(key, value);
|
|
}
|
|
}
|
|
|
|
return url.toString();
|
|
};
|
|
|
|
const resolveDataset = (datasetKeyOrId) => {
|
|
return INVIMA_DATASETS[datasetKeyOrId] || INVIMA_DATASET_IDS[datasetKeyOrId] || null;
|
|
};
|
|
|
|
const fetchInvimaDataset = async (
|
|
datasetKeyOrId,
|
|
options = {},
|
|
appToken = process.env.SOCRATA_APP_TOKEN,
|
|
requestOptions = {}
|
|
) => {
|
|
const dataset = resolveDataset(datasetKeyOrId);
|
|
if (!dataset) {
|
|
throw new Error(`Dataset no reconocido: ${datasetKeyOrId}`);
|
|
}
|
|
|
|
const url = buildInvimaUrl(dataset.id, options);
|
|
const headers = { Accept: 'application/json' };
|
|
const maxRetries = clampInt(requestOptions.retries, 4, 0, 8);
|
|
const timeoutMs = clampInt(requestOptions.timeoutMs, 35000, 4000, 120000);
|
|
|
|
if (appToken) {
|
|
headers['X-App-Token'] = appToken;
|
|
}
|
|
|
|
let lastError = null;
|
|
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
const attemptLabel = `${attempt + 1}/${maxRetries + 1}`;
|
|
try {
|
|
const signal = typeof AbortSignal?.timeout === 'function' ? AbortSignal.timeout(timeoutMs) : undefined;
|
|
const response = await fetch(url, { headers, signal });
|
|
if (!response.ok) {
|
|
const body = await response.text();
|
|
const error = new Error(
|
|
`Error consultando ${dataset.key} (${dataset.id}): ${response.status} ${response.statusText}. ${body}`
|
|
);
|
|
lastError = error;
|
|
|
|
if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < maxRetries) {
|
|
await sleep(computeBackoffMs(attempt));
|
|
continue;
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
return response.json();
|
|
} catch (error) {
|
|
lastError = error;
|
|
if (attempt < maxRetries && isNetworkRetryableError(error)) {
|
|
await sleep(computeBackoffMs(attempt));
|
|
continue;
|
|
}
|
|
|
|
if (error instanceof Error && error.message.includes('Error consultando')) {
|
|
throw error;
|
|
}
|
|
|
|
throw new Error(
|
|
`Error consultando ${dataset.key} (${dataset.id}) [intento ${attemptLabel}]: ${String(error?.message || error)}`
|
|
);
|
|
}
|
|
}
|
|
|
|
throw lastError || new Error(`No fue posible consultar dataset ${dataset.key} (${dataset.id}).`);
|
|
};
|
|
|
|
module.exports = {
|
|
apiConfig: config,
|
|
INVIMA_DATASETS,
|
|
INVIMA_DATASET_IDS,
|
|
ALLOWED_SODA_PARAMS,
|
|
buildInvimaUrl,
|
|
fetchInvimaDataset
|
|
};
|