import { type NextRequest, NextResponse } from 'next/server'; import { withAuth, buildItemFilter } from '@/lib/apiHelpers'; import pool from '@/lib/db'; export const GET = withAuth(async (req: NextRequest) => { const p = req.nextUrl.searchParams; const combinators = p.getAll('combinator'); const itemsWhitelist = p.getAll('item'); const itemsBlacklist = p.getAll('exclude'); const signalType = p.get('signal') ?? 'both'; const from = p.get('from'); const to = p.get('to'); const useRegex = p.get('regex') === 'true'; const orderBy = p.get('order_by') ?? 'value_asc'; const limitStr = p.get('limit'); const limit = limitStr ? parseInt(limitStr, 10) : null; const conditions: string[] = []; const values: unknown[] = []; const param = { current: 1 }; if (combinators.length > 0) { conditions.push(`combinator = ANY($${param.current++})`); values.push(combinators); } const wlClause = buildItemFilter(itemsWhitelist, useRegex, true, values, param); if (wlClause) conditions.push(wlClause); const blClause = buildItemFilter(itemsBlacklist, useRegex, false, values, param); if (blClause) conditions.push(blClause); if (from) { conditions.push(`real_time >= $${param.current++}`); values.push(new Date(from)); } if (to) { conditions.push(`real_time <= $${param.current++}`); values.push(new Date(to)); } const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''; const valueCol = signalType === 'red' ? 'red' : 'green'; const selectCols = signalType === 'green' ? 'real_time, game_tick, combinator, item_key, green' : signalType === 'red' ? 'real_time, game_tick, combinator, item_key, red' : 'real_time, game_tick, combinator, item_key, green, red'; if ((orderBy === 'delta_asc' || orderBy === 'delta_desc') && limit !== null) { const baseConditions: string[] = []; const baseValues: unknown[] = []; const baseParam = { current: 1 }; if (combinators.length > 0) { baseConditions.push(`combinator = ANY($${baseParam.current++})`); baseValues.push(combinators); } const wlBase = buildItemFilter(itemsWhitelist, useRegex, true, baseValues, baseParam); if (wlBase) baseConditions.push(wlBase); const blBase = buildItemFilter(itemsBlacklist, useRegex, false, baseValues, baseParam); if (blBase) baseConditions.push(blBase); const baseWhere = baseConditions.length > 0 ? `WHERE ${baseConditions.join(' AND ')}` : ''; const baseWhereAnd = baseConditions.length > 0 ? `AND ${baseConditions.join(' AND ')}` : ''; const deltaQuery = ` WITH snap_now AS ( SELECT DISTINCT ON (combinator, item_key) combinator, item_key, ${valueCol} AS val, real_time AS ref_time FROM signals ${baseWhere} ORDER BY combinator, item_key, real_time DESC ), snap_then AS ( SELECT DISTINCT ON (s.combinator, s.item_key) s.combinator, s.item_key, s.${valueCol} AS val FROM signals s JOIN snap_now n USING (combinator, item_key) WHERE s.real_time <= n.ref_time - INTERVAL '10 minutes' ${baseWhereAnd} ORDER BY s.combinator, s.item_key, s.real_time DESC ), deltas AS ( SELECT snap_now.combinator, snap_now.item_key, (snap_now.val - COALESCE(snap_then.val, snap_now.val)) AS delta FROM snap_now LEFT JOIN snap_then USING (combinator, item_key) ) SELECT combinator, item_key, delta FROM deltas ORDER BY delta ${orderBy === 'delta_asc' ? 'ASC' : 'DESC'} LIMIT $${baseParam.current} `; const deltaResult = await pool.query<{ combinator: string; item_key: string; delta: number; }>(deltaQuery, [...baseValues, limit]); const top = deltaResult.rows; if (top.length === 0) return NextResponse.json([]); const seriesConditions = top.map( (_, idx) => `(combinator = $${param.current + idx * 2} AND item_key = $${param.current + idx * 2 + 1})`, ); const fullWhere = `${where ? where + ' AND' : 'WHERE'} (${seriesConditions.join(' OR ')})`; const orderCase = top .map( (_, idx) => `WHEN combinator = $${param.current + idx * 2} AND item_key = $${param.current + idx * 2 + 1} THEN ${idx}`, ) .join(' '); const result = await pool.query( `SELECT ${selectCols} FROM signals ${fullWhere} ORDER BY CASE ${orderCase} ELSE ${top.length} END, real_time ASC`, [...values, ...top.flatMap((r) => [r.combinator, r.item_key])], ); return NextResponse.json(result.rows); } if ( (orderBy === 'value_asc' || orderBy === 'value_desc' || orderBy === 'abs_desc') && limit !== null ) { const latestVals = await pool.query<{ combinator: string; item_key: string; val: number; }>( `SELECT DISTINCT ON (combinator, item_key) combinator, item_key, ${valueCol} AS val FROM signals ${where} ORDER BY combinator, item_key, real_time DESC`, values, ); let sorted = latestVals.rows; if (orderBy === 'value_asc') sorted = [...sorted].sort((a, b) => a.val - b.val); if (orderBy === 'value_desc') sorted = [...sorted].sort((a, b) => b.val - a.val); if (orderBy === 'abs_desc') sorted = [...sorted].sort((a, b) => Math.abs(b.val) - Math.abs(a.val)); const top = sorted.slice(0, limit); if (top.length === 0) return NextResponse.json([]); const seriesConditions = top.map( (_, idx) => `(combinator = $${param.current + idx * 2} AND item_key = $${param.current + idx * 2 + 1})`, ); const fullWhere = `${where ? where + ' AND' : 'WHERE'} (${seriesConditions.join(' OR ')})`; const orderCase = top .map( (_, idx) => `WHEN combinator = $${param.current + idx * 2} AND item_key = $${param.current + idx * 2 + 1} THEN ${idx}`, ) .join(' '); const result = await pool.query( `SELECT ${selectCols} FROM signals ${fullWhere} ORDER BY CASE ${orderCase} ELSE ${top.length} END, real_time ASC`, [...values, ...top.flatMap((r) => [r.combinator, r.item_key])], ); return NextResponse.json(result.rows); } const rowLimit = orderBy === 'time' && limit ? `LIMIT ${limit}` : ''; const result = await pool.query( `SELECT ${selectCols} FROM signals ${where} ORDER BY real_time ASC ${rowLimit}`, values, ); return NextResponse.json(result.rows); });