← 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 });
}
}