← back to AbramsOS
lib/cpsc-fetcher.js
60 lines
// CPSC Recall API client.
// Public, unauthenticated. Docs: https://www.cpsc.gov/Recalls/CPSC-Recalls-Application-Programming-Interface-API
// Endpoint: https://www.saferproducts.gov/RestWebServices/Recall
// Filters: RecallDateStart, RecallDateEnd, RecallNumber, RecallTitle, RecallURL, etc.
const BASE = 'https://www.saferproducts.gov/RestWebServices/Recall';
async function fetchRecalls({ since = null, until = null, format = 'json', timeoutMs = 30_000 } = {}) {
const params = new URLSearchParams({ format });
if (since) params.set('RecallDateStart', toCpscDate(since));
if (until) params.set('RecallDateEnd', toCpscDate(until));
const url = `${BASE}?${params.toString()}`;
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), timeoutMs);
try {
const r = await fetch(url, { signal: ctrl.signal });
if (!r.ok) throw new Error(`CPSC ${r.status}: ${url}`);
return r.json();
} finally {
clearTimeout(t);
}
}
function toCpscDate(d) {
if (typeof d === 'string') return d;
return d.toISOString().slice(0, 10); // YYYY-MM-DD
}
// Normalize one CPSC recall into our recall_event shape
function normalize(raw) {
// CPSC API field names use Pascal case
const products = raw.Products || [];
const manufacturers = raw.Manufacturers || [];
const hazards = raw.Hazards || [];
const remedies = raw.Remedies || [];
const images = raw.Images || [];
const product_keys = {
models: products.flatMap(p => [p.Name, p.Model].filter(Boolean)),
brand: manufacturers[0]?.Name || null,
descriptions: products.map(p => p.Description).filter(Boolean),
upcs: products.map(p => p.UPC).filter(Boolean),
image_urls: images.map(i => i.URL).filter(Boolean),
};
return {
authority: 'CPSC',
external_id: String(raw.RecallNumber || raw.RecallID),
published_at: raw.RecallDate ? new Date(raw.RecallDate) : null,
title: raw.Title || raw.RecallTitle || null,
hazard: hazards.map(h => h.Name).filter(Boolean).join('; ') || null,
remedy: remedies.map(r => r.Name).filter(Boolean).join('; ') || null,
url: raw.URL || (raw.RecallNumber ? `https://www.cpsc.gov/Recalls/${raw.RecallNumber}` : null),
product_keys,
raw,
};
}
module.exports = { fetchRecalls, normalize };