chore: import maquina-comercial-pessoal history via subtree
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> git-subtree-dir: maquina-comercial-pessoal git-subtree-mainline:f5af038e19git-subtree-split:a9ef4d776d
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { paths } from '../../env.mjs';
|
||||
import { UF_NAMES } from '../../sources/common.mjs';
|
||||
import { getSource } from '../../sources/index.mjs';
|
||||
import { isSqliteConstraintError } from '../../db.mjs';
|
||||
|
||||
function hasCnpjBaseForUf(uf) {
|
||||
return fs.existsSync(path.join(paths.cnpjDir, `estabelecimentos-${uf}.csv`))
|
||||
|| (uf === 'BA' && fs.existsSync(path.join(paths.cnpjDir, 'estabelecimentos-bahia.csv')));
|
||||
}
|
||||
|
||||
export function registerCaptureRoutes(router, { db, jobs }) {
|
||||
router.add('POST', '/api/capture/runs', async (req, res, { json, body }) => {
|
||||
const sources = Array.isArray(body?.sources) ? body.sources.map((value) => typeof value === 'string' ? value.trim() : value) : [];
|
||||
const uf = String(body?.uf || '').toUpperCase();
|
||||
const cities = Array.isArray(body?.cities) ? body.cities.map((value) => typeof value === 'string' ? value.trim() : value) : [];
|
||||
const segments = Array.isArray(body?.segments) ? body.segments.map((value) => typeof value === 'string' ? value.trim() : value) : [];
|
||||
const limits = {
|
||||
maxPerQuery: Number(body?.limits?.maxPerQuery),
|
||||
queryLimit: Number(body?.limits?.queryLimit),
|
||||
googlePages: body?.limits?.googlePages === undefined ? 3 : Number(body.limits.googlePages)
|
||||
};
|
||||
|
||||
const invalidArray = (values, max) => !values.length || values.length > max
|
||||
|| values.some((value) => typeof value !== 'string' || !value || value.length > 100);
|
||||
if (invalidArray(sources, 10) || new Set(sources).size !== sources.length) {
|
||||
json(400, { erro: 'fontes invalidas: selecione de 1 a 10 fontes unicas' });
|
||||
return;
|
||||
}
|
||||
for (const id of sources) {
|
||||
if (!getSource(id)) {
|
||||
json(400, { erro: `fonte desconhecida: ${id}` });
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!UF_NAMES[uf]) {
|
||||
json(400, { erro: 'informe uma UF valida' });
|
||||
return;
|
||||
}
|
||||
if (invalidArray(cities, 1000) || (cities.includes('all') && cities.length !== 1)) {
|
||||
json(400, { erro: 'cidades invalidas: selecione de 1 a 1000 municipios ou somente "all"' });
|
||||
return;
|
||||
}
|
||||
if (!cities.includes('all')) {
|
||||
const findCity = db.prepare('SELECT 1 FROM municipios WHERE uf = ? AND nome = ?');
|
||||
const unknown = cities.find((city) => !findCity.get(uf, city));
|
||||
if (unknown) {
|
||||
json(400, { erro: `municipio invalido para ${uf}: ${unknown}` });
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (invalidArray(segments, 250)
|
||||
|| (segments.includes('all') && segments.some((segment) => segment !== 'all' && !segment.startsWith('cnae:')))) {
|
||||
json(400, { erro: 'segmentos invalidos: selecione de 1 a 250 nichos ou somente "all"' });
|
||||
return;
|
||||
}
|
||||
const findSegment = db.prepare('SELECT 1 FROM segments WHERE name = ? AND active = 1');
|
||||
const findCnae = db.prepare('SELECT 1 FROM cnaes WHERE codigo = ?');
|
||||
for (const segment of segments) {
|
||||
if (segment === 'all') continue;
|
||||
if (/^cnae:\d{7}$/.test(segment)) {
|
||||
if (findCnae.get(segment.slice(5))) continue;
|
||||
} else if (findSegment.get(segment)) {
|
||||
continue;
|
||||
}
|
||||
json(400, { erro: `segmento ou CNAE invalido: ${segment}` });
|
||||
return;
|
||||
}
|
||||
if (!Number.isInteger(limits.maxPerQuery) || limits.maxPerQuery < 1 || limits.maxPerQuery > 100
|
||||
|| !Number.isInteger(limits.queryLimit) || limits.queryLimit < 0 || limits.queryLimit > 10000
|
||||
|| !Number.isInteger(limits.googlePages) || limits.googlePages < 1 || limits.googlePages > 3) {
|
||||
json(400, { erro: 'limites invalidos: maxPerQuery 1..100, queryLimit 0..10000, googlePages 1..3' });
|
||||
return;
|
||||
}
|
||||
if ((sources.includes('cnpj-base') || sources.includes('brasilapi')) && !hasCnpjBaseForUf(uf)) {
|
||||
json(409, {
|
||||
erro: `a base CNPJ de ${uf} ainda nao foi extraida`,
|
||||
precisaExtracao: true,
|
||||
uf
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const running = db.prepare("SELECT id FROM capture_runs WHERE status IN ('pending','running')").get();
|
||||
if (running) {
|
||||
json(409, { erro: `ja existe uma captura em andamento (#${running.id}). Aguarde ou cancele.` });
|
||||
return;
|
||||
}
|
||||
|
||||
const params = { sources, uf, cities, segments, limits };
|
||||
let inserted;
|
||||
try {
|
||||
inserted = db.prepare("INSERT INTO capture_runs (params_json, status) VALUES (?, 'pending')")
|
||||
.run(JSON.stringify(params));
|
||||
} catch (error) {
|
||||
if (!isSqliteConstraintError(error)) throw error;
|
||||
json(409, { erro: 'outra captura foi iniciada simultaneamente. Aguarde ou cancele.' });
|
||||
return;
|
||||
}
|
||||
const runId = Number(inserted.lastInsertRowid);
|
||||
const ownerToken = randomUUID();
|
||||
|
||||
const jobId = jobs.startJob({
|
||||
type: 'captura',
|
||||
script: 'src/capture/capture-job.mjs',
|
||||
args: [`--run-id=${runId}`],
|
||||
meta: { runId, uf, sources },
|
||||
ownerToken
|
||||
});
|
||||
db.prepare("UPDATE capture_runs SET job_id = ?, owner_token = ?, heartbeat_at = datetime('now') WHERE id = ?")
|
||||
.run(jobId, ownerToken, runId);
|
||||
|
||||
json(202, { runId, jobId });
|
||||
});
|
||||
|
||||
router.add('GET', '/api/capture/runs', async (req, res, { json, query }) => {
|
||||
const limit = Math.min(100, Math.max(1, Number(query.get('limit')) || 20));
|
||||
const rows = db.prepare(
|
||||
`SELECT id, job_id, params_json, status, progress_json, raw_count, new_count, dup_count,
|
||||
error, started_at, finished_at, created_at
|
||||
FROM capture_runs ORDER BY id DESC LIMIT ?`
|
||||
).all(limit);
|
||||
|
||||
json(200, {
|
||||
runs: rows.map((row) => ({
|
||||
...row,
|
||||
params: JSON.parse(row.params_json || '{}'),
|
||||
progress: JSON.parse(row.progress_json || 'null'),
|
||||
params_json: undefined,
|
||||
progress_json: undefined
|
||||
}))
|
||||
});
|
||||
});
|
||||
|
||||
router.add('GET', '/api/capture/runs/:id', async (req, res, { json, params }) => {
|
||||
const row = db.prepare('SELECT * FROM capture_runs WHERE id = ?').get(Number(params.id));
|
||||
if (!row) {
|
||||
json(404, { erro: 'execucao nao encontrada' });
|
||||
return;
|
||||
}
|
||||
json(200, {
|
||||
...row,
|
||||
params: JSON.parse(row.params_json || '{}'),
|
||||
progress: JSON.parse(row.progress_json || 'null'),
|
||||
params_json: undefined,
|
||||
progress_json: undefined
|
||||
});
|
||||
});
|
||||
|
||||
router.add('POST', '/api/capture/runs/:id/cancel', async (req, res, { json, params }) => {
|
||||
const row = db.prepare('SELECT job_id, status FROM capture_runs WHERE id = ?').get(Number(params.id));
|
||||
if (!row) {
|
||||
json(404, { erro: 'execucao nao encontrada' });
|
||||
return;
|
||||
}
|
||||
const ok = row.job_id ? jobs.cancelJob(row.job_id) : false;
|
||||
json(ok ? 202 : 409, ok ? { cancelando: true } : { erro: `nao ha job ativo para cancelar (status: ${row.status})` });
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user