Skip to content

Comments

build: migrate to db#10

Merged
danielchim merged 6 commits intomasterfrom
build/migrate-to-db
Feb 7, 2026
Merged

build: migrate to db#10
danielchim merged 6 commits intomasterfrom
build/migrate-to-db

Conversation

@AkaraChen
Copy link
Collaborator

No description provided.

@danielchim danielchim marked this pull request as ready for review February 7, 2026 05:48
Copilot AI review requested due to automatic review settings February 7, 2026 05:48
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR introduces a new renderer “data layer” abstraction that can switch between Zustand-backed state and a DB-backed (SQLite/Drizzle) implementation accessed via tRPC, as part of the ongoing migration away from persisted Zustand stores.

Changes:

  • Add @/data service interfaces + React Query hooks (with a DataBridge for Zustand-mode cache invalidation) and swap multiple components to use them instead of direct Zustand store access.
  • Add DB-backed implementations on both renderer (vanilla tRPC client services) and main process (new appRouter.data.* procedures + Drizzle schema/migrations).
  • Initialize and migrate the user DB in Electron main, and add Drizzle tooling (drizzle-kit, drizzle-orm) + scripts.

Reviewed changes

Copilot reviewed 45 out of 47 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/vite-env.d.ts Adds VITE_DATASOURCE build flag typing.
src/main.tsx Mounts DataBridge at app root.
src/lib/trpc.tsx Adds a vanilla (non-React) tRPC client creator for services.
src/hooks/use-mod-installer.ts Switches mod state updates to @/data actions.
src/hooks/use-mod-actions.ts Switches mod state updates to @/data actions.
src/data/zustand.ts Adds Zustand-backed async service implementations.
src/data/trpc-services.ts Adds tRPC-backed service implementations (DB mode).
src/data/services.ts Exposes service singletons that switch by datasource.
src/data/interfaces.ts Introduces data service interfaces + domain types.
src/data/index.ts Provides @/data entrypoint exports for services/hooks/bridge.
src/data/hooks.ts Adds React Query hooks + DataBridge for Zustand-mode invalidation.
src/data/datasource.ts Adds datasource resolution logic via VITE_DATASOURCE.
src/components/layout/global-rail.tsx Migrates game/profile reads + actions to @/data hooks.
src/components/features/settings/settings-dialog.tsx Migrates managed game list source to @/data.
src/components/features/settings/panels/other-panel.tsx Migrates settings reads/writes to @/data.
src/components/features/settings/panels/locations-panel.tsx Migrates settings reads/writes to @/data.
src/components/features/settings/panels/game-settings-panel.tsx Migrates profile/settings/mod/game actions to @/data.
src/components/features/settings/panels/downloads-panel.tsx Migrates download settings reads/writes to @/data.
src/components/features/mods-library.tsx Migrates profile/mod/settings usage to @/data.
src/components/features/mod-tile.tsx Migrates mod/profile state to @/data.
src/components/features/mod-list-item.tsx Migrates mod/profile state to @/data.
src/components/features/mod-inspector.tsx Migrates mod/profile/settings state to @/data.
src/components/features/game-dashboard.tsx Migrates profile/mod/settings state/actions to @/data.
src/components/features/downloads-page.tsx Migrates mod/profile state reads to @/data.
src/components/features/dependencies/dependency-download-dialog.tsx Migrates mod/profile/settings reads + actions to @/data.
src/components/features/config-editor/config-editor-center.tsx Migrates active profile selection to @/data.
src/components/features/add-game-dialog.tsx Migrates game/profile/settings actions to @/data.
src/components/download-bridge.tsx Starts sourcing settings via @/data for syncing to main.
src/components/app-bootstrap.tsx Migrates initialization logic to @/data hooks/actions.
package.json Adds Drizzle deps and DB scripts.
pnpm-lock.yaml Locks new Drizzle dependencies.
electron/trpc/trpc.ts Centralizes tRPC init (superjson transformer).
electron/trpc/router.ts Wires in new data router under appRouter.
electron/trpc/data/index.ts Adds dataRouter composition (games/settings/profiles/mods).
electron/trpc/data/games.ts Implements DB-backed game procedures.
electron/trpc/data/settings.ts Implements DB-backed settings procedures.
electron/trpc/data/profiles.ts Implements DB-backed profile procedures.
electron/trpc/data/mods.ts Implements DB-backed mod state procedures.
electron/main.ts Initializes DB + runs migrations at app startup; closes DB on quit.
electron/db/schema.ts Defines Drizzle SQLite schema for user data.
electron/db/index.ts Adds SQLite/Drizzle DB initialization and singleton accessors.
electron/db/migrate.ts Runs Drizzle migrations at startup.
electron/db/migrations/0000_perpetual_mentallo.sql Initial SQL migration.
electron/db/migrations/meta/_journal.json Drizzle migration journal metadata.
electron/db/migrations/meta/0000_snapshot.json Drizzle schema snapshot metadata.
drizzle.config.ts Drizzle-kit configuration.
docs/architecture-database-migration.md Adds architecture doc for Zustand → DB migration plan.
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +7 to +10
// In development, migrations are relative to project root.
// In production, they need to be bundled — adjust path as needed.
const migrationsFolder = join(__dirname, "../../electron/db/migrations")
migrate(db, { migrationsFolder })
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

