-
Notifications
You must be signed in to change notification settings - Fork 8
Chore: Add scripts to import Switch Games and Listing from RyanRetro list #169
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging
Are you sure you want to change the base?
Chore: Add scripts to import Switch Games and Listing from RyanRetro list #169
Conversation
|
@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. |
WalkthroughAdds 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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this 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 keyingNeeded 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 pathHelpful 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 endingsPrevents 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 messageMessage 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 parserEdge 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/loggingmain() 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/loggingLet 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 insteadSkipping when
values.length < headers.lengthdiscards 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 fieldsManual parser won’t handle embedded newlines within quoted cells and is easy to get wrong. Consider
csv-parse(streaming) orpapaparse. 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-parseexample wired to this seeder?
105-119: CSV-level dedupe key may miss same device under different namingDedup 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 dataYou 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 onesThe seeder generates
submittedAtandapprovedAtbut the Listing model only hascreatedAt(defaulted to now) andprocessedAt. Either overridecreatedAtwithsubmittedAtand keepprocessedAt = approvedAt, or dropsubmittedAt/approvedAtand only back-dateprocessedAt.
📜 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.
⛔ Files ignored due to path filters (2)
prisma/seeders/data/rp5_flip2_odin2_switch_games.csvis excluded by!**/*.csvprisma/seeders/data/rp5_flip2_odin2_switch_listing.csvis 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)
| // 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) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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++ | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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 | |
| } |
| // 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 | ||
| } |
There was a problem hiding this comment.
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.
| // 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.
| // 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 |
There was a problem hiding this comment.
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.
| // 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.
| // Get seeded admin users for approval | ||
| const adminUsers = await prisma.user.findMany({ | ||
| where: { | ||
| email: { | ||
| in: ['superadmin@emuready.com', 'admin@emuready.com'], | ||
| }, | ||
| }, | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
| 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, | ||
| }, | ||
| }) | ||
|
|
There was a problem hiding this comment.
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.
| 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.
| } catch (error) { | ||
| console.error('💥 CSV games import failed:', error) | ||
| process.exit(1) | ||
| } finally { | ||
| await prisma.$disconnect() | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| } 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.
| } catch (error) { | ||
| console.error('💥 CSV listings import failed:', error) | ||
| process.exit(1) | ||
| } finally { | ||
| await prisma.$disconnect() | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| } 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.
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
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.tsThe 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.tsScreenshots (if applicable)
Checklist
Summary by CodeRabbit
New Features
Chores