← back to Nationalrealestate

src/ingest/run.ts

54 lines

/**
 * Shared ingest harness: every job opens an ingest_runs row, downloads to
 * data/downloads/ (sha256 recorded), and closes the run with counts.
 * HTTP != 200 or a 0-row parse must end as status='failed' + non-zero exit —
 * URL drift shows up as a red run, never silent staleness.
 */
import { createHash } from 'node:crypto';
import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { query } from '../../db/pool.ts';

const __dirname = dirname(fileURLToPath(import.meta.url));
export const DOWNLOADS = join(__dirname, '..', '..', 'data', 'downloads');

export async function openRun(source: string, url?: string): Promise<number> {
  const r = await query<{ id: number }>(
    `INSERT INTO ingest_runs (source, url) VALUES ($1, $2) RETURNING id`,
    [source, url ?? null],
  );
  return r.rows[0].id;
}

export async function closeRun(
  id: number,
  status: 'ok' | 'failed',
  counts: { upserted?: number; skipped?: number; notes?: string; fileHash?: string } = {},
): Promise<void> {
  await query(
    `UPDATE ingest_runs SET finished_at = NOW(), status = $2,
       rows_upserted = COALESCE($3, rows_upserted),
       rows_skipped = COALESCE($4, rows_skipped),
       notes = COALESCE($5, notes),
       file_hash = COALESCE($6, file_hash)
     WHERE id = $1`,
    [id, status, counts.upserted ?? null, counts.skipped ?? null, counts.notes ?? null, counts.fileHash ?? null],
  );
}

/** Download url -> data/downloads/<filename>. Returns { path, sha256 }. Throws on non-200. */
export async function download(url: string, filename: string, opts: { reuseCached?: boolean; headers?: Record<string, string> } = {}): Promise<{ path: string; sha256: string }> {
  mkdirSync(DOWNLOADS, { recursive: true });
  const dest = join(DOWNLOADS, filename);
  if (opts.reuseCached && existsSync(dest)) {
    const buf = readFileSync(dest);
    return { path: dest, sha256: createHash('sha256').update(buf).digest('hex') };
  }
  const res = await fetch(url, { redirect: 'follow', headers: opts.headers });
  if (!res.ok) throw new Error(`download failed ${res.status} ${res.statusText}: ${url}`);
  const buf = Buffer.from(await res.arrayBuffer());
  writeFileSync(dest, buf);
  return { path: dest, sha256: createHash('sha256').update(buf).digest('hex') };
}