Skip to content

Conversation

@lfelipeserrano
Copy link

@lfelipeserrano lfelipeserrano commented Sep 1, 2025

Description

The initial setup to import listing from Ryan retro listing, this only covers Switch games with all the devices supported on the page, if this helps, I can continue working on the other platforms

Fixes # (issue)

Type of change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Refactor
  • [ x] Other (please describe): Listing migrations

How Has This Been Tested?

Tested on local environments, this use the CSV added, first it should run the game import with:
npx tsx scripts/import-csv-games.ts
The we can check the games listed on the app.
Once that's is completed, we can import the listing for each game with:
npx tsx scripts/import-csv-listings.ts

  • [ x] Local build
  • Lint
  • Typecheck
  • Unit tests
  • Manual testing

Screenshots (if applicable)

image image

Checklist

  • [x ] My code follows the style guidelines of this project
  • [x ] I have performed a self-review of my code
  • I have made corresponding changes to the documentation
  • [ x] I have checked that all checks (lint, typecheck, test) pass

Summary by CodeRabbit

  • New Features

    • Added CLI tools to bulk import games and listings from CSV.
    • Includes robust CSV parsing (supports quoted fields), required-field validation, deduplication, and mapping to existing systems/devices/emulators.
    • Performs pre-checks to avoid duplicates, assigns submitter/approver, logs progress, and outputs final import summaries (successes, duplicates, failures).
  • Chores

    • Introduced seeding utilities with improved error handling and safe resource cleanup to support large data migrations.

@vercel
Copy link

vercel bot commented Sep 1, 2025

@lfelipeserrano is attempting to deploy a commit to the Producdevity's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Sep 1, 2025

Walkthrough

Adds two CSV-based Prisma seeders for games and listings, plus two TSX CLI scripts to run them. Seeders parse and validate CSVs, deduplicate rows, resolve related DB entities, assign submitters/approvers and timestamps, insert approved records, and log progress and outcomes.

Changes

Cohort / File(s) Summary
CSV Seeders
prisma/seeders/csvGamesSeeder.ts, prisma/seeders/csvListingsSeeder.ts
New seeders that read specific CSV files, parse with a custom quoted-field parser, validate required headers, deduplicate, resolve DB relations (users, systems, games, devices, emulators, performance), assign submitter/approver and timestamps, create approved records, and log detailed metrics.
Import Scripts (CLI)
scripts/import-csv-games.ts, scripts/import-csv-listings.ts
New executable TSX scripts that instantiate PrismaClient, invoke respective seeders, log start/success, handle errors with exit codes, and ensure Prisma disconnection.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User as CLI User
  participant Script as import-csv-games.ts
  participant Seeder as csvGamesSeeder
  participant FS as FileSystem
  participant DB as Prisma / DB

  User->>Script: Execute
  Script->>Seeder: csvGamesSeeder(prisma)
  Seeder->>FS: Read rp5_flip2_odin2_switch_games.csv
  Seeder->>Seeder: Parse CSV (quoted fields), validate headers
  Seeder->>Seeder: Deduplicate rows
  Seeder->>DB: Fetch systems, users/admins
  loop Batch existence checks
    Seeder->>DB: Find existing games
  end
  loop For each new game
    Seeder->>Seeder: Assign submitter/approver, timestamps
    Seeder->>DB: Create game (status=APPROVED)
    DB-->>Seeder: Result
  end
  Seeder-->>Script: Summary (new/imported/duplicates/failures)
  Script-->>User: Log completion
