All checks were successful
Deploy production / deploy (push) Successful in 1m18s
- cs.skins catalog (weapons, knives, gloves, stickers, cases, agents, charms, …) refreshed weekly - every market config: wears in float range, StatTrak, Souvenir, Doppler/Gamma phases - cs.daily_prices (one row per config/stream/day, idempotent) from Skinport, Steam, CSFloat, Buff163 - cs.price_windows for 24h/7d/30d/90d sale aggregates, cs.fx_rates (ECB) - scripts/cs-prices.ts daily|catalog|prices; legacy ingest fixed for the new unique index - skin browser + /projects/cs-prices/[slug] with variant/wear/phase switcher and real charts - charts show points for short histories and pad sparse time axes
70 lines
3.3 KiB
TypeScript
70 lines
3.3 KiB
TypeScript
import { db } from '#lib/server/db/index.js';
|
|
import { sql } from 'drizzle-orm';
|
|
import type { RequestHandler } from './$types';
|
|
|
|
const maxSeries = 1000;
|
|
const maxDays = 365 * 20;
|
|
|
|
// Compare explorer: daily prices (cs.daily_prices) for up to 1000 configs or groups, bucketed per
|
|
// day, or per week for ranges over 400 days. Group lines average each config equally per bucket.
|
|
export const POST: RequestHandler = async ({ request }) => {
|
|
const body = (await request.json().catch(() => ({}))) as Record<string, unknown>;
|
|
const dimension = typeof body.dimension === 'string' ? body.dimension : 'item';
|
|
const kind = typeof body.kind === 'string' ? body.kind : 'lowest-listing';
|
|
const source = typeof body.source === 'string' ? body.source : 'skinport';
|
|
const currency = typeof body.currency === 'string' ? body.currency : 'EUR';
|
|
const ids = Array.isArray(body.ids) ? body.ids : [];
|
|
const values = Array.isArray(body.values) ? body.values : [];
|
|
if (
|
|
!/^[a-z][a-z0-9_]*$/.test(dimension) ||
|
|
!/^[a-z0-9][a-z0-9-]{0,79}$/.test(source) ||
|
|
!/^[a-z0-9][a-z0-9-]{0,79}$/.test(kind) ||
|
|
!/^[A-Z]{3}$/.test(currency) ||
|
|
ids.length > maxSeries ||
|
|
values.length > maxSeries ||
|
|
ids.some((id) => !Number.isSafeInteger(id) || id <= 0) ||
|
|
values.some((value) => typeof value !== 'string' || value.length > 200)
|
|
)
|
|
return Response.json({ error: 'Invalid selection' }, { status: 400 });
|
|
if (dimension === 'item' ? !ids.length : !values.length)
|
|
return Response.json({ points: [], bucketSeconds: 0 });
|
|
const to = new Date(typeof body.to === 'string' ? body.to : Date.now());
|
|
const from = new Date(typeof body.from === 'string' ? body.from : to.getTime() - 365 * 86400_000);
|
|
if (Number.isNaN(+from) || Number.isNaN(+to) || from >= to || +to - +from > maxDays * 86400_000)
|
|
return Response.json({ error: 'Invalid date range' }, { status: 400 });
|
|
const weekly = +to - +from > 400 * 86400_000;
|
|
const seconds = weekly ? 7 * 86400 : 86400;
|
|
const selection =
|
|
dimension === 'item'
|
|
? sql`JOIN cs.items i ON i.id = p.item_id AND i.id IN (${sql.join(
|
|
ids.map((id) => sql`${id}`),
|
|
sql`,`
|
|
)})`
|
|
: sql`JOIN cs.item_dimensions d ON d.item_id = p.item_id AND d.key = ${dimension}
|
|
AND d.value IN (${sql.join(
|
|
values.map((value) => sql`${value}`),
|
|
sql`,`
|
|
)})`;
|
|
const seriesKey = dimension === 'item' ? sql`i.id::text` : sql`d.value`;
|
|
const bucket = weekly ? sql`date_trunc('week', p.day)::date` : sql`p.day`;
|
|
const rows = await db.execute(sql`
|
|
WITH stream AS (
|
|
SELECT id FROM cs.price_streams
|
|
WHERE source = ${source} AND currency = ${currency} AND kind = ${kind}
|
|
), per_item AS (
|
|
SELECT ${seriesKey} AS series_key, p.item_id, ${bucket} AS bucket,
|
|
avg(p.price_cents) AS price_cents, sum(p.volume) AS volume,
|
|
min(p.price_cents) AS min_cents, max(p.price_cents) AS max_cents
|
|
FROM cs.daily_prices p ${selection}
|
|
WHERE p.stream_id = (SELECT id FROM stream)
|
|
AND p.day >= ${from.toISOString().slice(0, 10)}::date
|
|
AND p.day <= ${to.toISOString().slice(0, 10)}::date
|
|
GROUP BY 1, 2, 3
|
|
)
|
|
SELECT series_key, bucket::text AS bucket, round(avg(price_cents))::int AS price_cents,
|
|
count(*)::int AS items, sum(volume)::bigint AS volume,
|
|
min(min_cents)::int AS min_cents, max(max_cents)::int AS max_cents
|
|
FROM per_item GROUP BY 1, 2 ORDER BY 1, 2`);
|
|
return Response.json({ bucketSeconds: seconds, currency, source, kind, points: rows });
|
|
};
|