ui changes and cs prices
All checks were successful
Test and deploy staging / staging (push) Successful in 11m52s
All checks were successful
Test and deploy staging / staging (push) Successful in 11m52s
This commit is contained in:
68
src/routes/projects/cs-prices/api/series/+server.ts
Normal file
68
src/routes/projects/cs-prices/api/series/+server.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import { db } from '$lib/server/db';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
const maxSeries = 1000;
|
||||
const maxDays = 365 * 20;
|
||||
|
||||
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 : 'sale-median';
|
||||
const source = typeof body.source === 'string' ? body.source : 'steam-market';
|
||||
const currency = typeof body.currency === 'string' ? body.currency : 'USD';
|
||||
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 json({ error: 'Invalid selection' }, { status: 400 });
|
||||
if (dimension === 'item' ? !ids.length : !values.length)
|
||||
return json({ series: [], 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 json({ error: 'Invalid date range' }, { status: 400 });
|
||||
// Aim for at most 240 buckets per series. Raw observations remain untouched.
|
||||
const seconds = Math.max(60, Math.ceil((+to - +from) / 1000 / 240 / 60) * 60);
|
||||
const selection =
|
||||
dimension === 'item'
|
||||
? sql`JOIN cs_market_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 rows = await db.execute(sql`
|
||||
WITH per_item AS (
|
||||
SELECT ${seriesKey} AS series_key, p.item_id,
|
||||
date_bin(make_interval(secs => ${seconds}), p.observed_at,
|
||||
TIMESTAMPTZ '1970-01-01') 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_price_observations p ${selection}
|
||||
WHERE p.source = ${source} AND p.currency = ${currency} AND p.kind = ${kind}
|
||||
AND p.observed_at >= ${from.toISOString()}::timestamptz
|
||||
AND p.observed_at <= ${to.toISOString()}::timestamptz
|
||||
GROUP BY 1, 2, 3
|
||||
)
|
||||
SELECT series_key, 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 json({ bucketSeconds: seconds, currency, source, kind, points: rows });
|
||||
};
|
||||
Reference in New Issue
Block a user