Loading
sequenceDiagram
  autonumber
  actor User as CLI User
  participant Script as import-csv-listings.ts
  participant Seeder as csvListingsSeeder
  participant FS as FileSystem
  participant DB as Prisma / DB

  User->>Script: Execute
  Script->>Seeder: csvListingsSeeder(prisma)
  Seeder->>FS: Read rp5_flip2_odin2_switch_listing.csv
  Seeder->>Seeder: Parse CSV (quoted fields), validate headers
  Seeder->>Seeder: Deduplicate rows
  par Resolve relations
    Seeder->>DB: Find games by title
    Seeder->>DB: Load emulators & performance scales
    Seeder->>DB: Load devices (full name/model)
    Seeder->>DB: Get author user and admin approvers
  end
  loop For each listing
    alt All relations resolved
      Seeder->>Seeder: Build notes, assign author/approver, timestamps
      Seeder->>DB: Create listing (status=APPROVED)
      DB-->>Seeder: Result
    else Missing relation
      Seeder->>Seeder: Count and skip failure
    end
  end
  Seeder-->>Script: Summary (totals, successes, failures, skipped)
  Script-->>User: Log completion
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

I nuzzle the rows where the commas hide,
Hop-hop through quotes on a parsing ride.
With paws on Prisma, I seed the field,
Approvals sprout—fine harvest yield.
Listings and games in tidy arrays—
Carrots up! For crisp import days. 🥕✨

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (13)
prisma/seeders/csvGamesSeeder.ts (5)

119-124: Precompute reverse system map for fast keying

Needed by the batched lookup to reconstruct the normalized key.

-  const systems = await prisma.system.findMany()
-  const systemMap = new Map(systems.map((system) => [system.name.toLowerCase(), system]))
+  const systems = await prisma.system.findMany()
+  const systemMap = new Map(systems.map((system) => [system.name.toLowerCase(), system]))
+  const systemIdToNameLower = new Map(systems.map((s) => [s.id, s.name.toLowerCase()]))

14-19: Allow overriding CSV path

Helpful for local runs and CI without moving files.

-  const csvPath = path.join(__dirname, 'data', 'rp5_flip2_odin2_switch_games.csv')
+  const csvPath =
+    process.env.CSV_GAMES_PATH ??
+    path.join(__dirname, 'data', 'rp5_flip2_odin2_switch_games.csv')

21-23: Handle BOM and CRLF line endings

Prevents header misreads on Windows and with BOM-encoded CSVs.

-  const csvContent = fs.readFileSync(csvPath, 'utf-8')
-  const lines = csvContent.split('\n').filter((line) => line.trim())
+  const csvContent = fs.readFileSync(csvPath, 'utf-8').replace(/^\uFEFF/, '')
+  const lines = csvContent.split(/\r?\n/).filter((line) => line.trim())

74-77: Fix required-headers message

Message says “systemName” but you check “systemname”.

-      `❌ Missing required headers: ${missingHeaders.join(', ')}. Required headers: title, systemName`,
+      `❌ Missing required headers: ${missingHeaders.join(', ')}. Required headers: title, systemname`,

29-64: Consider a CSV library instead of a custom parser

Edge cases (embedded commas/newlines) are easy to miss. csv-parse or fast-csv would reduce maintenance.

scripts/import-csv-games.ts (1)

22-25: Remove double top-level exit/logging

main() already handles errors and sets exitCode.

-main().catch((error) => {
-  console.error('💥 Unexpected error:', error)
-  process.exit(1)
-})
+main()
scripts/import-csv-listings.ts (1)

22-25: Remove double top-level exit/logging

Let main() own error handling.

-main().catch((error) => {
-  console.error('💥 Unexpected error:', error)
-  process.exit(1)
-})
+main()
prisma/seeders/csvListingsSeeder.ts (6)

93-103: Don’t drop rows with missing trailing fields; pad instead

Skipping when values.length < headers.length discards valid rows with empty tail fields. Pad to header length and keep the row.

