← back to Norma
app/api/intelligence/power-scores/route.ts
105 lines
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
const ALLOWED_SORT_COLUMNS = new Set([
'total_score',
'connection_density',
'financial_muscle',
'advocacy_record',
'rank',
]);
/**
* GET /api/intelligence/power-scores
* Query params:
* ?type= — filter by entity_type
* ?state= — filter by state (matched inside score_breakdown JSONB)
* ?min_score= — minimum total_score (inclusive)
* ?sort= — total_score | connection_density | financial_muscle | advocacy_record | rank
* ?limit= — rows to return (default 50, max 200)
* ?q= — search by entity_name (ILIKE)
* Returns: { scores: [...] }
*/
export async function GET(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
try {
const { searchParams } = new URL(request.url);
const type = searchParams.get('type');
const state = searchParams.get('state');
const minScoreRaw = searchParams.get('min_score');
const sortParam = searchParams.get('sort') || 'total_score';
const q = searchParams.get('q');
let limit = parseInt(searchParams.get('limit') || '50', 10);
if (isNaN(limit) || limit < 1) limit = 50;
if (limit > 200) limit = 200;
// Validate sort column against allowlist to prevent SQL injection
const sortColumn = ALLOWED_SORT_COLUMNS.has(sortParam) ? sortParam : 'total_score';
// rank sorts ASC (lower rank = better); all score columns sort DESC
const sortDirection = sortColumn === 'rank' ? 'ASC' : 'DESC';
const conditions: string[] = [];
const values: unknown[] = [];
let paramIndex = 1;
if (type) {
conditions.push(`entity_type = $${paramIndex}`);
values.push(type);
paramIndex++;
}
if (state) {
conditions.push(`score_breakdown->>'state' = $${paramIndex}`);
values.push(state);
paramIndex++;
}
if (minScoreRaw !== null) {
const minScore = parseFloat(minScoreRaw);
if (!isNaN(minScore)) {
conditions.push(`total_score >= $${paramIndex}`);
values.push(minScore);
paramIndex++;
}
}
if (q) {
conditions.push(`entity_name ILIKE $${paramIndex}`);
values.push(`%${q}%`);
paramIndex++;
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
const result = await query(
`SELECT
entity_type,
entity_id,
entity_name,
connection_density,
financial_muscle,
advocacy_record,
total_score,
rank,
top_connections,
score_breakdown,
computed_at
FROM power_scores
${whereClause}
ORDER BY ${sortColumn} ${sortDirection} NULLS LAST
LIMIT $${paramIndex}`,
[...values, limit],
);
return NextResponse.json({ scores: result.rows });
} catch (err) {
console.error('[api/intelligence/power-scores] GET error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to fetch power scores' }, { status: 500 });
}
}