Files
factorio-signal-exporter/web/app/api/signals/route.ts
Caesar2011 20ed6ee9fb Initial web
2026-05-17 19:55:53 +02:00

191 lines
7.2 KiB
TypeScript

import { NextRequest, NextResponse } from 'next/server';
import pool from '@/lib/db';
import { withAuth } from '@/lib/apiHelpers';
import { getServerLocaleMap } from '@/lib/localeServer';
import { matchKeys } from '@/lib/localization';
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') ?? 'time';
const limit = p.get('limit') ? parseInt(p.get('limit')!, 10) : null;
const conditions: string[] = [];
const values: unknown[] = [];
let i = 1;
if (combinators.length > 0) {
conditions.push(`combinator = ANY($${i++})`);
values.push(combinators);
}
if (itemsWhitelist.length > 0) {
if (useRegex) {
const localeMap = getServerLocaleMap();
// Each pattern is expanded to matching keys (tested against key AND localized name).
// Union all patterns — if a pattern matches nothing, it contributes no keys.
const keys = [...new Set(itemsWhitelist.flatMap(p => matchKeys(p, localeMap)))];
if (keys.length === 0) return NextResponse.json([]);
conditions.push(`item_key = ANY($${i++})`);
values.push(keys);
} else {
conditions.push(`item_key = ANY($${i++})`);
values.push(itemsWhitelist);
}
}
if (itemsBlacklist.length > 0) {
if (useRegex) {
const localeMap = getServerLocaleMap();
const keys = [...new Set(itemsBlacklist.flatMap(p => matchKeys(p, localeMap)))];
// If blacklist pattern matches nothing, nothing to exclude — skip condition
if (keys.length > 0) {
conditions.push(`item_key != ALL($${i++})`);
values.push(keys);
}
} else {
conditions.push(`item_key != ALL($${i++})`);
values.push(itemsBlacklist);
}
}
if (from) { conditions.push(`real_time >= $${i++}`); values.push(new Date(from)); }
if (to) { conditions.push(`real_time <= $${i++}`); 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[] = [];
let j = 1;
if (combinators.length > 0) {
baseConditions.push(`combinator = ANY($${j++})`);
baseValues.push(combinators);
}
if (itemsWhitelist.length > 0) {
if (useRegex) {
const localeMap = getServerLocaleMap();
const keys = [...new Set(itemsWhitelist.flatMap(p => matchKeys(p, localeMap)))];
if (keys.length === 0) return NextResponse.json([]);
baseConditions.push(`item_key = ANY($${j++})`);
baseValues.push(keys);
} else {
baseConditions.push(`item_key = ANY($${j++})`);
baseValues.push(itemsWhitelist);
}
}
if (itemsBlacklist.length > 0) {
if (useRegex) {
const localeMap = getServerLocaleMap();
const keys = [...new Set(itemsBlacklist.flatMap(p => matchKeys(p, localeMap)))];
if (keys.length > 0) {
baseConditions.push(`item_key != ALL($${j++})`);
baseValues.push(keys);
}
} else {
baseConditions.push(`item_key != ALL($${j++})`);
baseValues.push(itemsBlacklist);
}
}
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 $${j}
`;
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 = $${i + idx * 2} AND item_key = $${i + idx * 2 + 1})`,
);
const fullWhere = `${where ? where + ' AND' : 'WHERE'} (${seriesConditions.join(' OR ')})`;
const result = await pool.query(
`SELECT ${selectCols} FROM signals ${fullWhere} ORDER BY 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 = $${i + idx * 2} AND item_key = $${i + idx * 2 + 1})`,
);
const fullWhere = `${where ? where + ' AND' : 'WHERE'} (${seriesConditions.join(' OR ')})`;
const result = await pool.query(
`SELECT ${selectCols} FROM signals ${fullWhere} ORDER BY 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);
});