← back to Norma
app/api/ingest/action-network/route.ts
127 lines
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
import { getOrgId } from '@/lib/orgId';
import {
listPetitions,
type ActionNetworkPetition,
} from '@/lib/action-network';
/**
* POST /api/ingest/action-network
*
* Ingests petitions from Action Network.
* Body: { pages?: number }
*
* Upserts into action_network_petitions via ON CONFLICT (an_id) DO UPDATE.
*/
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 = { ingested: 0, errors: [] as string[] };
try {
const body = await request.json().catch(() => ({}));
const pages: number = Math.min(body.pages ?? 3, 20); // Cap at 20 pages
// De-duplicate petitions across pages
const allPetitions = new Map<string, ActionNetworkPetition>();
for (let page = 1; page <= pages; page++) {
try {
const petitions = await listPetitions(page, 25);
if (petitions.length === 0) break; // No more pages
for (const petition of petitions) {
if (petition.id && !allPetitions.has(petition.id)) {
allPetitions.set(petition.id, petition);
}
}
} catch (err) {
stats.errors.push(`page_${page}: ${(err as Error).message}`);
}
}
// Upsert each petition
for (const petition of allPetitions.values()) {
try {
await query(
`INSERT INTO action_network_petitions (
an_id, title, description, petition_text, target,
total_signatures, status, created_date, modified_date,
url, raw_json, org_id
) VALUES (
$1, $2, $3, $4, $5,
$6, $7, $8, $9,
$10, $11, $12
)
ON CONFLICT (an_id) DO UPDATE SET
title = EXCLUDED.title,
description = EXCLUDED.description,
petition_text = EXCLUDED.petition_text,
target = EXCLUDED.target,
total_signatures = EXCLUDED.total_signatures,
status = EXCLUDED.status,
modified_date = EXCLUDED.modified_date,
url = EXCLUDED.url,
raw_json = EXCLUDED.raw_json,
updated_at = NOW()`,
[
petition.id,
petition.title,
petition.description,
petition.petitionText,
petition.target,
petition.totalSignatures,
petition.status,
petition.createdDate || null,
petition.modifiedDate || null,
petition.url,
JSON.stringify(petition.rawJson),
orgId,
],
);
stats.ingested++;
} catch (err) {
stats.errors.push(`insert_${petition.id}: ${(err as Error).message}`);
}
}
} catch (err) {
stats.errors.push('action_network_ingest: ' + (err as Error).message);
}
return NextResponse.json({
success: true,
ingested: stats.ingested,
errors: stats.errors,
message: `Ingested ${stats.ingested} Action Network petitions`,
});
}
/* ─── GET: Return ingested Action Network petitions ──────────────────────── */
export async function GET(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
try {
const { rows } = await query(`
SELECT id, an_id, title, description, petition_text, target,
total_signatures, status, created_date, modified_date,
url, created_at, updated_at
FROM action_network_petitions
ORDER BY total_signatures DESC, modified_date DESC NULLS LAST
LIMIT 200
`);
return NextResponse.json({
petitions: rows,
total: rows.length,
});
} catch (err) {
console.error('[ingest/action-network] GET error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to fetch Action Network petitions' }, { status: 500 });
}
}