import postgres from 'postgres'; const expectedOrigin = process.argv[2]; if ( !expectedOrigin || !['https://stanrunge.dev', 'https://staging.stanrunge.dev'].includes(expectedOrigin) ) { throw new Error('Expected a known production or staging origin'); } if (!process.env.DB_URL) throw new Error('DB_URL is missing'); if (!process.env.BETTER_AUTH_SECRET || process.env.BETTER_AUTH_SECRET.length < 32) { throw new Error('BETTER_AUTH_SECRET must be at least 32 characters'); } if (process.env.ORIGIN !== expectedOrigin || process.env.BETTER_AUTH_URL !== expectedOrigin) { throw new Error('ORIGIN and BETTER_AUTH_URL must both match the public site origin'); } const client = postgres(process.env.DB_URL, { max: 1, connect_timeout: 10 }); try { await client`select 1`; // Schema layout after the reorg (auth/money/house/ref/games/cs/animaldex/site/meta/lists). const required = [ 'auth.user', 'auth.session', 'auth.account', 'auth.verification', 'money.accounts', 'money.transactions', 'money.recurring', 'money.forecasts', 'money.utility_settings', 'money.utility_readings', 'money.wishlist', 'money.insurances', 'money.projection_settings', 'house.listings', 'house.price_history', 'house.alert_rules', 'house.worker_runs', 'ref.pokemon', 'ref.pokemon_games', 'ref.pokemon_game_entries', 'ref.pokemon_locations', 'ref.pokemon_encounters', 'ref.pokemon_evolutions', 'ref.pokemon_research', 'ref.pokemon_dex_numbers', 'ref.pokemon_moves', 'ref.pokemon_learnsets', 'ref.pokemon_game_research', 'ref.tracker_games', 'ref.tracker_items', 'ref.tracker_challenges', 'games.catch_em_all_entries', 'games.catch_em_all_games', 'games.catch_em_all_settings', 'games.pokemon_journeys', 'games.pokemon_catches', 'games.pokemon_milestones', 'games.pokemon_seen', 'games.tracker_progress', 'games.tracker_ranks', 'games.tracker_weapon_ranks', 'games.tracker_challenge_progress', 'games.osu_entries', 'cs.items', 'cs.price_observations', 'cs.item_dimensions', 'cs.price_streams', 'animaldex.animals', 'animaldex.sightings', 'animaldex.sighting_photos', 'site.blog_articles', 'site.tasks', 'site.topics', 'site.task_topics', 'meta.tables', 'meta.columns' ]; const missing = await client<{ name: string }[]>` select name from unnest(${required}::text[]) as name where to_regclass(name) is null`; if (missing.length) throw new Error(`Schema reorg/migrations missing: ${missing.map((m) => m.name).join(', ')}`); const [schema] = await client` select (select count(*) from information_schema.columns where table_schema = 'auth' and table_name = 'user' and column_name = 'role') as user_role, (select count(*) from information_schema.columns where table_schema = 'money' and table_name = 'projection_settings' and column_name = 'include_recurring' and data_type = 'boolean') as include_recurring_bool, (select count(*) from information_schema.columns where table_schema = 'games' and table_name = 'osu_entries' and column_name in ('accuracy', 'pp', 'rate') and data_type = 'numeric') as osu_numeric, (select count(*) from meta.tables where kind = 'list') as lists, has_function_privilege('meta.create_list(text, text, text, boolean, jsonb, text, text, text, integer)', 'execute') as can_create_list, has_schema_privilege('money', 'create') as app_has_ddl `; if ( !Number(schema.user_role) || !Number(schema.include_recurring_bool) || Number(schema.osu_numeric) !== 3 || !Number(schema.lists) || !schema.can_create_list ) { throw new Error( 'The schema reorg has not been applied to this database (or grants are missing)' ); } if (schema.app_has_ddl) throw new Error('DB_URL must use the app role (personal_app), not the schema owner'); // Private health schema: the app role reads and writes it, nobody else (not even personal_ro). const [health] = await client` select count(*) filter (where has_table_privilege(c.oid, 'select, insert, update, delete')) as app_rw, count(*) filter ( where exists ( select 1 from aclexplode(c.relacl) a join pg_roles r on r.oid = a.grantee where r.rolname = 'personal_ro' ) ) as ro_read, count(*) as tables, has_schema_privilege('public', 'health', 'usage') as public_usage from pg_class c join pg_namespace n on n.oid = c.relnamespace where n.nspname = 'health' and c.relkind = 'r' `; if ( Number(health.tables) !== 7 || Number(health.app_rw) !== 7 || Number(health.ro_read) !== 0 || health.public_usage ) throw new Error(`health schema grants are wrong: ${JSON.stringify(health)}`); // Energy tracker reads the ned-collector tables (SELECT only). const [energy] = await client` select count(*) as hours, max(valid_from) as latest from energy.ned_utilization where granularity = 'Hour' and classification = 'Current' and valid_from > now() - interval '7 days' `; if (!Number(energy.hours)) console.warn('Warning: energy.ned_utilization has no hourly data from the last 7 days'); console.log( `health: ${health.tables} tables, app read/write, personal_ro none; energy: ${energy.hours} hourly rows, latest ${energy.latest ? new Date(energy.latest).toISOString() : 'none'}` ); const [catalog] = await client` select (select count(*) from ref.pokemon) as pokemon, (select count(*) from ref.pokemon_games where source_id is not null) as games, (select count(*) from ref.pokemon_game_entries) as entries, (select count(*) from ref.pokemon_encounters) as encounters, (select count(*) from ref.tracker_games where franchise = 'call-of-duty') as cod_games, (select count(*) from ref.pokemon_research) as pokemon_research, (select count(*) from ref.pokemon_moves) as pokemon_moves, (select count(*) from ref.pokemon_learnsets) as pokemon_learnsets, (select count(*) from ref.pokemon_dex_numbers) as pokemon_dex_numbers, (select count(*) from ref.tracker_challenges) as tracker_challenges `; if ( Number(catalog.pokemon) < 1025 || Number(catalog.games) < 53 || Number(catalog.entries) < 27267 || Number(catalog.encounters) < 60000 ) { throw new Error('Pokémon catalog has not been seeded in this database'); } if ( Number(catalog.pokemon_research) < 1025 || Number(catalog.pokemon_moves) < 937 || Number(catalog.pokemon_learnsets) < 11000 || Number(catalog.pokemon_dex_numbers) < 16000 || Number(catalog.tracker_challenges) < 3600 ) throw new Error( 'Run db:seed:research and db:seed:challenges before deploying the tracker expansion' ); if (Number(catalog.cod_games) < 7) { throw new Error('Call of Duty catalog has not been seeded in this database'); } const [bo2] = await client`select rules, (select count(*)::int from ref.tracker_items i where i.game_id=g.id and i.kind='weapon') as weapons, (select count(*)::int from ref.tracker_items i where i.game_id=g.id and i.kind='weapon' and i.icon is not null) as pictures from ref.tracker_games g where slug='black-ops-2'`; if ( !bo2 || bo2.rules.rankCaps?.weaponPrestige !== 2 || bo2.rules.rankCaps?.weaponLevelsByClass?.['Assault Rifles'] !== 17 || bo2.weapons < 40 || bo2.pictures < 39 ) { throw new Error('Seed the updated BO2 catalog and artwork before deploying'); } } finally { await client.end(); } const google = !!(process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET); const osu = !!(process.env.OSU_CLIENT_ID && process.env.OSU_CLIENT_SECRET); console.log( `Preflight passed for ${expectedOrigin}; Google ${google ? 'configured' : 'disabled'}, osu! ${osu ? 'configured' : 'disabled'}` );