← back to Norma

app/api/news/resolve-authors/route.ts

152 lines

import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { extractAuthorFromUrl } from '@/lib/author-extract';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';

/**
 * POST /api/news/resolve-authors
 *
 * Batch-resolves author names for news items that have a URL but no author_name.
 * Fetches each article page, parses HTML metadata for the byline, and saves.
 * Limits to 20 per call to avoid long-running requests.
 */
export async function POST(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;
  const stats = { resolved: 0, failed: 0, skipped: 0, errors: [] as string[] };

  try {
    // Find news items that need author resolution
    const orgFilter = orgId ? ' AND org_id = $1' : '';
    const orgParams = orgId ? [orgId] : [];
    const { rows } = await query(
      `SELECT id, url, headline, outlet FROM news_items WHERE url IS NOT NULL AND author_name IS NULL${orgFilter} ORDER BY published_at DESC NULLS LAST LIMIT 20`,
      orgParams
    );

    for (const row of rows) {
      try {
        const result = await extractAuthorFromUrl(row.url);
        if (result.authorName) {
          const updateParams: unknown[] = [result.authorName, result.linkedinUrl, row.id];
          let updateWhere = 'WHERE id = $3';
          if (orgId) {
            updateParams.push(orgId);
            updateWhere += ' AND org_id = $4';
          }
          await query(
            `UPDATE news_items SET author_name = $1, author_linkedin = $2 ${updateWhere}`,
            updateParams
          );
          stats.resolved++;
        } else {
          // Mark as attempted so we don't retry endlessly
          const markParams: unknown[] = [row.id];
          let markWhere = 'WHERE id = $1';
          if (orgId) {
            markParams.push(orgId);
            markWhere += ' AND org_id = $2';
          }
          await query(
            `UPDATE news_items SET author_name = '' ${markWhere}`,
            markParams
          );
          stats.failed++;
        }
      } catch (err) {
        stats.errors.push(row.id + ': ' + (err as Error).message);
        stats.skipped++;
      }
    }

    return NextResponse.json({
      success: true,
      stats,
      message: 'Resolved ' + stats.resolved + ' authors, ' + stats.failed + ' not found, ' + stats.skipped + ' skipped',
    });
  } catch (err) {
    return NextResponse.json({ error: (err as Error).message }, { status: 500 });
  }
}

/**
 * GET /api/news/resolve-authors?id=<uuid>
 *
 * Resolve author for a single news item on demand.
 */
export async function GET(request: NextRequest) {
  const auth = requireRole(request, 'admin', 'staff');
  if (auth instanceof NextResponse) return auth;

  const { searchParams } = new URL(request.url);
  const id = searchParams.get('id');

  if (!id) {
    return NextResponse.json({ error: 'id parameter required' }, { status: 400 });
  }

  const orgId = auth.role === 'admin' ? getOrgId(request) : auth.orgId;

  try {
    const selectParams: unknown[] = [id];
    let selectWhere = 'WHERE id = $1';
    if (orgId) {
      selectParams.push(orgId);
      selectWhere += ' AND org_id = $2';
    }
    const { rows } = await query(`SELECT id, url, headline, outlet, author_name, author_linkedin FROM news_items ${selectWhere}`, selectParams);
    if (rows.length === 0) {
      return NextResponse.json({ error: 'Not found' }, { status: 404 });
    }

    const row = rows[0];

    // Already resolved
    if (row.author_name && row.author_name.length > 0) {
      return NextResponse.json({
        authorName: row.author_name,
        linkedinUrl: row.author_linkedin,
        cached: true,
      });
    }

    if (!row.url) {
      return NextResponse.json({ authorName: null, linkedinUrl: null, method: 'no_url' });
    }

    const result = await extractAuthorFromUrl(row.url);

    // Save to DB — mark as attempted even if not found (prevents infinite retries)
    if (result.authorName) {
      const saveParams: unknown[] = [result.authorName, result.linkedinUrl, id];
      let saveWhere = 'WHERE id = $3';
      if (orgId) {
        saveParams.push(orgId);
        saveWhere += ' AND org_id = $4';
      }
      await query(
        `UPDATE news_items SET author_name = $1, author_linkedin = $2 ${saveWhere}`,
        saveParams
      );
    } else {
      const markParams: unknown[] = [id];
      let markWhere = 'WHERE id = $1';
      if (orgId) {
        markParams.push(orgId);
        markWhere += ' AND org_id = $2';
      }
      await query(
        `UPDATE news_items SET author_name = '' ${markWhere}`,
        markParams
      );
    }

    return NextResponse.json(result);
  } catch (err) {
    return NextResponse.json({ error: (err as Error).message }, { status: 500 });
  }
}