From 50353952db0bf49c6262c3b990ed584cf61558e1 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Tue, 5 May 2026 01:29:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20add=20ensureSteamGame=20=E2=80=94=20uni?= =?UTF-8?q?fied=20stub-then-sync=20entry=20point?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/steam/sync.ts | 69 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/lib/steam/sync.ts b/lib/steam/sync.ts index 10fe8e8..621783a 100644 --- a/lib/steam/sync.ts +++ b/lib/steam/sync.ts @@ -287,4 +287,73 @@ export async function syncSteamGame( } return { success: false, error: errorMsg } } +} + +/** + * Ensure a Steam game exists in the database with full data. + * If the game doesn't exist, inserts a minimal stub then runs a full sync. + * If the game already exists, returns it without re-syncing. + * + * This is the SINGLE ENTRY POINT for creating new Steam game records. + * Use this instead of direct Steam API fetches + manual inserts. + */ +export async function ensureSteamGame( + steamAppId: number +): Promise<{ game: typeof games.$inferSelect | null; created: boolean; error?: string }> { + // 1. Check if game already exists + const [existing] = await db + .select() + .from(games) + .where(eq(games.steamAppId, steamAppId)) + .limit(1) + + if (existing) { + return { game: existing, created: false } + } + + // 2. Insert minimal stub + const [stub] = await db + .insert(games) + .values({ + steamAppId, + source: "steam", + title: `Steam App ${steamAppId}`, + storeUrl: `https://store.steampowered.com/app/${steamAppId}`, + syncStatus: "pending", + }) + .returning() + + // Handle race condition: if another request inserted first, UNIQUE constraint + // on steamAppId will throw. Catch and return the existing record. + if (!stub) { + const [raceWinner] = await db + .select() + .from(games) + .where(eq(games.steamAppId, steamAppId)) + .limit(1) + return { game: raceWinner ?? null, created: false } + } + + // 3. Run full sync (forceRetry=true to bypass staleness check on a brand-new record) + const syncResult = await syncSteamGame(steamAppId, { forceRetry: true }) + + if (!syncResult.success) { + // Sync failed — return the stub (it has error tracking fields populated by + // recordSyncFailure inside syncSteamGame). Re-fetch to get updated fields. + const [failedGame] = await db + .select() + .from(games) + .where(eq(games.steamAppId, steamAppId)) + .limit(1) + return { game: failedGame ?? null, created: true, error: syncResult.error } + } + + // 4. Return fully populated game + const [fullGame] = await db + .select() + .from(games) + .where(eq(games.steamAppId, steamAppId)) + .limit(1) + + return { game: fullGame ?? null, created: true } } \ No newline at end of file