runMigrations() uses a hard-coded migrations folder relative to __dirname ("../../electron/db/migrations"). In packaged builds __dirname points into the bundled output, so this path is likely invalid unless migrations are explicitly copied there. Consider resolving migrationsFolder via app.getAppPath()/process.resourcesPath and ensure electron-builder includes the migrations directory as an extraResource.

Copilot uses AI. Check for mistakes.
Comment on lines +4 to +6
export const globalSettings = sqliteTable("global_settings", {
id: integer("id").primaryKey().$default(() => 1),

Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

global_settings is intended to be a singleton (id=1) but the schema doesn't enforce that only id=1 can exist. Consider adding a CHECK constraint (id = 1) or another enforcement mechanism to prevent accidental insertion of multiple rows.

Copilot uses AI. Check for mistakes.
Comment on lines +6 to +7
export const DATASOURCE: "db" | "zustand" =
(import.meta.env.VITE_DATASOURCE as "db" | "zustand" | undefined) ?? "db"
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

DATASOURCE defaults to "db" when VITE_DATASOURCE is unset. In non-Electron/web mode this will cause the data hooks/services to attempt to create a vanilla tRPC client and throw (electronTRPC is not available). Consider defaulting to "zustand" (preserve current behavior) or deriving the default from hasElectronTRPC()/import.meta.env.MODE and requiring an explicit flag to enable DB mode.

Copilot uses AI. Check for mistakes.

// Select the game
selectGame(defaultGameId)
}, [])
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

This initialization effect has an empty dependency array but depends on defaultGameId and several functions (gameMut, profileMut, getPerGame, selectGame). If defaultGameId is initially null (e.g., during persisted store rehydration) and becomes available later, this effect will never run and the app won't auto-select/manage the default game. Include the relevant dependencies and keep the hasInitialized guard to ensure it still runs only once.

Suggested change
}, [])
}, [defaultGameId, gameMut, profileMut, getPerGame, selectGame])

Copilot uses AI. Check for mistakes.
Comment on lines 135 to +140
// Check if auto-install is enabled (read from store directly to get latest value)
const autoInstallEnabled = useSettingsStore.getState().global.autoInstallMods
if (autoInstallEnabled && event.result?.extractedPath) {
// Get active profile for this game
const activeProfileId = useProfileStore.getState().activeProfileIdByGame[task.gameId]

Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

In DB mode, this callback reads autoInstallMods and activeProfileId from Zustand stores (useSettingsStore/useProfileStore), but those stores are no longer the source of truth when VITE_DATASOURCE=db. This can make auto-install behavior incorrect. Consider sourcing these values from the data services (or store the latest values from useSettingsData/useProfileData into refs that the IPC callback can read).

Copilot uses AI. Check for mistakes.
Comment on lines 147 to 160
@@ -162,10 +154,10 @@ export function DownloadBridge() {
version: task.modVersion,
extractedPath: event.result.extractedPath,
})

// Mark as installed in state
useModManagementStore.getState().installMod(activeProfileId, task.modId, task.modVersion)

Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

After installing a mod via profiles.installMod, this code updates only the Zustand mod-management store. In DB mode that will not persist to SQLite, so the installed mod won't appear in the DB-backed UI. Record the install via the DB/tRPC data router (e.g., data.mods.install or modService.install) and invalidate the relevant React Query keys instead of (or in addition to) updating the Zustand store.

Copilot uses AI. Check for mistakes.
Comment on lines +215 to +219
// Activate target
await tx
.update(profile)
.set({ isActive: true })
.where(eq(profile.id, input.profileId))
Copy link

Copilot AI Feb 7, 2026

Choose a reason for hiding this comment

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

setActive activates the target profile by id only. If a profileId from a different game is provided, this can incorrectly move the active flag across games and leave the requested game with no active profile. Add gameId to the WHERE clause (and/or verify the profile belongs to the game before updating).

Suggested change
// Activate target
await tx
.update(profile)
.set({ isActive: true })
.where(eq(profile.id, input.profileId))
// Activate target (ensure it belongs to the same game)
await tx
.update(profile)
.set({ isActive: true })
.where(and(eq(profile.id, input.profileId), eq(profile.gameId, input.gameId)))

Copilot uses AI. Check for mistakes.
@danielchim danielchim merged commit 9d6cbe3 into master Feb 7, 2026
9 of 10 checks passed
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.

2 participants