import db from './db.js'; import { fetchFromNhtsa, mapNhtsaToSchema, TTL_SECONDS } from './nhtsa.js'; const VIN_RE = /^[A-HJ-NPR-Z0-9]{17}$/; export function validateVin(vin) { return typeof vin === 'string' && VIN_RE.test(vin.toUpperCase()); } const getStmt = db.prepare( 'SELECT * FROM vin_cache WHERE vin = ? AND expires_at > unixepoch()' ); const upsertStmt = db.prepare(` INSERT INTO vin_cache (vin, make, model, model_year, trim, series, body_class, drive_type, engine_displacement_cc, engine_displacement_l, engine_cylinders, fuel_type_primary, transmission_style, transmission_speeds, plant_city, plant_state, plant_country, manufacturer_name, vehicle_type, error_code, error_text, raw_nhtsa, cached_at, expires_at) VALUES (@vin, @make, @model, @model_year, @trim, @series, @body_class, @drive_type, @engine_displacement_cc, @engine_displacement_l, @engine_cylinders, @fuel_type_primary, @transmission_style, @transmission_speeds, @plant_city, @plant_state, @plant_country, @manufacturer_name, @vehicle_type, @error_code, @error_text, @raw_nhtsa, @cached_at, @expires_at) ON CONFLICT(vin) DO UPDATE SET make=excluded.make, model=excluded.model, model_year=excluded.model_year, trim=excluded.trim, series=excluded.series, body_class=excluded.body_class, drive_type=excluded.drive_type, engine_displacement_cc=excluded.engine_displacement_cc, engine_displacement_l=excluded.engine_displacement_l, engine_cylinders=excluded.engine_cylinders, fuel_type_primary=excluded.fuel_type_primary, transmission_style=excluded.transmission_style, transmission_speeds=excluded.transmission_speeds, plant_city=excluded.plant_city, plant_state=excluded.plant_state, plant_country=excluded.plant_country, manufacturer_name=excluded.manufacturer_name, vehicle_type=excluded.vehicle_type, error_code=excluded.error_code, error_text=excluded.error_text, raw_nhtsa=excluded.raw_nhtsa, cached_at=excluded.cached_at, expires_at=excluded.expires_at `); const evictStmt = db.prepare('DELETE FROM vin_cache WHERE expires_at < unixepoch()'); export function evictExpired() { return evictStmt.run().changes; } export async function decodeVin(vin) { const upperVin = vin.toUpperCase(); const cached = getStmt.get(upperVin); if (cached) { return { result: { ...cached, cached: true }, fromCache: true }; } const raw = await fetchFromNhtsa(upperVin); const mapped = mapNhtsaToSchema(raw); const now = Math.floor(Date.now() / 1000); upsertStmt.run({ vin: upperVin, ...mapped, raw_nhtsa: JSON.stringify(raw), cached_at: now, expires_at: now + TTL_SECONDS, }); return { result: { vin: upperVin, ...mapped, cached: false }, fromCache: false }; } export function getCacheStats() { const count = db.prepare('SELECT COUNT(*) AS c FROM vin_cache WHERE expires_at > unixepoch()').get(); const size = db.prepare("SELECT page_count * page_size AS sz FROM pragma_page_count(), pragma_page_size()").get(); return { entries: count?.c ?? 0, size_mb: size ? Math.round(size.sz / 1024 / 1024 * 100) / 100 : 0, }; }