← back to Norma
app/api/email-analyzer/learnings/route.ts
147 lines
/**
* Cross-element learnings journal.
*
* GET /api/email-analyzer/learnings?element_type=subject&session_id=xxx
* POST /api/email-analyzer/learnings — { element_type, element_id, snippet, learning, tags, session_id, version_id }
* PATCH /api/email-analyzer/learnings — { id, learning?, tags? }
* DELETE /api/email-analyzer/learnings?id=xxx
*
* Use cases:
* - Note why a subject line worked / didn't
* - Capture a reusable rule ("short subjects beat long for fundraising")
* - Pin a rewrite instruction that consistently improves scores
*/
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 sp = new URL(request.url).searchParams;
const elementType = sp.get('element_type');
const sessionId = sp.get('session_id');
const conditions: string[] = [];
const params: unknown[] = [];
if (elementType) {
params.push(elementType);
conditions.push(`element_type = $${params.length}`);
}
if (sessionId) {
params.push(sessionId);
conditions.push(`session_id = $${params.length}`);
}
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
const { rows } = await query(
`SELECT id, session_id, version_id, element_type, element_id,
snippet, learning, tags, created_at
FROM email_analyzer_learnings
${where}
ORDER BY created_at DESC
LIMIT 200`,
params,
);
return NextResponse.json({ learnings: rows });
} catch (err) {
console.error('[learnings] GET error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to load learnings' }, { status: 500 });
}
}
export async function POST(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
try {
const body = await request.json();
const {
session_id,
version_id,
element_type,
element_id,
snippet,
learning,
tags,
} = body;
if (!element_type || !learning) {
return NextResponse.json({ error: 'element_type and learning are required' }, { status: 400 });
}
const { rows } = await query(
`INSERT INTO email_analyzer_learnings
(session_id, version_id, element_type, element_id, snippet, learning, tags)
VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *`,
[
session_id || null,
version_id || null,
element_type,
element_id || null,
snippet || null,
learning,
Array.isArray(tags) ? tags.slice(0, 20) : [],
],
);
return NextResponse.json({ learning: rows[0] }, { status: 201 });
} catch (err) {
console.error('[learnings] POST error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to save learning' }, { status: 500 });
}
}
export async function PATCH(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
try {
const { id, learning, tags } = await request.json();
if (!id) return NextResponse.json({ error: 'id required' }, { status: 400 });
const sets: string[] = [];
const params: unknown[] = [];
if (learning !== undefined) {
params.push(learning);
sets.push(`learning = $${params.length}`);
}
if (tags !== undefined) {
params.push(Array.isArray(tags) ? tags.slice(0, 20) : []);
sets.push(`tags = $${params.length}`);
}
if (sets.length === 0) {
return NextResponse.json({ error: 'nothing to update' }, { status: 400 });
}
params.push(id);
const { rows, rowCount } = await query(
`UPDATE email_analyzer_learnings SET ${sets.join(', ')}
WHERE id = $${params.length} RETURNING *`,
params,
);
if (!rowCount) return NextResponse.json({ error: 'not found' }, { status: 404 });
return NextResponse.json({ learning: rows[0] });
} catch (err) {
console.error('[learnings] PATCH error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to update' }, { status: 500 });
}
}
export async function DELETE(request: NextRequest) {
const auth = requireRole(request, 'admin', 'staff');
if (auth instanceof NextResponse) return auth;
try {
const id = new URL(request.url).searchParams.get('id');
if (!id) return NextResponse.json({ error: 'id required' }, { status: 400 });
const { rowCount } = await query(
`DELETE FROM email_analyzer_learnings WHERE id = $1`,
[id],
);
if (!rowCount) return NextResponse.json({ error: 'not found' }, { status: 404 });
return NextResponse.json({ deleted: true, id });
} catch (err) {
console.error('[learnings] DELETE error:', (err as Error).message);
return NextResponse.json({ error: 'Failed to delete' }, { status: 500 });
}
}