refactor: convert to bun workspaces monorepo

- Move web app into apps/web/
- Create packages/shared/ with shared types
- Create plugins/decky-vault/ scaffold
- Root package.json manages workspaces only
This commit is contained in:
2026-06-28 05:20:28 +08:00
parent c4bede20d4
commit cd72b7a948
345 changed files with 488 additions and 126 deletions
+9
View File
@@ -0,0 +1,9 @@
import { drizzle } from "drizzle-orm/node-postgres"
import { Pool } from "pg"
import * as schema from "./schema"
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
})
export const db = drizzle(pool, { schema })
@@ -0,0 +1,50 @@
import { db } from "@/lib/db/index"
import { games } from "@/lib/db/schema"
import { generateSlug } from "@/lib/utils/slug"
import { ne, isNull, isNotNull, and, eq } from "drizzle-orm"
async function backfillSlugs() {
const nonSteamGames = await db
.select({ id: games.id, title: games.title })
.from(games)
.where(and(ne(games.source, "steam"), isNull(games.slug)))
console.log(`Found ${nonSteamGames.length} non-Steam games without slugs`)
const usedSlugs = new Set<string>()
const existing = await db
.select({ slug: games.slug })
.from(games)
.where(isNotNull(games.slug))
for (const row of existing) {
if (row.slug) usedSlugs.add(row.slug)
}
let updated = 0
let errors = 0
for (const game of nonSteamGames) {
let slug = generateSlug(game.title)
if (!slug) {
slug = `game-${game.id.slice(0, 8)}`
}
let candidate = slug
let suffix = 2
while (usedSlugs.has(candidate)) {
candidate = `${slug}-${suffix}`
suffix++
}
usedSlugs.add(candidate)
try {
await db.update(games).set({ slug: candidate }).where(eq(games.id, game.id))
updated++
} catch (err) {
console.error(`Failed to update ${game.title} (${game.id}):`, err)
errors++
}
}
console.log(`Backfill complete: ${updated} updated, ${errors} errors`)
}
backfillSlugs()
.then(() => process.exit(0))
.catch((err) => { console.error("Backfill failed:", err); process.exit(1) })
+137
View File
@@ -0,0 +1,137 @@
import { relations } from "drizzle-orm"
import {
pgTable,
text,
timestamp,
boolean,
integer,
index,
} from "drizzle-orm/pg-core"
export const user = pgTable("user", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: boolean("email_verified").default(false).notNull(),
image: text("image"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
role: text("role"),
banned: boolean("banned").default(false),
banReason: text("ban_reason"),
banExpires: timestamp("ban_expires"),
lastLoginMethod: text("last_login_method"),
})
export const session = pgTable(
"session",
{
id: text("id").primaryKey(),
expiresAt: timestamp("expires_at").notNull(),
token: text("token").notNull().unique(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
impersonatedBy: text("impersonated_by"),
},
(table) => [index("session_userId_idx").on(table.userId)],
)
export const account = pgTable(
"account",
{
id: text("id").primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: timestamp("access_token_expires_at"),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
},
(table) => [index("account_userId_idx").on(table.userId)],
)
export const verification = pgTable(
"verification",
{
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expires_at").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
},
(table) => [index("verification_identifier_idx").on(table.identifier)],
)
export const passkey = pgTable(
"passkey",
{
id: text("id").primaryKey(),
name: text("name"),
publicKey: text("public_key").notNull(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
credentialID: text("credential_id").notNull(),
counter: integer("counter").notNull(),
deviceType: text("device_type").notNull(),
backedUp: boolean("backed_up").notNull(),
transports: text("transports"),
createdAt: timestamp("created_at"),
aaguid: text("aaguid"),
},
(table) => [
index("passkey_userId_idx").on(table.userId),
index("passkey_credentialID_idx").on(table.credentialID),
],
)
export const userRelations = relations(user, ({ many }) => ({
sessions: many(session),
accounts: many(account),
passkeys: many(passkey),
}))
export const sessionRelations = relations(session, ({ one }) => ({
user: one(user, {
fields: [session.userId],
references: [user.id],
}),
}))
export const accountRelations = relations(account, ({ one }) => ({
user: one(user, {
fields: [account.userId],
references: [user.id],
}),
}))
export const passkeyRelations = relations(passkey, ({ one }) => ({
user: one(user, {
fields: [passkey.userId],
references: [user.id],
}),
}))
@@ -0,0 +1,54 @@
import {
index,
pgEnum,
pgTable,
text,
timestamp,
uniqueIndex,
} from "drizzle-orm/pg-core"
import { games } from "./games"
import { user } from "./auth"
export const suggestionStatusEnum = pgEnum("suggestion_status", [
"pending",
"approved",
"rejected",
])
export const communitySuggestions = pgTable(
"community_suggestions",
{
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
gameId: text("game_id")
.notNull()
.references(() => games.id, { onDelete: "cascade" }),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
fieldName: text("field_name").notNull(), // e.g. "title", "description", "developer"
currentValue: text("current_value"), // current value (snapshot)
proposedValue: text("proposed_value").notNull(), // proposed new value
reason: text("reason"), // optional explanation
status: suggestionStatusEnum("status").default("pending").notNull(),
reviewedBy: text("reviewed_by").references(() => user.id, {
onDelete: "set null",
}),
reviewedAt: timestamp("reviewed_at"),
reviewNote: text("review_note"), // reviewer's note
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.$onUpdate(() => new Date())
.notNull(),
},
(table) => [
uniqueIndex("community_suggestions_game_field_user").on(
table.gameId,
table.fieldName,
table.userId,
),
index("suggestions_status_idx").on(table.status),
],
)
@@ -0,0 +1,30 @@
import {
integer,
pgTable,
text,
timestamp,
index,
} from "drizzle-orm/pg-core"
import { performanceEntries } from "./performanceEntries"
export const entryScreenshots = pgTable(
"entry_screenshots",
{
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
entryId: text("entry_id")
.notNull()
.references(() => performanceEntries.id, { onDelete: "cascade" }),
storageKey: text("storage_key").notNull(),
orderIndex: integer("order_index").notNull().default(0),
mimeType: text("mime_type").notNull(),
width: integer("width").notNull(),
height: integer("height").notNull(),
originalName: text("original_name"),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(table) => [
index("entry_screenshots_entry_idx").on(table.entryId),
],
)
+39
View File
@@ -0,0 +1,39 @@
import {
boolean,
integer,
jsonb,
pgTable,
text,
timestamp,
index,
} from "drizzle-orm/pg-core"
import { games } from "./games"
import { user } from "./auth"
export const gameComments = pgTable(
"game_comments",
{
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
gameId: text("game_id")
.notNull()
.references(() => games.id, { onDelete: "cascade" }),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
parentId: text("parent_id").references((): any => gameComments.id, { // eslint-disable-line @typescript-eslint/no-explicit-any
onDelete: "cascade",
}),
// Tiptap JSON document
content: jsonb("content").notNull().$type<Record<string, unknown>>(),
upvotes: integer("upvotes").default(0).notNull(),
isRemoved: boolean("is_removed").default(false).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
},
(table) => [
index("comments_game_created_idx").on(table.gameId, table.createdAt),
index("comments_parent_idx").on(table.parentId),
],
)
@@ -0,0 +1,66 @@
import {
boolean,
pgEnum,
pgTable,
text,
timestamp,
unique,
} from "drizzle-orm/pg-core"
import { games, playabilityStatusEnum } from "./games"
import { hardware } from "./hardware"
export const protonStatusEnum = pgEnum("proton_status", [
"native",
"proton",
"unsupported",
"unknown",
])
export const antiCheatStatusEnum = pgEnum("anti_cheat_status", [
"none",
"supported",
"unsupported",
"unknown",
])
export const gamePlatformSupport = pgTable(
"game_platform_support",
{
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
gameId: text("game_id")
.notNull()
.references(() => games.id, { onDelete: "cascade" }),
hardwareSlug: text("hardware_slug")
.notNull()
.references(() => hardware.slug, { onDelete: "restrict" }),
// Compatibility
isSupported: boolean("is_supported").default(false).notNull(),
protonStatus: protonStatusEnum("proton_status")
.default("unknown")
.notNull(),
// Anti-cheat details
antiCheatRelevant: boolean("anti_cheat_relevant")
.default(false)
.notNull(),
antiCheatName: text("anti_cheat_name"),
antiCheatVersion: text("anti_cheat_version"),
antiCheatStatus: antiCheatStatusEnum("anti_cheat_status")
.default("unknown")
.notNull(),
// Per-device playability
playabilityStatus: playabilityStatusEnum("playability_status").default("unknown").notNull(),
playabilityOverride: boolean("playability_override").default(false).notNull(),
playabilityCalculatedAt: timestamp("playability_calculated_at"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
},
(table) => [
unique("game_hardware_unique").on(table.gameId, table.hardwareSlug),
]
)
+29
View File
@@ -0,0 +1,29 @@
import {
boolean,
index,
pgTable,
text,
timestamp,
unique,
} from "drizzle-orm/pg-core"
import { games } from "./games"
export const gameVersions = pgTable(
"game_versions",
{
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
gameId: text("game_id")
.notNull()
.references(() => games.id, { onDelete: "cascade" }),
buildId: text("build_id"),
versionString: text("version_string"),
isLatest: boolean("is_latest").default(false).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(table) => [
unique("game_build_unique").on(table.gameId, table.buildId),
index("perf_game_lookup_idx").on(table.gameId),
]
)
+105
View File
@@ -0,0 +1,105 @@
import {
boolean,
integer,
jsonb,
pgEnum,
pgTable,
text,
timestamp,
index,
} from "drizzle-orm/pg-core"
export const gameSourceEnum = pgEnum("game_source", [
"steam",
"manual",
"gog",
"epic",
])
export const onlineMultiplayerStatusEnum = pgEnum(
"online_multiplayer_status",
["none", "supported", "unknown"],
)
export const steamReviewSentimentEnum = pgEnum("steam_review_sentiment", [
"overwhelmingly_positive",
"very_positive",
"positive",
"mostly_positive",
"mixed",
"mostly_negative",
"negative",
"very_negative",
"overwhelmingly_negative",
])
export const playabilityStatusEnum = pgEnum("playability_status", [
"great",
"playable",
"needs_tweaks",
"unplayable",
"unknown",
])
export const games = pgTable(
"games",
{
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
steamAppId: integer("steam_app_id").unique(),
source: gameSourceEnum("source").default("steam").notNull(),
slug: text("slug").unique(),
title: text("title").notNull(),
description: text("description"),
publisher: text("publisher"),
developer: text("developer"),
genres: jsonb("genres").$type<string[]>(),
headerImage: text("header_image"),
capsuleImage: text("capsule_image"),
storeUrl: text("store_url"),
onlineMultiplayerStatus: onlineMultiplayerStatusEnum(
"online_multiplayer_status",
)
.default("unknown")
.notNull(),
systemRequirements: jsonb("system_requirements").$type<{
minimum: string | null
recommended: string | null
}>(),
metacriticScore: integer("metacritic_score"),
metacriticUrl: text("metacritic_url"),
recommendationsTotal: integer("recommendations_total"),
steamReviewScore: integer("steam_review_score"), // 0-100 normalized score
steamReviewSentiment: steamReviewSentimentEnum("steam_review_sentiment"),
steamReviewCount: integer("steam_review_count"), // Total review count from Steam
// Playability (aggregate from all devices)
playabilityStatus: playabilityStatusEnum("playability_status").default("unknown").notNull(),
playabilityOverride: boolean("playability_override").default(false).notNull(), // true = manually set
playabilityCalculatedAt: timestamp("playability_calculated_at"), // when auto-calculated
priceCurrent: integer("price_current"),
priceInitial: integer("price_initial"),
priceCurrency: text("price_currency"),
isFree: boolean("is_free").default(false).notNull(),
releaseDate: text("release_date"),
categories: jsonb("categories").$type<string[]>(),
platforms: jsonb("platforms").$type<{
windows: boolean
mac: boolean
linux: boolean
}>(),
lastSync: timestamp("last_sync"),
syncStatus: text("sync_status").default("pending"),
syncError: text("sync_error"),
syncRetryCount: integer("sync_retry_count").default(0),
syncNextRetry: timestamp("sync_next_retry"),
createdBy: text("created_by"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
},
(table) => [
index("games_source_idx").on(table.source),
index("games_sync_status_idx").on(table.syncStatus, table.steamAppId),
],
)
+14
View File
@@ -0,0 +1,14 @@
import { integer, pgEnum, pgTable, real, text, timestamp } from "drizzle-orm/pg-core"
export const deviceTypeEnum = pgEnum("device_type", ["handheld", "console"])
export const hardware = pgTable("hardware", {
slug: text("slug").primaryKey(),
name: text("name").notNull(),
deviceType: deviceTypeEnum("device_type").notNull(),
image: text("image"),
wattHours: real("watt_hours"),
tdpMax: real("tdp_max"),
sortOrder: integer("sort_order").default(0).notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
})
+14
View File
@@ -0,0 +1,14 @@
// Barrel export — domain + auth schemas
export * from "./auth"
export * from "./games"
export * from "./gameVersions"
export * from "./hardware"
export * from "./performanceEntries"
export * from "./gamePlatformSupport"
export * from "./gameComments"
export * from "./savedGames"
export * from "./reports"
export * from "./community-suggestions"
export * from "./saved-filters"
export * from "./storage"
export * from "./entryScreenshots"
@@ -0,0 +1,118 @@
import {
boolean,
integer,
jsonb,
pgEnum,
pgTable,
real,
text,
timestamp,
index,
} from "drizzle-orm/pg-core"
import { gameVersions } from "./gameVersions"
import { hardware } from "./hardware"
import { user } from "./auth"
export const upscalerTypeEnum = pgEnum("upscaler_type", [
"none",
"fsr",
"dlss",
"xess",
"lsfg",
"other",
])
export const frameGenMethodEnum = pgEnum("frame_gen_method", [
"none",
"fsr_fg",
"dlss_fg",
"lsfg",
"other",
])
export type GameSettingCategory = {
category: string
settings: { title: string; value: string | number | boolean }[]
}
export const performanceEntries = pgTable(
"performance_entries",
{
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
versionId: text("version_id")
.notNull()
.references(() => gameVersions.id, { onDelete: "cascade" }),
hardwareSlug: text("hardware_slug")
.notNull()
.references(() => hardware.slug, { onDelete: "restrict" }),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
// Performance metrics
fpsAvg: real("fps_avg").notNull(),
fpsLow: real("fps_low"),
fpsOnePercentLow: real("fps_one_percent_low"),
fpsHigh: real("fps_high"),
// Environment
protonVersion: text("proton_version"),
osVersion: text("os_version"),
// Upscaler tracking
upscalerType: upscalerTypeEnum("upscaler_type").default("none").notNull(),
upscalerVersion: text("upscaler_version"),
frameGenMethod: frameGenMethodEnum("frame_gen_method")
.default("none")
.notNull(),
// Load times (seconds)
loadTimeSsd: real("load_time_ssd"),
loadTimeSd: real("load_time_sd"),
// Launch options (Steam launch options string)
launchOptions: text("launch_options"),
// Settings & notes
settingsJson: jsonb("settings_json").$type<GameSettingCategory[]>(),
userNotes: text("user_notes"),
// TDP tracking (watts) — user-set TDP cap during benchmark
tdpWatts: real("tdp_watts"),
// YouTube video linking
youtubeVideoId: text("youtube_video_id"),
// Custom system flag
customSystem: boolean("custom_system").default(false).notNull(),
// Moderation
isRemoved: boolean("is_removed").default(false).notNull(),
isPinned: boolean("is_pinned").default(false).notNull(),
pinnedAt: timestamp("pinned_at"),
removedReason: text("removed_reason"),
// Community rating
upvotes: integer("upvotes").default(0).notNull(),
downvotes: integer("downvotes").default(0).notNull(),
// Verification (admin/mod workflow)
verifiedAt: timestamp("verified_at"),
verifiedBy: text("verified_by").references(() => user.id, {
onDelete: "set null",
}),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
},
(table) => [
index("perf_hardware_upscaler_idx").on(table.hardwareSlug, table.upscalerType),
index("perf_version_idx").on(table.versionId),
index("perf_user_idx").on(table.userId),
index("perf_removed_created_idx").on(table.isRemoved, table.createdAt.desc()),
index("perf_upvotes_idx").on(table.upvotes.desc()),
],
)
+42
View File
@@ -0,0 +1,42 @@
import {
index,
text,
pgEnum,
pgTable,
timestamp,
uniqueIndex,
} from "drizzle-orm/pg-core"
import { performanceEntries } from "./performanceEntries"
import { user } from "./auth"
export const reportReasonEnum = pgEnum("report_reason", [
"inaccurate",
"spam",
"inappropriate",
"other",
])
export const reportStatusEnum = pgEnum("report_status", [
"open",
"reviewed",
"dismissed",
])
export const reports = pgTable("reports", {
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
entryId: text("entry_id")
.notNull()
.references(() => performanceEntries.id, { onDelete: "cascade" }),
reporterId: text("reporter_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
reason: reportReasonEnum("reason").notNull(),
details: text("details"),
status: reportStatusEnum("status").default("open").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
}, (table) => [
uniqueIndex("reports_entry_reporter_unique").on(table.entryId, table.reporterId),
index("reports_status_idx").on(table.status),
])
+30
View File
@@ -0,0 +1,30 @@
import {
pgTable,
text,
timestamp,
jsonb,
unique,
} from "drizzle-orm/pg-core"
import { user } from "./auth"
export const savedFilters = pgTable(
"saved_filters",
{
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
name: text("name").notNull(),
filters: jsonb("filters").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.$onUpdate(() => new Date())
.notNull(),
},
(table) => [
unique("saved_filters_user_name").on(table.userId, table.name),
],
)
+27
View File
@@ -0,0 +1,27 @@
import {
pgTable,
text,
timestamp,
unique,
} from "drizzle-orm/pg-core"
import { user } from "./auth"
import { games } from "./games"
export const savedGames = pgTable(
"saved_games",
{
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
gameId: text("game_id")
.notNull()
.references(() => games.id, { onDelete: "cascade" }),
createdAt: timestamp("created_at").defaultNow().notNull(),
},
(table) => [
unique("saved_games_user_game_unique").on(table.userId, table.gameId),
],
)
+34
View File
@@ -0,0 +1,34 @@
import {
boolean,
integer,
pgTable,
text,
timestamp,
index,
} from "drizzle-orm/pg-core"
import { user } from "./auth"
export const storageObjects = pgTable(
"storage_objects",
{
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
key: text("key").notNull(),
bucket: text("bucket").notNull(),
size: integer("size").notNull(),
mimeType: text("mime_type").notNull(),
entityType: text("entity_type").notNull(), // "avatar" | "game_cover" | "hardware_image"
entityId: text("entity_id"), // user ID, game ID, or hardware slug
uploadedBy: text("uploaded_by")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
createdAt: timestamp("created_at").defaultNow().notNull(),
lastAccessedAt: timestamp("last_accessed_at"),
isOrphaned: boolean("is_orphaned").default(false).notNull(),
},
(table) => [
index("storage_entity_idx").on(table.entityType, table.entityId),
index("storage_key_idx").on(table.key),
],
)
+17
View File
@@ -0,0 +1,17 @@
/**
* Normalizes a raw search query so that common word separators
* (spaces, hyphens, underscores, colons, dots) are treated equivalently.
*
* Replaces any sequence of separator chars with a single `%` ILIKE wildcard,
* then wraps the whole pattern in `%…%`.
*
* Example:
* fuzzySearchTerm("counter strike") → "%counter%strike%"
* fuzzySearchTerm("counter-strike") → "%counter%strike%"
*
* Both will match "Counter-Strike", "Counter Strike", "Counter_Strike", etc.
*/
export function fuzzySearchTerm(rawQuery: string): string {
const normalized = rawQuery.replace(/[-_\s:.]+/g, "%")
return `%${normalized}%`
}
+51
View File
@@ -0,0 +1,51 @@
import "dotenv/config"
import { drizzle } from "drizzle-orm/node-postgres"
import { Pool } from "pg"
import { hardware } from "./schema/hardware"
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
})
const db = drizzle(pool)
async function seed() {
// ── Hardware ──────────────────────────────────────────────────────
console.log("Seeding hardware table...")
const devices = [
{
slug: "steamdeck-oled",
name: "Steam Deck OLED",
deviceType: "handheld" as const,
sortOrder: 0,
},
{
slug: "steamdeck-lcd",
name: "Steam Deck LCD",
deviceType: "handheld" as const,
sortOrder: 1,
},
{
slug: "steam-machine",
name: "Steam Machine",
deviceType: "console" as const,
sortOrder: 2,
},
]
for (const device of devices) {
await db
.insert(hardware)
.values(device)
.onConflictDoNothing({ target: hardware.slug })
}
console.log(`Seeded ${devices.length} hardware devices.`)
await pool.end()
}
seed().catch((err) => {
console.error("Seed failed:", err)
process.exit(1)
})