← back to Norma
app/api/email-analyzer/search/route.ts
73 lines
/**
* GET /api/email-analyzer/search?q=foo&favorites_only=true
*
* Search across saved analysis drafts and versions.
* Ranking: favorites first, then trigram similarity, then newest.
* Returns a flat list so the client can show "successful emails" inline.
*/
import { NextRequest, NextResponse } from 'next/server';
import { query } from '@/lib/db';
import { requireRole } from '@/lib/require-role';
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 q = (searchParams.get('q') || '').trim();
const favoritesOnly = searchParams.get('favorites_only') === 'true';
const limit = Math.min(Number(searchParams.get('limit') || 25), 100);
const conditions: string[] = [];
const params: unknown[] = [];
if (favoritesOnly) conditions.push(`d.is_favorite = TRUE`);
if (q) {
params.push(`%${q}%`);
conditions.push(`(d.label ILIKE $${params.length} OR d.current_version ILIKE $${params.length})`);
}
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
// Rank: favorites float to top, then trigram similarity to label+body, then newest.
params.push(q || ''); // $N: trigram source
const simParamIdx = params.length;
params.push(limit);
const limitParamIdx = params.length;
const { rows } = await query(
`SELECT
d.id,
d.session_id,
d.version_number,
d.label,
d.tone,
d.word_count,
d.paragraph_count,
d.current_version AS content,
d.is_favorite,
d.created_at,
CASE WHEN $${simParamIdx} = '' THEN 0
ELSE similarity(
COALESCE(d.label,'') || ' ' || COALESCE(d.current_version,''),
$${simParamIdx}
)
END AS sim
FROM email_analyzer_drafts d
${where}
ORDER BY d.is_favorite DESC,
sim DESC,
d.created_at DESC
LIMIT $${limitParamIdx}`,
params,
);
return NextResponse.json({ results: rows });
} catch (err) {
console.error('[email-analyzer/search] error:', (err as Error).message);
return NextResponse.json({ error: 'Search failed' }, { status: 500 });
}
}