-    if (values.length < headers.length) continue
-
-    const listing: ListingData = {
-      game: values[headers.indexOf('game')] || '',
-      performance: values[headers.indexOf('performance')] || '',
-      drive: values[headers.indexOf('drive')] || '',
-      emulator: values[headers.indexOf('emulator')] || '',
-      update: values[headers.indexOf('update')] || '',
-      notes: values[headers.indexOf('notes')] || '',
-      device: values[headers.indexOf('device')] || '',
-    }
+    const row = Array.from({ length: headers.length }, (_, idx) => values[idx] ?? '')
+    const idx = (h: string) => headers.indexOf(h)
+    const listing: ListingData = {
+      game: row[idx('game')] || '',
+      performance: row[idx('performance')] || '',
+      drive: row[idx('drive')] || '',
+      emulator: row[idx('emulator')] || '',
+      update: row[idx('update')] || '',
+      notes: row[idx('notes')] || '',
+      device: row[idx('device')] || '',
+    }

34-69: CSV parsing: prefer a proven parser or extend for multi-line fields

Manual parser won’t handle embedded newlines within quoted cells and is easy to get wrong. Consider csv-parse (streaming) or papaparse. If keeping manual, document limitations and add tests covering edge cases (escaped quotes, empty last columns, CRLF, embedded commas/newlines).

Would you like a drop-in csv-parse example wired to this seeder?


105-119: CSV-level dedupe key may miss same device under different naming

Dedup uses the raw CSV device string; the same device might appear as “ASUS ROG Ally” vs “ROG Ally”. Consider normalizing device names (trim, collapse spaces, remove brand if present) or dedupe after resolving device.id.

-      const listingKey = `${listing.game.toLowerCase()}|||${listing.performance.toLowerCase()}|||${listing.emulator.toLowerCase()}|||${listing.device.toLowerCase()}`
+      const norm = (s: string) => s.toLowerCase().replace(/\s+/g, ' ').trim()
+      const listingKey = `${norm(listing.game)}|||${norm(listing.performance)}|||${norm(listing.emulator)}|||${norm(listing.device)}`

Or move dedupe after device lookup and key by IDs.


131-146: Precondition checks for referenced data

You only guard for empty devices. Add early exits for empty games/emulators/performance scales to avoid noisy per-row warnings.

   const games = await prisma.game.findMany({
     include: { system: true },
   })
@@
   const performanceScales = await prisma.performanceScale.findMany()
