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:
@@ -3,9 +3,10 @@ import postgres from 'postgres';
|
||||
// Read-only fingerprints: lets a deployment verify saved progress without logging it.
|
||||
if (!process.env.DB_URL) throw new Error('DB_URL is required');
|
||||
const client = postgres(process.env.DB_URL, { max: 1 });
|
||||
// All saved progress lives in schema `games` since the schema reorg.
|
||||
const tables = [
|
||||
'user_entries',
|
||||
'user_games',
|
||||
'catch_em_all_entries',
|
||||
'catch_em_all_games',
|
||||
'catch_em_all_settings',
|
||||
'pokemon_journeys',
|
||||
'pokemon_catches',
|
||||
@@ -20,11 +21,11 @@ const tables = [
|
||||
try {
|
||||
const result: Record<string, unknown> = {};
|
||||
for (const table of tables) {
|
||||
const [exists] = await client`select to_regclass(${'public.' + table}) is not null as present`;
|
||||
const [exists] = await client`select to_regclass(${'games.' + table}) is not null as present`;
|
||||
if (!exists.present) continue;
|
||||
const [row] = await client.unsafe(`select count(*)::int as rows,
|
||||
md5(coalesce(string_agg(record, E'\\n' order by record), '')) as fingerprint
|
||||
from (select row_to_json(t)::text as record from "${table}" t) s`);
|
||||
from (select row_to_json(t)::text as record from games."${table}" t) s`);
|
||||
result[table] = row;
|
||||
}
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
|
||||
@@ -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/');
|
||||
|
||||
@@ -8,11 +8,11 @@ try {
|
||||
await sql.begin(async (tx) => {
|
||||
for (const image of artwork) {
|
||||
const rows =
|
||||
await tx`update tracker_items i set icon=${image.path} from tracker_games g where i.game_id=g.id and g.slug=${image.game} and i.key=${image.key} returning i.id`;
|
||||
await tx`update ref.tracker_items i set icon=${image.path} from ref.tracker_games g where i.game_id=g.id and g.slug=${image.game} and i.key=${image.key} returning i.id`;
|
||||
if (rows.length !== 1) throw new Error(`Unknown artwork target: ${image.game}/${image.key}`);
|
||||
}
|
||||
for (const [source, path] of Object.entries(achievementArt)) {
|
||||
await tx`update tracker_items set icon=${path} where kind='achievement' and icon=${source}`;
|
||||
await tx`update ref.tracker_items set icon=${path} where kind='achievement' and icon=${source}`;
|
||||
}
|
||||
});
|
||||
console.log(
|
||||
|
||||
@@ -22,17 +22,17 @@ try {
|
||||
}[]
|
||||
][]) {
|
||||
const [game] =
|
||||
await tx`select id from tracker_games where slug=${slug} and franchise='call-of-duty'`;
|
||||
await tx`select id from ref.tracker_games where slug=${slug} and franchise='call-of-duty'`;
|
||||
if (!game) throw new Error(`Seed Call of Duty catalog first: ${slug}`);
|
||||
for (let offset = 0; offset < rows.length; offset += 500) {
|
||||
const batch = rows
|
||||
.slice(offset, offset + 500)
|
||||
.map(({ sourceUrl, ...r }) => ({ ...r, game_id: game.id, source_url: sourceUrl }));
|
||||
await tx`insert into tracker_challenges ${tx(batch)} on conflict (game_id,key) do update set name=excluded.name,category=excluded.category,requirement=excluded.requirement,reward=excluded.reward,icon=excluded.icon,target=excluded.target,source_url=excluded.source_url`;
|
||||
await tx`insert into ref.tracker_challenges ${tx(batch)} on conflict (game_id,key) do update set name=excluded.name,category=excluded.category,requirement=excluded.requirement,reward=excluded.reward,icon=excluded.icon,target=excluded.target,source_url=excluded.source_url`;
|
||||
}
|
||||
// Retire removed source definitions only when nobody has saved progress.
|
||||
if (rows.length)
|
||||
await tx`delete from tracker_challenges c where c.game_id=${game.id} and c.key not in ${tx(rows.map((r) => r.key))} and not exists (select 1 from tracker_challenge_progress p where p.challenge_id=c.id)`;
|
||||
await tx`delete from ref.tracker_challenges c where c.game_id=${game.id} and c.key not in ${tx(rows.map((r) => r.key))} and not exists (select 1 from games.tracker_challenge_progress p where p.challenge_id=c.id)`;
|
||||
console.log(`${slug}: ${rows.length} challenge rows`);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -9,10 +9,10 @@ const client = postgres(process.env.DB_URL, { max: 1 });
|
||||
try {
|
||||
await client.begin(async (tx) => {
|
||||
const species = new Map(
|
||||
(await tx`select id, number from pokemon`).map((r) => [r.number, r.id])
|
||||
(await tx`select id, number from ref.pokemon`).map((r) => [r.number, r.id])
|
||||
);
|
||||
const games = new Map(
|
||||
(await tx`select id, source_id from games`).map((r) => [r.source_id, r.id])
|
||||
(await tx`select id, source_id from ref.pokemon_games`).map((r) => [r.source_id, r.id])
|
||||
);
|
||||
const speciesId = (n: number) => {
|
||||
const id = species.get(n);
|
||||
@@ -25,10 +25,10 @@ try {
|
||||
return id;
|
||||
};
|
||||
for (const row of catalog.facts)
|
||||
await tx`insert into pokemon_research (pokemon_id,facts) values (${speciesId(row.number)},${tx.json(row.facts)}) on conflict (pokemon_id) do update set facts=excluded.facts`;
|
||||
await tx`insert into ref.pokemon_research (pokemon_id,facts) values (${speciesId(row.number)},${tx.json(row.facts)}) on conflict (pokemon_id) do update set facts=excluded.facts`;
|
||||
for (const row of catalog.games) {
|
||||
if (!games.has(row.sourceId)) continue;
|
||||
await tx`insert into pokemon_game_research (game_id,version_group,regions) values (${gameId(row.sourceId)},${row.versionGroup},${row.regions}) on conflict (game_id) do update set version_group=excluded.version_group,regions=excluded.regions`;
|
||||
await tx`insert into ref.pokemon_game_research (game_id,version_group,regions) values (${gameId(row.sourceId)},${row.versionGroup},${row.regions}) on conflict (game_id) do update set version_group=excluded.version_group,regions=excluded.regions`;
|
||||
}
|
||||
for (let offset = 0; offset < catalog.dexes.length; offset += 1000) {
|
||||
const batch = catalog.dexes
|
||||
@@ -42,10 +42,10 @@ try {
|
||||
number
|
||||
}));
|
||||
if (batch.length)
|
||||
await tx`insert into pokemon_dex_numbers ${tx(batch)} on conflict (pokemon_id,game_id,dex) do update set number=excluded.number,dex_name=excluded.dex_name`;
|
||||
await tx`insert into ref.pokemon_dex_numbers ${tx(batch)} on conflict (pokemon_id,game_id,dex) do update set number=excluded.number,dex_name=excluded.dex_name`;
|
||||
}
|
||||
for (const row of catalog.moves)
|
||||
await tx`insert into pokemon_moves ${tx(row)} on conflict (id) do update set name=excluded.name,type=excluded.type,category=excluded.category,power=excluded.power,accuracy=excluded.accuracy,pp=excluded.pp,priority=excluded.priority,target=excluded.target`;
|
||||
await tx`insert into ref.pokemon_moves ${tx(row)} on conflict (id) do update set name=excluded.name,type=excluded.type,category=excluded.category,power=excluded.power,accuracy=excluded.accuracy,pp=excluded.pp,priority=excluded.priority,target=excluded.target`;
|
||||
for (let offset = 0; offset < catalog.learnsets.length; offset += 500) {
|
||||
const batch = catalog.learnsets
|
||||
.slice(offset, offset + 500)
|
||||
@@ -54,7 +54,7 @@ try {
|
||||
version_group,
|
||||
moves: tx.json(moves)
|
||||
}));
|
||||
await tx`insert into pokemon_learnsets ${tx(batch)} on conflict (pokemon_id,version_group) do update set moves=excluded.moves`;
|
||||
await tx`insert into ref.pokemon_learnsets ${tx(batch)} on conflict (pokemon_id,version_group) do update set moves=excluded.moves`;
|
||||
}
|
||||
console.log(
|
||||
`Seeded ${catalog.facts.length} profiles, ${catalog.dexes.length} dex entries, ${catalog.moves.length} moves and ${catalog.learnsets.length} versioned learnsets`
|
||||
|
||||
@@ -6,7 +6,7 @@ if (!process.env.DB_URL) throw new Error('DB_URL is required');
|
||||
const sql = postgres(process.env.DB_URL, { max: 1 });
|
||||
try {
|
||||
const users =
|
||||
await sql`update "user" set role = ${role}, updated_at = now() where id = ${userId} returning id, role`;
|
||||
await sql`update auth."user" set role = ${role}, updated_at = now() where id = ${userId} returning id, role`;
|
||||
if (!users.length)
|
||||
throw new Error('User ID not found. Register first and copy the ID from your account page.');
|
||||
console.log(users[0]);
|
||||
|
||||
Reference in New Issue
Block a user