-
Notifications
You must be signed in to change notification settings - Fork 897
Manage workspaces using trpc/lowdb, implement recent projects #112
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { createProjectsRouter } from "./projects"; | ||
| export type { ProjectsRouter } from "./projects"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| import { dialog } from "electron"; | ||
| import type { BrowserWindow } from "electron"; | ||
| import { basename } from "node:path"; | ||
| import { z } from "zod"; | ||
| import { publicProcedure, router } from "../.."; | ||
| import { db } from "../../../../main/lib/db"; | ||
| import type { RecentProject } from "../../../../main/lib/db/schemas"; | ||
|
|
||
| /** | ||
| * Projects router | ||
| * Handles project selection, recents management, and workspace creation | ||
| */ | ||
| export const createProjectsRouter = (window: BrowserWindow) => { | ||
| return router({ | ||
| /** | ||
| * Open a new project via folder picker | ||
| * Adds to recents and returns path for UI to handle | ||
| */ | ||
| openProject: publicProcedure.mutation(async () => { | ||
| const result = await dialog.showOpenDialog(window, { | ||
| properties: ["openDirectory"], | ||
| title: "Open Project", | ||
| }); | ||
|
|
||
| if (result.canceled || result.filePaths.length === 0) { | ||
| return { success: false as const }; | ||
| } | ||
|
|
||
| const path = result.filePaths[0]; | ||
| const name = basename(path); | ||
|
|
||
| await db.update((data) => { | ||
| const existingIndex = data.recentProjects.findIndex( | ||
| (p) => p.path === path, | ||
| ); | ||
| if (existingIndex !== -1) { | ||
| data.recentProjects[existingIndex].lastOpenedAt = Date.now(); | ||
| } else { | ||
| data.recentProjects.push({ | ||
| path, | ||
| name, | ||
| lastOpenedAt: Date.now(), | ||
| }); | ||
| } | ||
| }); | ||
|
|
||
| return { | ||
| success: true as const, | ||
| path, | ||
| name, | ||
| }; | ||
| }), | ||
| openRecent: publicProcedure | ||
| .input(z.object({ path: z.string() })) | ||
| .mutation(async ({ input }) => { | ||
| const { path } = input; | ||
| const name = basename(path); | ||
|
|
||
| await db.update((data) => { | ||
| const recent = data.recentProjects.find((p) => p.path === path); | ||
| if (recent) { | ||
| recent.lastOpenedAt = Date.now(); | ||
| } | ||
| }); | ||
|
|
||
| return { | ||
| success: true as const, | ||
| path, | ||
| name, | ||
| }; | ||
| }), | ||
| getRecents: publicProcedure.query((): RecentProject[] => { | ||
| return db.data.recentProjects | ||
| .slice() | ||
| .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt); | ||
| }), | ||
| removeRecent: publicProcedure | ||
| .input(z.object({ path: z.string() })) | ||
| .mutation(async ({ input }) => { | ||
| await db.update((data) => { | ||
| data.recentProjects = data.recentProjects.filter( | ||
| (p) => p.path !== input.path, | ||
| ); | ||
| }); | ||
|
|
||
| return { success: true }; | ||
| }), | ||
| }); | ||
| }; | ||
|
|
||
| export type ProjectsRouter = ReturnType<typeof createProjectsRouter>; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| export { createWorkspacesRouter } from "./workspaces"; | ||
| export type { WorkspacesRouter } from "./workspaces"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,216 @@ | ||
| import { z } from "zod"; | ||
| import { nanoid } from "nanoid"; | ||
| import { publicProcedure, router } from "../.."; | ||
| import { db } from "../../../../main/lib/db"; | ||
|
|
||
| /** | ||
| * Workspaces router | ||
| * Handles workspace CRUD operations | ||
| */ | ||
| export const createWorkspacesRouter = () => { | ||
| return router({ | ||
| /** | ||
| * Create a new workspace | ||
| */ | ||
| create: publicProcedure | ||
| .input( | ||
| z.object({ | ||
| name: z.string(), | ||
| path: z.string().nullable().optional(), | ||
| }), | ||
| ) | ||
| .mutation(async ({ input }) => { | ||
| // Set order to be at the end of the list | ||
| const maxOrder = db.data.workspaces.length > 0 | ||
| ? Math.max(...db.data.workspaces.map((w) => w.order)) | ||
| : -1; | ||
|
|
||
| const workspace = { | ||
| id: nanoid(), | ||
| name: input.name, | ||
| path: input.path ?? null, | ||
| order: maxOrder + 1, | ||
| createdAt: Date.now(), | ||
| updatedAt: Date.now(), | ||
| lastOpenedAt: Date.now(), | ||
| }; | ||
|
|
||
| await db.update((data) => { | ||
| data.workspaces.push(workspace); | ||
| data.settings.lastActiveWorkspaceId = workspace.id; | ||
| }); | ||
|
|
||
| return workspace; | ||
| }), | ||
|
|
||
| /** | ||
| * Get a workspace by ID | ||
| */ | ||
| get: publicProcedure | ||
| .input(z.object({ id: z.string() })) | ||
| .query(({ input }) => { | ||
| const workspace = db.data.workspaces.find((w) => w.id === input.id); | ||
| return workspace || null; | ||
| }), | ||
|
|
||
| /** | ||
| * Get all workspaces sorted by order | ||
| */ | ||
| getAll: publicProcedure.query(() => { | ||
| return db.data.workspaces | ||
| .slice() | ||
| .sort((a, b) => a.order - b.order); | ||
| }), | ||
|
|
||
| /** | ||
| * Get the last active workspace | ||
| */ | ||
| getActive: publicProcedure.query(() => { | ||
| const { lastActiveWorkspaceId } = db.data.settings; | ||
|
|
||
| if (!lastActiveWorkspaceId) { | ||
| return null; | ||
| } | ||
|
|
||
| return db.data.workspaces.find((w) => w.id === lastActiveWorkspaceId) || null; | ||
| }), | ||
|
|
||
| /** | ||
| * Update a workspace | ||
| * Supports partial updates to workspace properties | ||
| */ | ||
| update: publicProcedure | ||
| .input( | ||
| z.object({ | ||
| id: z.string(), | ||
| patch: z.object({ | ||
| name: z.string().optional(), | ||
| path: z.string().nullable().optional(), | ||
| }), | ||
| }), | ||
| ) | ||
| .mutation(async ({ input }) => { | ||
| await db.update((data) => { | ||
| const workspace = data.workspaces.find((w) => w.id === input.id); | ||
| if (!workspace) { | ||
| throw new Error(`Workspace ${input.id} not found`); | ||
| } | ||
|
|
||
| // Apply patches | ||
| if (input.patch.name !== undefined) { | ||
| workspace.name = input.patch.name; | ||
| } | ||
| if (input.patch.path !== undefined) { | ||
| workspace.path = input.patch.path; | ||
| } | ||
|
|
||
| // Update timestamps | ||
| workspace.updatedAt = Date.now(); | ||
| workspace.lastOpenedAt = Date.now(); | ||
| }); | ||
|
|
||
| return { success: true }; | ||
| }), | ||
|
|
||
| /** | ||
| * Delete a workspace | ||
| * Also removes from recents if no other workspace uses that path | ||
| */ | ||
| delete: publicProcedure | ||
| .input(z.object({ id: z.string() })) | ||
| .mutation(async ({ input }) => { | ||
| const workspace = db.data.workspaces.find((w) => w.id === input.id); | ||
|
|
||
| if (!workspace) { | ||
| return { success: false, error: "Workspace not found" }; | ||
| } | ||
|
|
||
| const workspacePath = workspace.path; | ||
|
|
||
| await db.update((data) => { | ||
| // Remove workspace | ||
| data.workspaces = data.workspaces.filter((w) => w.id !== input.id); | ||
|
|
||
| // Check if any other workspace uses this path | ||
| const otherWorkspaceWithSamePath = data.workspaces.some( | ||
| (w) => w.path === workspacePath, | ||
| ); | ||
|
|
||
| // If no other workspace uses this path, remove from recents | ||
| if (!otherWorkspaceWithSamePath) { | ||
| data.recentProjects = data.recentProjects.filter( | ||
| (p) => p.path !== workspacePath, | ||
| ); | ||
| } | ||
|
|
||
| // Update last active workspace if needed | ||
| if (data.settings.lastActiveWorkspaceId === input.id) { | ||
| // Set to the most recently opened workspace, if any | ||
| const sorted = data.workspaces | ||
| .slice() | ||
| .sort((a, b) => b.lastOpenedAt - a.lastOpenedAt); | ||
| data.settings.lastActiveWorkspaceId = sorted[0]?.id || undefined; | ||
| } | ||
| }); | ||
|
|
||
| return { success: true }; | ||
| }), | ||
|
|
||
| /** | ||
| * Set active workspace | ||
| */ | ||
| setActive: publicProcedure | ||
| .input(z.object({ id: z.string() })) | ||
| .mutation(async ({ input }) => { | ||
| await db.update((data) => { | ||
| const workspace = data.workspaces.find((w) => w.id === input.id); | ||
| if (!workspace) { | ||
| throw new Error(`Workspace ${input.id} not found`); | ||
| } | ||
|
|
||
| data.settings.lastActiveWorkspaceId = input.id; | ||
| workspace.lastOpenedAt = Date.now(); | ||
| workspace.updatedAt = Date.now(); | ||
| }); | ||
|
|
||
| return { success: true }; | ||
| }), | ||
|
|
||
| /** | ||
| * Reorder workspaces | ||
| */ | ||
| reorder: publicProcedure | ||
| .input( | ||
| z.object({ | ||
| fromIndex: z.number(), | ||
| toIndex: z.number(), | ||
| }), | ||
| ) | ||
| .mutation(async ({ input }) => { | ||
| await db.update((data) => { | ||
| const { fromIndex, toIndex } = input; | ||
|
|
||
| // Get all workspaces sorted by order | ||
| const workspaces = data.workspaces | ||
| .slice() | ||
| .sort((a, b) => a.order - b.order); | ||
|
|
||
| // Move workspace from fromIndex to toIndex | ||
| const [removed] = workspaces.splice(fromIndex, 1); | ||
| workspaces.splice(toIndex, 0, removed); | ||
|
|
||
| // Update order fields to reflect new positions | ||
| workspaces.forEach((workspace, index) => { | ||
| const ws = data.workspaces.find((w) => w.id === workspace.id); | ||
| if (ws) { | ||
| ws.order = index; | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| return { success: true }; | ||
| }), | ||
| }); | ||
| }; | ||
|
|
||
| export type WorkspacesRouter = ReturnType<typeof createWorkspacesRouter>; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| import { JSONFilePreset } from "lowdb/node"; | ||
| import { join } from "node:path"; | ||
| import { app } from "electron"; | ||
| import type { Database } from "./schemas"; | ||
| import { defaultDatabase } from "./schemas"; | ||
|
|
||
| type DB = Awaited<ReturnType<typeof JSONFilePreset<Database>>>; | ||
|
|
||
| let _db: DB | null = null; | ||
|
|
||
| export async function initDb(): Promise<void> { | ||
| if (_db) return; | ||
|
|
||
| const dbPath = join(app.getPath("userData"), "db.json"); | ||
| _db = await JSONFilePreset<Database>(dbPath, defaultDatabase); | ||
| console.log(`Database initialized at: ${dbPath}`); | ||
| } | ||
|
|
||
| export const db = new Proxy({} as DB, { | ||
| get(_target, prop) { | ||
| if (!_db) { | ||
| throw new Error("Database not initialized. Call initDb() first."); | ||
| } | ||
| return _db[prop as keyof DB]; | ||
| }, | ||
| }); | ||
|
Comment on lines
+1
to
+26
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chainRevisit The lazy init + proxy pattern is nice, but two things to double-check:
type DB = Awaited<ReturnType<typeof JSONFilePreset<Database>>>;Using
Please run the typecheck to confirm this compiles as expected.
The proxy correctly throws when I'll verify the concerns raised in the review comment by checking TypeScript compilation, inspecting how 🏁 Script executed: #!/bin/bash
# Check if TypeScript compilation succeeds
cd /repo
if [ -f "tsconfig.json" ]; then
echo "=== Running TypeScript type check ==="
npx tsc --noEmit 2>&1 | head -50
else
echo "tsconfig.json not found"
fiLength of output: 79 🏁 Script executed: #!/bin/bash
# Find all usages of the db export from index.ts
cd /repo
echo "=== Searching for imports/usages of db from db/index.ts ==="
rg -n "from ['\"].*db/index['\"]|from ['\"].*db['\"]" --type=ts --type=tsx -A 2 -B 2Length of output: 143 🏁 Script executed: #!/bin/bash
# Check the main entry point for initialization order
cd /repo
echo "=== Checking apps/desktop/src/main/index.ts for initDb call ==="
if [ -f "apps/desktop/src/main/index.ts" ]; then
cat "apps/desktop/src/main/index.ts"
else
echo "File not found, searching for main entry point..."
find apps/desktop/src/main -name "index.ts" -o -name "main.ts" | head -5
fiLength of output: 1268 🏁 Script executed: #!/bin/bash
# Search for direct db access patterns outside of function scopes
cd /repo
echo "=== Searching for top-level db accesses ==="
rg -n "\bdb\." --type=ts --type=tsx | grep -v "function\|const\|let\|async\|=>|//|initDb" | head -20Length of output: 127 I see the issue with the previous scripts. Let me regenerate them to work with the correct environment and settings. 🏁 Script executed: #!/bin/bash
# Check if TypeScript exists and run type check
echo "=== Checking TypeScript and running type check ==="
if command -v tsc &> /dev/null; then
tsc --noEmit 2>&1 | grep -i "error\|db" | head -30
else
echo "tsc not found, checking for lowdb types..."
find . -name "lowdb" -type d | head -5
find . -name "package.json" | head -1 | xargs grep -l "lowdb" 2>/dev/null
fiLength of output: 93 🏁 Script executed: #!/bin/bash
# Search for all db imports and usages
echo "=== Searching for db imports ==="
rg -n "from.*db/index|import.*db" --type=js -A 3 -B 1 | head -80Length of output: 32 🏁 Script executed: #!/bin/bash
# Check lowdb version and exported types in package.json
echo "=== Checking lowdb package ==="
find . -name "package.json" | xargs grep -A 2 -B 2 "lowdb" 2>/dev/null | head -20Length of output: 846 🏁 Script executed: #!/bin/bash
# Look for top-level db property access (not inside functions)
echo "=== Checking for direct db accesses ==="
rg -n "db\.\w+" --type=js -B 5 -A 2 | grep -v "function\|async\|=>\|//" | head -50Length of output: 39 🏁 Script executed: #!/bin/bash
# Verify the db/schemas file exists and check Database type
echo "=== Checking db/schemas file ==="
if [ -f "apps/desktop/src/main/lib/db/schemas.ts" ]; then
echo "=== Content of schemas.ts ==="
cat "apps/desktop/src/main/lib/db/schemas.ts"
else
find . -name "schemas.ts" -path "*db*" | xargs cat 2>/dev/null
fiLength of output: 1029 Let me search more thoroughly for db usages and verify the lowdb type behavior. [/run_scripts] [/web_search] 🏁 Script executed: #!/bin/bash
# Search with different patterns for db usage
echo "=== Searching all .ts files for db references ==="
find . -name "*.ts" -type f | xargs grep -l "db\." 2>/dev/null | head -20Length of output: 981 🏁 Script executed: #!/bin/bash
# Check the storage-ipcs handler mentioned in main entry
echo "=== Checking registerStorageHandlers ==="
find . -path "*storage-ipcs*" -name "*.ts" | head -5 | xargs cat 2>/dev/null | head -100Length of output: 646 🏁 Script executed: #!/bin/bash
# Look for any imports of db from db/index
echo "=== Searching for db imports from db/index ==="
grep -r "from.*db/index\|from.*db\"" --include="*.ts" . 2>/dev/null | head -30Length of output: 324 🏁 Script executed: #!/bin/bash
# Check if db is ever accessed at module level (not in functions)
echo "=== Checking db/index.ts for the DB type alias ==="
cat "apps/desktop/src/main/lib/db/index.ts" | head -30Length of output: 777 🌐 Web query: 💡 Result: Signature (TypeScript): async function JSONFilePreset(filename: fs.PathLike | string, defaultData: T): Promise<Low> Usage example: Sources: lowdb README and v7 release notes. [1][2] References [/run_scripts] 🏁 Script executed: #!/bin/bash
# Check how db is used in projects router
echo "=== Checking projects/projects.ts db usage ==="
cat "./apps/desktop/src/lib/trpc/routers/projects/projects.ts" | grep -A 10 -B 5 "db\."Length of output: 1307 🏁 Script executed: #!/bin/bash
# Check how db is used in workspaces router
echo "=== Checking workspaces/workspaces.ts db usage ==="
cat "./apps/desktop/src/lib/trpc/routers/workspaces/workspaces.ts" | grep -A 10 -B 5 "db\."Length of output: 3760 🏁 Script executed: #!/bin/bash
# Verify the actual type returned by JSONFilePreset
echo "=== Checking lowdb Low type structure ==="
find . -path "*node_modules/lowdb*" -name "*.d.ts" | head -5 | xargs grep -A 3 "class Low\|interface Low\|export.*Low" 2>/dev/null | head -30Length of output: 41 Fix the The lazy init + proxy pattern is sound, and all type DB = Awaited<ReturnType<typeof JSONFilePreset<Database>>>;
To fix, import import { Low } from "lowdb";
type DB = Low<Database>;Alternatively, if you prefer avoiding the extra import, use The import-time access concern is addressed by proper initialization sequencing in 🤖 Prompt for AI Agents |
||
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.
Guard against out-of-range indices in
reorderto avoid runtime errorsreorderassumesfromIndexandtoIndexare valid in the sortedworkspacesarray:If
fromIndexis out of bounds,removedbecomesundefined, andworkspace.idin the laterforEachwill throw aTypeError. Similarly, an invalidtoIndexcan insertundefinedinto the array.Add explicit bounds checks before splicing:
This prevents malformed state and makes the mutation robust to bad indices.
🤖 Prompt for AI Agents