@@
-  if (devices.length === 0) {
+  if (games.length === 0) {
+    console.error('❌ No games found. Seed games first.')
+    return
+  }
+  if (emulators.length === 0) {
+    console.error('❌ No emulators found. Seed emulators first.')
+    return
+  }
+  if (performanceScales.length === 0) {
+    console.error('❌ No performance scales found. Seed performance scales first.')
+    return
+  }
+  if (devices.length === 0) {
     console.error('❌ No devices found in database. Please seed devices first.')
     return
   }

Also applies to: 155-159


131-139: Minor: drop unused includes

include: { system: true } on games isn’t used; removing it avoids unnecessary payload.

-  const games = await prisma.game.findMany({
-    include: { system: true },
-  })
+  const games = await prisma.game.findMany()

249-266: Align timestamps with schema or remove unused ones

The seeder generates submittedAt and approvedAt but the Listing model only has createdAt (defaulted to now) and processedAt. Either override createdAt with submittedAt and keep processedAt = approvedAt, or drop submittedAt/approvedAt and only back-date processedAt.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 25fc331 and 0dd4ed7.

⛔ Files ignored due to path filters (2)
  • prisma/seeders/data/rp5_flip2_odin2_switch_games.csv is excluded by !**/*.csv
  • prisma/seeders/data/rp5_flip2_odin2_switch_listing.csv is excluded by !**/*.csv
📒 Files selected for processing (4)
  • prisma/seeders/csvGamesSeeder.ts (1 hunks)
  • prisma/seeders/csvListingsSeeder.ts (1 hunks)
  • scripts/import-csv-games.ts (1 hunks)
  • scripts/import-csv-listings.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
scripts/import-csv-listings.ts (1)
src/test/orm-mock.ts (1)
  • PrismaClient (50-85)
scripts/import-csv-games.ts (1)
src/test/orm-mock.ts (1)
  • PrismaClient (50-85)
prisma/seeders/csvGamesSeeder.ts (1)
src/test/orm-mock.ts (1)
  • PrismaClient (50-85)
prisma/seeders/csvListingsSeeder.ts (1)
src/test/orm-mock.ts (1)
  • PrismaClient (50-85)

Comment on lines +166 to +190
// Check in batches to avoid overwhelming the database
const batchSize = 50
for (let i = 0; i < games.length; i += batchSize) {
const batch = games.slice(i, i + batchSize)

for (const game of batch) {
const system = systemMap.get(game.systemName.toLowerCase())
if (!system) continue

const existing = await prisma.game.findFirst({
where: {
title: {
equals: game.title,
mode: 'insensitive',
},
systemId: system.id,
},
})

if (existing) {
const gameKey = `${game.title.toLowerCase()}|||${game.systemName.toLowerCase()}`
existingGames.add(gameKey)
}
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Eliminate N+1 lookups: batch with a single findMany per chunk

Current code issues 1 query per game. Replace with chunked OR queries to cut DB roundtrips dramatically.

Apply:

-  // Check in batches to avoid overwhelming the database
-  const batchSize = 50
-  for (let i = 0; i < games.length; i += batchSize) {
-    const batch = games.slice(i, i + batchSize)
-
-    for (const game of batch) {
-      const system = systemMap.get(game.systemName.toLowerCase())
-      if (!system) continue
-
-      const existing = await prisma.game.findFirst({
-        where: {
-          title: {
-            equals: game.title,
-            mode: 'insensitive',
-          },
-          systemId: system.id,
-        },
-      })
-
-      if (existing) {
-        const gameKey = `${game.title.toLowerCase()}|||${game.systemName.toLowerCase()}`
-        existingGames.add(gameKey)
-      }
-    }
-  }
+  // Batch OR queries to avoid N+1 lookups
+  const pairs = Array.from(seenGames).map((key) => {
+    const [titleLower, systemNameLower] = key.split('|||')
+    const system = systemMap.get(systemNameLower)
+    return system ? { titleLower, systemId: system.id, key } : null
+  }).filter(Boolean) as Array<{ titleLower: string; systemId: string; key: string }>
+
+  const orChunkSize = 250
+  for (let i = 0; i < pairs.length; i += orChunkSize) {
+    const chunk = pairs.slice(i, i + orChunkSize)
+    const found = await prisma.game.findMany({
+      where: {
+        OR: chunk.map((p) => ({
+          AND: [
+            { systemId: p.systemId },
+            { title: { equals: p.titleLower, mode: 'insensitive' } },
+          ],
+        })),
+      },
+      select: { title: true, systemId: true },
+    })
+    for (const r of found) {
+      const key = `${r.title.toLowerCase()}|||${systemIdToNameLower.get(r.systemId) ?? ''}`
+      existingGames.add(key)
+    }
+  }

Note: relies on systemIdToNameLower added in a nearby change (see next comment).

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In prisma/seeders/csvGamesSeeder.ts around lines 166 to 190, the code currently
issues a findFirst per game (N+1 queries); replace the inner per-game lookup
with a single prisma.game.findMany per batch: build an array of OR conditions
for the batch where each condition matches title equals (mode: 'insensitive')
and the corresponding systemId (use systemIdToNameLower mapping already added),
call findMany once for the batch, then iterate the returned existing games to
populate existingGames using the same lowercase title|||systemName key; keep the
same batchSize logic and skip games whose system lookup fails before building
the OR list.

Comment on lines +216 to +251
for (const game of newGames) {
const system = systemMap.get(game.systemName.toLowerCase())
if (!system) {
console.warn(`⚠️ System "${game.systemName}" not found, skipping game "${game.title}"`)
systemNotFoundCount++
continue
}

const submitter = getRandomElement(seededUsers)
const approver = getRandomElement(adminUsers)
const submittedAt = getRandomPastDate(30) // Recent submissions
const approvedAt = new Date(submittedAt.getTime() + Math.random() * 7 * 24 * 60 * 60 * 1000) // Approved within 7 days

try {
await prisma.game.create({
data: {
title: game.title,
systemId: system.id,
status: ApprovalStatus.APPROVED, // Auto-approve CSV imports
submittedBy: submitter?.id ?? null,
submittedAt: submittedAt,
approvedBy: approver?.id ?? null,
approvedAt: approvedAt,
},
})

successCount++

if (successCount % 100 === 0) {
console.log(`📈 Progress: ${successCount}/${newGames.length} games imported`)
}
} catch (error) {
console.error(`❌ Failed to create game "${game.title}" for ${game.systemName}:`, error)
failCount++
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Bulk insert with createMany to reduce latency

Insert row-by-row is slow and noisy; createMany drastically speeds this up.

-  for (const game of newGames) {
-    const system = systemMap.get(game.systemName.toLowerCase())
-    if (!system) {
-      console.warn(`⚠️ System "${game.systemName}" not found, skipping game "${game.title}"`)
-      systemNotFoundCount++
-      continue
-    }
-
-    const submitter = getRandomElement(seededUsers)
-    const approver = getRandomElement(adminUsers)
-    const submittedAt = getRandomPastDate(30) // Recent submissions
-    const approvedAt = new Date(submittedAt.getTime() + Math.random() * 7 * 24 * 60 * 60 * 1000) // Approved within 7 days
-
-    try {
-      await prisma.game.create({
-        data: {
-          title: game.title,
-          systemId: system.id,
-          status: ApprovalStatus.APPROVED, // Auto-approve CSV imports
-          submittedBy: submitter?.id ?? null,
-          submittedAt: submittedAt,
-          approvedBy: approver?.id ?? null,
-          approvedAt: approvedAt,
-        },
-      })
-
-      successCount++
-
-      if (successCount % 100 === 0) {
-        console.log(`📈 Progress: ${successCount}/${newGames.length} games imported`)
-      }
-    } catch (error) {
-      console.error(`❌ Failed to create game "${game.title}" for ${game.systemName}:`, error)
-      failCount++
-    }
-  }
+  const rows = []
+  for (const game of newGames) {
+    const system = systemMap.get(game.systemName.toLowerCase())
+    if (!system) {
+      console.warn(`⚠️ System "${game.systemName}" not found, skipping game "${game.title}"`)
+      systemNotFoundCount++
+      continue
+    }
+    const submitter = getRandomElement(seededUsers)
+    const approver = getRandomElement(adminUsers)
+    const submittedAt = getRandomPastDate(30)
+    const approvedAt = new Date(submittedAt.getTime() + Math.random() * 7 * 24 * 60 * 60 * 1000)
+    rows.push({
+      title: game.title,
+      systemId: system.id,
+      status: ApprovalStatus.APPROVED,
+      submittedBy: submitter?.id ?? null,
+      submittedAt,
+      approvedBy: approver?.id ?? null,
+      approvedAt,
+    })
+  }
+  if (rows.length) {
+    const res = await prisma.game.createMany({ data: rows })
+    successCount += res.count ?? rows.length
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const game of newGames) {
const system = systemMap.get(game.systemName.toLowerCase())
if (!system) {
console.warn(`⚠️ System "${game.systemName}" not found, skipping game "${game.title}"`)
systemNotFoundCount++
continue
}
const submitter = getRandomElement(seededUsers)
const approver = getRandomElement(adminUsers)
const submittedAt = getRandomPastDate(30) // Recent submissions
const approvedAt = new Date(submittedAt.getTime() + Math.random() * 7 * 24 * 60 * 60 * 1000) // Approved within 7 days
try {
await prisma.game.create({
data: {
title: game.title,
systemId: system.id,
status: ApprovalStatus.APPROVED, // Auto-approve CSV imports
submittedBy: submitter?.id ?? null,
submittedAt: submittedAt,
approvedBy: approver?.id ?? null,
approvedAt: approvedAt,
},
})
successCount++
if (successCount % 100 === 0) {
console.log(`📈 Progress: ${successCount}/${newGames.length} games imported`)
}
} catch (error) {
console.error(`❌ Failed to create game "${game.title}" for ${game.systemName}:`, error)
failCount++
}
}
const rows = []
for (const game of newGames) {
const system = systemMap.get(game.systemName.toLowerCase())
if (!system) {
console.warn(`⚠️ System "${game.systemName}" not found, skipping game "${game.title}"`)
systemNotFoundCount++
continue
}
const submitter = getRandomElement(seededUsers)
const approver = getRandomElement(adminUsers)
const submittedAt = getRandomPastDate(30)
const approvedAt = new Date(submittedAt.getTime() + Math.random() * 7 * 24 * 60 * 60 * 1000)
rows.push({
title: game.title,
systemId: system.id,
status: ApprovalStatus.APPROVED,
submittedBy: submitter?.id ?? null,
submittedAt,
approvedBy: approver?.id ?? null,
approvedAt,
})
}
if (rows.length) {
const res = await prisma.game.createMany({ data: rows })
successCount += res.count ?? rows.length
}

Comment on lines +18 to +24
// Read the CSV file
const csvPath = path.join(__dirname, 'data', 'rp5_flip2_odin2_switch_listing.csv')

if (!fs.existsSync(csvPath)) {
console.error(`❌ CSV file not found at: ${csvPath}`)
return
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid __dirname (ESM risk) and make CSV path configurable; fail fast

Running with tsx/ESM can make __dirname undefined. Resolve from process.cwd() and allow an override via env var. Also throw to surface failures.

-  // Read the CSV file
-  const csvPath = path.join(__dirname, 'data', 'rp5_flip2_odin2_switch_listing.csv')
-
-  if (!fs.existsSync(csvPath)) {
-    console.error(`❌ CSV file not found at: ${csvPath}`)
-    return
-  }
+  // Read the CSV file
+  const csvPath =
+    process.env.CSV_LISTINGS_PATH ??
+    path.resolve(process.cwd(), 'prisma', 'seeders', 'data', 'rp5_flip2_odin2_switch_listing.csv')
+
+  if (!fs.existsSync(csvPath)) {
+    throw new Error(`CSV file not found at: ${csvPath}`)
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Read the CSV file
const csvPath = path.join(__dirname, 'data', 'rp5_flip2_odin2_switch_listing.csv')
if (!fs.existsSync(csvPath)) {
console.error(`❌ CSV file not found at: ${csvPath}`)
return
}
// Read the CSV file
const csvPath =
process.env.CSV_LISTINGS_PATH ??
path.resolve(process.cwd(), 'prisma', 'seeders', 'data', 'rp5_flip2_odin2_switch_listing.csv')
if (!fs.existsSync(csvPath)) {
throw new Error(`CSV file not found at: ${csvPath}`)
}
🤖 Prompt for AI Agents
In prisma/seeders/csvListingsSeeder.ts around lines 18 to 24, avoid using
__dirname (which breaks in ESM/tsx), make the CSV path configurable via an
environment variable, and fail fast by throwing an error instead of returning;
specifically, compute the csvPath by checking process.env.CSV_PATH first and
falling back to path.resolve(process.cwd(), 'prisma', 'seeders', 'data',
'rp5_flip2_odin2_switch_listing.csv') (or the appropriate relative location),
use fs.existsSync(csvPath) to verify the file, and if missing throw a
descriptive Error (`throw new Error(...)`) so the process surfaces the failure.

Comment on lines +160 to +171
// Get specific user for authorship
const specificUser = await prisma.user.findUnique({
where: {
email: 'ryanretrocompatibility@gmail.com',
},
})

if (!specificUser) {
console.error(
'❌ User "ryanretrocompatibility@gmail.com" not found, cannot proceed with listings import',
)
return
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Avoid hard-coded author email; make configurable with fallback

Hard-coding ties the script to one environment. Allow override via env, keep current email as default.

-  const specificUser = await prisma.user.findUnique({
-    where: {
-      email: 'ryanretrocompatibility@gmail.com',
-    },
-  })
+  const authorEmail = process.env.LISTINGS_AUTHOR_EMAIL ?? 'ryanretrocompatibility@gmail.com'
+  const specificUser = await prisma.user.findUnique({
+    where: { email: authorEmail },
+  })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Get specific user for authorship
const specificUser = await prisma.user.findUnique({
where: {
email: 'ryanretrocompatibility@gmail.com',
},
})
if (!specificUser) {
console.error(
'❌ User "ryanretrocompatibility@gmail.com" not found, cannot proceed with listings import',
)
return
// Get specific user for authorship
const authorEmail = process.env.LISTINGS_AUTHOR_EMAIL ?? 'ryanretrocompatibility@gmail.com'
const specificUser = await prisma.user.findUnique({
where: { email: authorEmail },
})
if (!specificUser) {
console.error(
'❌ User "ryanretrocompatibility@gmail.com" not found, cannot proceed with listings import',
)
return
🤖 Prompt for AI Agents
In prisma/seeders/csvListingsSeeder.ts around lines 160 to 171, replace the
hard-coded author email with a configurable value by reading an environment
variable (e.g. process.env.LISTINGS_AUTHOR_EMAIL) and falling back to
'ryanretrocompatibility@gmail.com'; use the resolved email (trimmed/validated)
in the prisma.user.findUnique query, keep the existing not-found error
handling/return behavior, and update the log message to reference the configured
email so the script works across environments.

Comment on lines +174 to +181
// Get seeded admin users for approval
const adminUsers = await prisma.user.findMany({
where: {
email: {
in: ['superadmin@emuready.com', 'admin@emuready.com'],
},
},
})
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Ensure approvers exist; don’t allow undefined processedByUserId

If no admin users are found, every create may fail with a null foreign key. Guard and bail early; then use the non-null approver.

   const adminUsers = await prisma.user.findMany({
     where: {
       email: {
         in: ['superadmin@emuready.com', 'admin@emuready.com'],
       },
     },
   })
+
+  if (adminUsers.length === 0) {
+    console.error('❌ No admin approvers found (seed superadmin/admin first).')
+    return
+  }
@@
-    const approver = getRandomElement(adminUsers)
+    const approver = getRandomElement(adminUsers)!
@@
-          processedByUserId: approver?.id ?? adminUsers[0]?.id,
+          processedByUserId: approver.id,

Also applies to: 247-248, 265-266

Comment on lines +253 to +268
try {
await prisma.listing.create({
data: {
gameId: game.id,
deviceId: device.id,
emulatorId: emulator.id,
performanceId: performance.id,
notes: combinedNotes,
authorId: author.id,
status: ApprovalStatus.APPROVED, // Auto-approve CSV imports
processedAt: approvedAt,
processedNotes: 'Imported from CSV data',
processedByUserId: approver?.id ?? adminUsers[0]?.id,
},
})

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Make import idempotent (skip duplicates instead of re-inserting)

Re-running the seeder will currently create duplicates. Check for an existing listing first.

-    try {
-      await prisma.listing.create({
+    try {
+      const existing = await prisma.listing.findFirst({
+        where: {
+          gameId: game.id,
+          deviceId: device.id,
+          emulatorId: emulator.id,
+          performanceId: performance.id,
+        },
+        select: { id: true },
+      })
+      if (existing) {
+        // Already imported
+        successCount++
+        continue
+      }
+      await prisma.listing.create({
         data: {

Additionally, consider a unique index on (gameId, deviceId, emulatorId, performanceId) to enforce this at the DB level.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
await prisma.listing.create({
data: {
gameId: game.id,
deviceId: device.id,
emulatorId: emulator.id,
performanceId: performance.id,
notes: combinedNotes,
authorId: author.id,
status: ApprovalStatus.APPROVED, // Auto-approve CSV imports
processedAt: approvedAt,
processedNotes: 'Imported from CSV data',
processedByUserId: approver?.id ?? adminUsers[0]?.id,
},
})
try {
// Skip duplicate imports by checking for an existing listing
const existing = await prisma.listing.findFirst({
where: {
gameId: game.id,
deviceId: device.id,
emulatorId: emulator.id,
performanceId: performance.id,
},
select: { id: true },
})
if (existing) {
// Already imported
successCount++
continue
}
await prisma.listing.create({
data: {
gameId: game.id,
deviceId: device.id,
emulatorId: emulator.id,
performanceId: performance.id,
notes: combinedNotes,
authorId: author.id,
status: ApprovalStatus.APPROVED, // Auto-approve CSV imports
processedAt: approvedAt,
processedNotes: 'Imported from CSV data',
processedByUserId: approver?.id ?? adminUsers[0]?.id,
},
})
🤖 Prompt for AI Agents
In prisma/seeders/csvListingsSeeder.ts around lines 253 to 268, the seeder
always inserts listings and will create duplicates on re-run; change the logic
to first check for an existing listing matching the composite key (gameId,
deviceId, emulatorId, performanceId) and skip insertion if found (or use
prisma.listing.upsert with a no-op update) so imports are idempotent;
additionally add or recommend creating a DB-level unique index on (gameId,
deviceId, emulatorId, performanceId) to enforce uniqueness and avoid
race-condition duplicates.

Comment on lines +14 to +19
} catch (error) {
console.error('💥 CSV games import failed:', error)
process.exit(1)
} finally {
await prisma.$disconnect()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid process.exit inside catch; it can skip prisma.$disconnect

Let finally run, set exitCode, and return.

-  } catch (error) {
-    console.error('💥 CSV games import failed:', error)
-    process.exit(1)
-  } finally {
+  } catch (error) {
+    console.error('💥 CSV games import failed:', error)
+    process.exitCode = 1
+    return
+  } finally {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
console.error('💥 CSV games import failed:', error)
process.exit(1)
} finally {
await prisma.$disconnect()
}
} catch (error) {
console.error('💥 CSV games import failed:', error)
process.exitCode = 1
return
} finally {
await prisma.$disconnect()
}
🤖 Prompt for AI Agents
In scripts/import-csv-games.ts around lines 14 to 19, the catch block calls
process.exit(1) which can prevent the finally block (and prisma.$disconnect())
from running; instead remove the process.exit call, set process.exitCode = 1 in
the catch, and return/allow the function to complete so the finally block
executes and awaits prisma.$disconnect() before the process exits naturally.

Comment on lines +14 to +19
} catch (error) {
console.error('💥 CSV listings import failed:', error)
process.exit(1)
} finally {
await prisma.$disconnect()
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid process.exit inside catch; it can skip prisma.$disconnect

Mirror the games script fix.

-  } catch (error) {
-    console.error('💥 CSV listings import failed:', error)
-    process.exit(1)
-  } finally {
+  } catch (error) {
+    console.error('💥 CSV listings import failed:', error)
+    process.exitCode = 1
+    return
+  } finally {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
console.error('💥 CSV listings import failed:', error)
process.exit(1)
} finally {
await prisma.$disconnect()
}
} catch (error) {
console.error('💥 CSV listings import failed:', error)
process.exitCode = 1
return
} finally {
await prisma.$disconnect()
}
🤖 Prompt for AI Agents
In scripts/import-csv-listings.ts around lines 14 to 19, the catch block calls
process.exit(1) which can prevent the finally block from running and skip
prisma.$disconnect; replace the direct process.exit call with setting
process.exitCode = 1 (or rethrow the error) and remove the process.exit
invocation so the finally block runs and prisma.$disconnect is always awaited,
mirroring the games script fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant