app: lists on meta tables, schema-qualified SQL, typed columns

- lists read/write real lists.* tables via meta.tables/meta.columns and the
  SECURITY DEFINER meta functions; typed extra columns in the item forms
- old numeric list URLs 308-redirect to the slug
- include_recurring is boolean, osu accuracy/pp/rate numeric
- raw SQL qualified with the new schemas
This commit is contained in:
2026-10-23 22:03:42 +00:00
parent 64fb8df262
commit 4cfe6bad6c
25 changed files with 529 additions and 218 deletions

View File

@@ -85,7 +85,7 @@ async function itemId(
extraDimensions: Dimensions = {}
) {
const [row] = await sql<{ id: number }[]>`
INSERT INTO cs_market_items (market_hash_name, name, icon_url, item_type)
INSERT INTO cs.items (market_hash_name, name, icon_url, item_type)
VALUES (${name}, ${displayName}, ${iconUrl ?? null}, ${itemType ?? null})
ON CONFLICT (market_hash_name) DO UPDATE SET
name = EXCLUDED.name,
@@ -98,13 +98,13 @@ async function itemId(
for (const [key, value] of Object.entries(inferred)) {
if (!/^[a-z][a-z0-9_]*$/.test(key) || !value) continue;
await sql`
INSERT INTO cs_item_dimensions (item_id, key, value) VALUES (${row.id}, ${key}, ${value})
INSERT INTO cs.item_dimensions (item_id, key, value) VALUES (${row.id}, ${key}, ${value})
ON CONFLICT (item_id, key) DO NOTHING`;
}
for (const [key, value] of Object.entries(extraDimensions)) {
if (!/^[a-z][a-z0-9_]*$/.test(key) || !value) continue;
await sql`
INSERT INTO cs_item_dimensions (item_id, key, value) VALUES (${row.id}, ${key}, ${value})
INSERT INTO cs.item_dimensions (item_id, key, value) VALUES (${row.id}, ${key}, ${value})
ON CONFLICT (item_id, key) DO UPDATE SET value = EXCLUDED.value`;
}
return row.id;
@@ -116,7 +116,7 @@ async function saveObservation(row: Observation, knownItemId?: number) {
(await itemId(row.marketHashName, row.name, undefined, undefined, row.dimensions));
await ensureStream(row.source, row.currency, row.kind);
await sql`
INSERT INTO cs_price_observations
INSERT INTO cs.price_observations
(item_id, source, currency, observed_at, price_cents, volume, kind)
VALUES (${item}, ${row.source}, ${row.currency}, ${new Date(row.observedAt)},
${row.priceCents}, ${row.volume ?? null}, ${row.kind})
@@ -128,7 +128,7 @@ async function saveObservation(row: Observation, knownItemId?: number) {
async function ensureStream(source: string, currency: string, kind: string) {
const key = `${source}\0${currency}\0${kind}`;
if (knownStreams.has(key)) return;
await sql`INSERT INTO cs_price_streams (source, currency, kind)
await sql`INSERT INTO cs.price_streams (source, currency, kind)
VALUES (${source}, ${currency}, ${kind}) ON CONFLICT DO NOTHING`;
knownStreams.add(key);
}
@@ -150,7 +150,7 @@ async function saveHistoryRows(
kind: 'sale-median'
}));
for (let offset = 0; offset < rows.length; offset += 500) {
await sql`INSERT INTO cs_price_observations ${sql(rows.slice(offset, offset + 500))}
await sql`INSERT INTO cs.price_observations ${sql(rows.slice(offset, offset + 500))}
ON CONFLICT (item_id, source, currency, observed_at, kind) DO UPDATE SET
price_cents = EXCLUDED.price_cents, volume = EXCLUDED.volume`;
}
@@ -242,7 +242,7 @@ async function history() {
if (!process.env.STEAM_SESSIONID || !process.env.STEAM_LOGIN_SECURE)
throw new Error('STEAM_SESSIONID and STEAM_LOGIN_SECURE are required for Steam history');
const items = await sql<{ id: number; market_hash_name: string }[]>`
SELECT id, market_hash_name FROM cs_market_items ORDER BY id LIMIT ${limit} OFFSET ${start}`;
SELECT id, market_hash_name FROM cs.items ORDER BY id LIMIT ${limit} OFFSET ${start}`;
let done = 0;
for (const item of items) {
const url = new URL('https://steamcommunity.com/market/pricehistory/');